Blog

  • Spring 단위 테스트

    테스트 종류

    Spring는 다양한 시나리오 별로, 여러 개의 테스트를 수행할 수 있는 기능을 제공한다. 테스트는 다음과 같다.

    SpringBootTest

    전체 애플리케이션 컨텍스트를 로드하여 통합 테스트를 수행한다.

    @SpringBootTest는 Spring Boot 애플리케이션을 완전하게 로드하여 실제 애플리케이션과 같은 환경에서 테스트를 수행한다. 데이터베이스, 메시지 큐, REST API 등 모든 컴포넌트를 포함한 전체 애플리케이션을 테스트하고자 할 때 유용하다.

     

    사용 예)

    @SpringBootTest private class Test() {
    @Autowired private MyService myService;
    @Test void testServiceMethod() {
    }
    }

     

    DataJpaTest

    JPA 리포지토리 관련 테스트를 위한 어노테이션이다.

    JPA와 관련된 빈만 로드하고, 데이터베이스와 관련된 테스트를 진행한다.

    사용 예)

    @DataJpaTest public class Test {
    @Autowired private UserRepository userRepository;
    @Test void test() {
    User user = new User(“John”, “john@example.com“);
    userRepository.save(user);

    User foundUser = userRepository.findById(user.getId()).orElse(null);
    assertThat(foundUser).isNotNull;
    }
    }

     

    WebMvcTest

    웹 계층(컨트롤러, HTTP 요청/응답)만 테스트를 진행하는 어노테이션이다.

    Spring MVC의 웹 컴포넌트만 로드하여 컨트롤러 테스트에 사용한다.

    사용 예)

    @WebMvcTest(Controller.class) public class Test {
    @Autowired private MockMvc mockMvc;
    @Test void testController() throws Exception {
    mockMvc.perform(get(“/api/endpoint”))
    .andExpect(status().isOk())
    .andExpect(jsonPath(“$.name”).value(“John”));
    }
    }

     

    MockBean

    목적: 테스트에서 빈을 mock으로 대체할 때 사용한다.

    설명: 실제 빈 대신 mock 객체를 주입하여 테스트에서 의존성을 격리할 때 사용한다.

    사용 예)

    @WebMvcTest(Controller.class) public class Test {
    @MockBean private MockService mockService;
    @Test void testController() throws Exception {

     

    Mockito.when(mockService.getUser()).thenReturn(new User(“John”));

    mockMvc.perform(get(“/api/endpoint”))
    .andExpect(status().isOk())
    .andExpect(jsonPath(“$.name”).value(“John”));
    }
    }

     

    TestConfiguration

    테스트 환경에서만 사용하는 설정을 정의할 때 사용한다.

    테스트 전용 설정을 작성할 수 있는 어노테이션이다.

    사용 예)

    @TestConfiguration static class MyTestConfig {
    @Bean public MyService myService() {
    return new MyServiceImpl();
    }
    }

     

    JsonTest

    JSON 직렬화/역직렬화 테스트.

    객체를 JSON으로 직렬화하거나 JSON을 객체로 역직렬화하는 로직을 테스트할 때 사용.

    사용 예)

    @JsonTest public class UserJsonTest {
    @Autowired private ObjectMapper objectMapper;

    @Test void testSerialization() throws JsonProcessingException {
    User user = new User(“John”, “john@example.com”);
    String json = objectMapper.writeValueAsString(user);
    assertThat(json).contains(“John”);
    }
    }

     

    ConfigurationPropertiesTest

    @ConfigurationProperties로 설정된 프로퍼티 클래스 테스트

    프로퍼티 값이 제대로 바인딩되는지 테스트.

    사용 예)

    @ConfigurationPropertiesTest public class MyConfigPropertiesTest {
    @Autowired private MyConfigProperties properties;

    @Test void testProperties() {
    assertThat(properties.getName()).isEqualTo(“John”);
    }
    }

     

    ActiveProfiles

    특정 프로파일을 활성화하여 테스트 환경 설정.

    설정 파일이나 환경을 테스트용으로 변경하고 싶을 때 사용.

    사용 예)

    @SpringBootTest
    @ActiveProfiles(“test”)
    public class MyServiceTest {
    @Autowired
    private MyService myService;

    @Test
    void testService() {
    // ‘test’ 프로파일 환경에서 테스트
    }
    }

     

    기타 문제 해결

    Spring Application 없이 DataJpaTest 설정 시 Configuration 관련 에러

    DataJpaTest는, JPA로 설정된 데이터들만 불러오는 것이 아니라, 어플리케이션을 초기화 하고, JPA와 관련 없는 Bean들을 무시하고 로딩되는 테스트이다. 따라서, Application 설정 파일이 존재해야만 한다. 다음과 같은 작업으로, 클래스를 만들고, 테스트 케이스를 구동할 수 있다.

    1. TestCase 내부에 Application 클래스 생성

      클래스 내부에, 임시 구동을 위한 Application을 아래와 같이 생성한다.

       

      @SpringBootApplication

      public static class TestApplication {}

       

    2. 만약 외부에 참고해야 할 Component 들이 존재한다면, 해당 경로를 설정해 준다.

      보통 SprinbBootApplication Annotation을 추가하게 되면, 자동으로 Component들을 Import 한다. 하지만, 대상이 되는 Component들이 해당 클래스의 하위 클래스 들로만 한정되기 때문에, 1번으로 인해 TestCase 내부에 생성된 TestApplication은 외부 참조를 수행하지 못하게 된다. Pakcage나 Classes 레벨로 추가할 수 있다.

       

      @ComponentScan(basePackages = {…})

       

    3. Entity 클래스와 Repository Interface의 필수 Annotation을 확인한다.

      Entity 클래스의 경우는 필수적으로 Entity Annotation이 클래스에 추가되어 있어야 하며, Repository의 경우는, Repository Annotation이나, JpaRepository<T, U>를 상속 받아야 한다.

  • Spring boot 상의 추적 로그 관리 방법

    Spring 추적 라이브러리

    Spring 추적 라이브러리는, 기존에는 개발자가 직접 만들어 사용하거나, Spring Cloud Sleuth를 사용하여 추적 로그를 수집하고, 분석하였다. 하지만, 2022년 11월 Spring Cloud 에서 해당 프로젝트를 분리하여, Micrometer Tracing 프로젝트를 만들었다. 이는 Spring Cloud Sleuth의 독립적인 복사본이며, Spring Cloud의 라이브러리 종속성을 제거하여, 완전히 분리되어 사용될 수 있는 구조를 가진 추적 라이브러리로 만들어 졌다.

    설치

    다음 Gradle 문장을 통해 Micrometer-Tracing를 사용하도록 설정할 수 있다.

    implementation platform(‘io.micrometer:micrometer-tracing-bom:latest.release’)
    implementation ‘io.micrometer:micrometer-tracing’
    (tracing 부분은, 아래 Tracer를 추가하게 되면 전이적으로 추가됨)

    이후, 필요한 추적 브리지에 따라 다음과 같이 추가로 설정을 진행한다.

    Micrometer-Tracing에서는 유연성과 확장성, 호환성 등의 이유로 아래와 같은 두가지의 Tracer를 제공하며, 목적과 필요에 따라 선택적으로 사용하면 된다.

    Tracer

    Tracer는 추적 로그를 수집하는데, 다음과 같은 기능을 제공한다.

    1. 스팬 생성
      1. 스팬(Span): Tracer는 특정 작업이나 요청을 나타내는 스팬을 생성한다. 각 스팬은 시작 시간, 종료 시간, 이름, 메타데이터(태그 등)를 포함하여 요청의 처리 과정을 추적한다.
    2. 스팬 관리
      1. 시작 및 종료: Tracer는 스팬을 시작하고 종료하는 메서드를 제공하여, 특정 작업의 결과 시간을 기록
      2. 중첩 관계 관리: 여러 스팬을 중첩하여 계증 구조를 만들 수 있고, 이를 통해, 복잡한 호출 체인을 시각화 하고 분석할 수 있음
    3. 컨텍스트 전파
      1. Request Context: Tracer는 요청 간의 컨텍스트를 전파할 수 있는 기능을 제공, 이를 통해 분산 시스템에서 여러 서비스 간의 호출을 연결할 수 있음
      2. Http 헤더 전파: Tracer는 HTTP 요청 및 응답에서 스팬 ID와 같은 정보를 포함하여 컨텍스트를 전파할 수 있음
    4. 메타데이터 추가
      1. 태그와 애노테이션: Tracer는 스팬에 추가 정보를 포함할 수 있도록 태그나 애노테이션을 추가할 수 있는 기능을 제공 (에러메시지, 응답 코드 등을 기록할 수 있음)
    5. 데이터 내보내기
      1. 외부 백엔드 Jaeger, Zipkin등으로 내보내는 기능을 제공하여 실시간 모니터링 및 분석을 가능하게 함

    Brave Tracer

    1. 간단하고 가벼운 라이브러리: Brave는 Zipkin에 기반한 경량화된 트레이싱 라이브러리로, 간단한 사용 사례에 적합
    2. Zipkin과의 통합: Zipkin과의 연동이 용이하며, 간단한 설정으로 빠르게 사용할 수 있음
    3. 전통적인 사용 사례: 기존의 Zipkin 사용자가 많은 경우, 쉽게 통합할 수 있음

    implementation ‘io.micrometer:micrometer-tracing-bridge-brave’

    Open-Telemetry Tracer

    1. 표준화된 프레임워크 : OpenTelemetry는 분산 트레이싱, 메트릭, 로그를 위한 표준화된 프레임워크로, 다양한 언어와 플랫폼에서 사용할 수 있는 통합 솔루션임
    2. 확장성 : 여러 백엔드와 쉽게 연동할 수 있으며, 사용자가 원하는 방식으로 데이터 수집 및 전송을 커스터마이즈 할 수 있음
    3. 정밀한 매트릭과 트레이싱: OpenTelemetry는 보다 정교한 트레이싱과 메트릭 수집 기능을 제공하므로, 복잡한 요구 사항이 있는 경우 성능 모니터링이 더 효과적일 수 있음
    4. 활발한 커뮤니티: 오픈 소스 프로젝트로, 큰 커뮤니티와 활발한 개발이 이루어 지고 있음

    implementation ‘io.micrometer:micrometer-tracing-bridge-otel’

    Reporter

    Micrometer는 Wavefront나 Zipkin 등의 외부 시각화 도구를 지원하며, 이런 도구들에 리포팅을 위한 도구를 제공한다.

    Wavefront Reporter

    implementation ‘io.micrometer:micrometer-tracing-reporter-wavefront’

    Brave Zipkin Reporter

    implementation ‘io.zipkin.reporter2:zipkin-reporter-brave’

    Open Telemetry Zipkin Reporter

    implementation ‘io.opentelemetry:opentelemetry-exporter-zipkin’

    Zipkin을 통해 span을 전송하기 위한 OpenZipkin URL 발신자 종속성

    implementation ‘io.zipkin.reporter2:zipkin-sender-urlconnection’

    Spring boot 설정

    설정된 Report URL은 다음과 같은 형태로 application.yml(properties)에 추가하여 설정한다.

    management.zipkin.tracing.endpoint: [URL]/api/v2/spans

     

    시각화를 위한 설정 – Zipkin

    사용 라이브러리 및 도구

    Zipkin을 사용하여, 추적성 데이터를 시각화 할 수 있으며, 대상 모듈에서는 zipkin 라이브러리 의존성 추가와 zipkin서버 정보를 입력하여 데이터를 수집할 수 있다.

    Zipkin 서버 설치

    docker를 이용한 설치

    docker run -d -p 9411:9411 openzipkin/zipkin

    설정 파일을 위한 설정

    Zipkin은 추적 정보를 DB로 관리한다. 따라서, 다음과 같은 설정을 추가하여 외부 DB를 사용하도록 설정하여 관리 데이터를 분리할 수 있다.

    environment:
        – STORAGE_TYPE=mysql
        – MYSQL_DB=zipkin
        – MYSQL_USER=zipkin
        – MYSQL_PASS=zipkin
        – MYSQL_HOST=zipkin-mysql
        – MYSQL_TCP_PORT=3306

    서버 확인

    http://localhost:9411/

     

    네트워크를 통한 호출 설정

    Micrometer는 네트워크를 통한 타 서버 호출 시, 자동으로 TraceId와 SpanId를 입력하여, 추적할 수 있는 정보를 전달한다.

    다만, 지원되는 함수는, RestTemplateBuilder, RestClient.Builder, WebClient.Builder를 지원하기 때문에, 해당 Builder를 주입 받아, Client를 생성하는 형태로 구현을 해야만, 정확한 추적 정보를 대상 서버에 전달할 수 있다.

    RestClient.Builder

    Trace정보를 입력하기 위하여 자동으로 설정된 코드는, Builder를 통해 넘어오기 때문에, 필히 Builder를 주입받아, Client를 생성하도록 구현한다.

    public class Test {
    private final RestClient client;
    public Test(RestClient.Builder builder) {
    client = builder.baseUrl(…).build();
    }
    }

  • Spring Boot – Reactive

    선언형 프로그래밍

    무엇을 할 것인가를 명시하는 방식으로 프로그래밍 접근법이며 주요 특징은 다음과 같다.

    1. 목표 지향적: 해결 하고자 하는 문제의 목표나 결과를 명확하게 정의하고 이를 어떻게 구현할지는 시스템에 맡긴다.
    2. 과정에 대한 신경을 덜 씀: 구현 세부 사항이나 처리 흐름보다는 결과만을 기술한다.
    3. 고수준의 추상화: 구현 세부 사항은 보통 프레임워크나 라이브러리, 혹은 시스템 내부에서 처리되므로, 프로그래머는 세부적인 구현에 대한 부담을 덜 수 있다.

    대표적인 선언 형 프로그램으로는, SQL, HTML, 리액티브 프로그래밍(React, Vue)등이 있다.

    프로그램 예시

    명령형 프로그램 예시

    reault = 0

    for i in range(1,6)

    result += i

    print(result)

    선언형 프로그램 예시

    SELECT SUM(value) FROM numbers WHERE value > 0;

     

    선언적 프로그래밍의 장단점

    장점

    1. 가독성 향상: 선언형 코드가 보통 명령형 코드보다 더 직관적이고 간결하다.
    2. 추상화: 프로그램의 로직을 추상화하여, 세부 구현을 신경 쓰지 않고 비즈니스 로직에 집중할 수 있다.
    3. 오류 감소: 세부적인 구현을 시스템에 맡기기 때문에, 개발자가 실수할 여지가 적다.

    단점

    1. 제어력 부족: “무엇을 할 것인가”는 정의할 수 있지만, “어떻게 할 것인가”에 대한 세부 제어는 시스템에 맡겨지므로, 때로는 특정한 방식으로 동작하도록 세부적으로 조정하는 데 한계가 있을 수 있다.
    2. 성능 최적화: 자동화된 추상화가 성능 최적화를 방해할 수 있다. 특정 상황에서는 세부적인 구현을 제어하는 것이 중요할 수 있다.

     

    Reactive 프로그래밍 코드 구성

    Reactive 프로그래밍은 비동기 데이터 흐름과 변경 가능한 데이터를 처리하는 프로그래밍 패러다임이다. 이 방식은 데이터 스트림을 처리하고, 비동기로 흐르는 데이터를 처리하는 데 초점을 맞춘다. Reactive 프로그래밍은 주로 불변성, 비동기 처리, 데이터 흐름을 강조한다.

     

    주요 개념

    1. 스트림(Stream) or 스토어(Store): 데이터의 흐름을 나타낸다. 스트림은 지속적으로 발생하는 데이터 요소들의 시퀀스로, 주로 비동기 적인 데이터 처리를 위해 사용된다.
    2. 옵저버(Observer): 스트림에서 발생하는 데이터 이벤트를 구독하고 처리하는 객체다. 옵저버는 스트림에서 방출된 데이터를 처리하는 로직을 포함한다.
    3. 발행자(Publisher): 데이터를 방출하는 객체다. Reactive 스트림에서 데이터를 생산하는 역할을 한다.
    4. 구독(Subscription): 옵저버가 스트림을 구독하여 데이터의 흐름을 처리하도록 연결하는 과정이다.
    5. 연산자(Operators): 스트림을 변환하거나 처리할 수 있는 함수적 방법들을 제공한다. 예를 들어, map, filter, merge 등의 연산자를 사용하여 데이터 스트림을 조작할 수 있다.

     

    설계 원칙

    1. 비동기 메시지 기반 통신으로 동작해야 한다.
    2. 탄력적이고 회복성을 지녀야 한다.
    3. 높은 응답성을 지녀야 한다.
    4. 유지보수와 확장이 용이해야 한다.

     

    Reactive Streams

    Reactive Streams는 비동기 데이터 흐름을 처리하고, 스트림을 통해 데이터를 효율적으로 처리하는 표준을 정의한 규격이다. 이 규격은 Backpressure를 관리하고, 비동기적으로 흐르는 데이터 스트림을 처리하기 위해 설계되었다. Reactive Streams는 주로 Reactive 프로그래밍에서 사용되며, Publisher, Subscriber, Subscription, Processor와 같은 네 가지 주요 구성 요소로 이루어져 있다.

     

    Publisher

    1. Publisher는 데이터를 방출하는 객체이다. 데이터 스트림을 생성하고, 이를 Subscriber에게 전달한다. 데이터는 비동기적으로 방출되며, Publisher는 스트림이 끝날 때까지 데이터를 지속적으로 방출할 수 있다.
    2. Publisher는 subscribe() 메서드를 사용하여 데이터를 Subscriber에게 전달한다.

    import reactor.core.publisher.Mono;

    Mono<String> publisher = Mono.just(“Hello, Reactive Streams!”);

     

    Subscriber

    1. Subscriber는 Publisher가 방출한 데이터를 처리하는 객체이다. Subscriber는 Publisher가 데이터를 방출할 때마다 이를 받아 처리하며, 각 데이터 항목에 대해 적절한 처리를 수행한다.
    2. Subscriber는 onNext(), onComplete(), onError()와 같은 메서드를 통해 이벤트를 처리한다.
    3. Subscriber는 Subscription 객체를 통해 Publisher와의 연결을 관리하고, 데이터를 요청할 수 있다.

    publisher.subscribe(new Subscriber<String>() {

    @Override

    public void onSubscribe(Subscription subscription) {

    subscription.request(Long.MAX_VALUE); // 데이터를 요청

    }

     

    @Override

    public void onNext(String item) {

    System.out.println(item); // 받은 데이터 처리

    }

     

    @Override

    public void onError(Throwable throwable) {

    System.err.println(“Error: ” + throwable.getMessage());

    }

     

    @Override

    public void onComplete() {

    System.out.println(“Completed”);

    }

    });

     

    Subscription

    1. Subscription은 Publisher와 Subscriber 간의 연결을 나타내며, Subscriber가 Publisher에 대해 데이터를 요청할 수 있는 방법을 제공한다. Subscription은 데이터 요청을 관리하고, Subscriber가 처리할 수 있는 양만큼 데이터를 방출하도록 한다.
    2. Subscription 객체는 request(long n) 메서드를 사용하여 데이터를 요청한다. 이 메서드는 Publisher가 얼마나 많은 데이터를 방출할지 제어한다.
    3. Subscription은 cancel() 메서드를 통해 구독을 취소할 수 있다.

    subscription.request(10); // 10개의 데이터 항목을 요청

    subscription.cancel(); // 구독 취소

     

    Processor

    1. Processor는 Publisher이자 Subscriber로, 데이터를 처리하면서 스트림을 변환하는 역할을 한다. Processor는 Subscriber로서 데이터를 받아 처리하고, 처리된 데이터를 다른 Subscriber에게 전달하는 Publisher 역할도 수행한다.
    2. Processor는 transform과 같은 변환 작업을 수행할 수 있으며, 주로 스트림을 필터링하거나 변경하는 데 사용된다.

    import reactor.core.publisher.BaseSubscriber;

    import reactor.core.publisher.Flux;

     

    Flux<Integer> processed = Flux.range(1, 5)

    .map(x -> x * 2); // 스트림 변환

    Reactive Streams의 흐름

    1. Publisher는 데이터를 생성하고 방출한다.
    2. Subscriber는 Publisher의 데이터를 구독하고 처리한다.
    3. Subscription은 구독 중인 데이터 흐름을 관리하고, Subscriber가 요청하는 데이터의 양을 제어한다.
    4. Processor는 데이터를 변환하거나 필터링하는 데 사용된다.

    Backpressure

    Reactive Streams의 핵심 개념 중 하나는 Backpressure이다. Backpressure는 데이터가 너무 많이 발생하거나 소비되는 속도에 따라 흐름을 제어하는 방법을 의미한다. Subscriber는 request() 메서드를 통해 자신이 처리할 수 있는 데이터 양을 제한함으로써 과도한 데이터 방출을 방지할 수 있다. 이렇게 함으로써, 시스템의 안정성과 성능을 유지할 수 있다.

     

    Non-Blocking

    Multi-Threading

    Blocking 되는 프로그램의 단점을 보완하기 위하여 CPU의 개수보다 많은 Thread를 생성하여 처리하는 방안이 만들어 졌다. 하지만, 해당 방법은 다음과 같은 단점을 가지게 된다.

    컨텍스트 스위칭으로 인한 문제

    멀티 스레딩을 처리하기 위한 기법으로, CPU가 처리할 수 있는 양보다 더 많은 요청을 처리하기 위하여 사용하는 방법이다. 컨텍스트 스위칭은 다음과 같은 과정을 수행하며 쓰레드를 전환하여 다수의 쓰레드 처리를 지원하게 된다.

    1. 현재 스레드의 상태 저장:
      1. 스레드가 CPU에서 실행 중일 때, CPU는 해당 스레드의 레지스터 값(예: 프로그램 카운터, 스택 포인터 등)과 기타 상태 정보를 저장한다. 이 정보를 컨텍스트(Context)라고 부른다.
      2. 이 상태 정보는 보통 프로세스의 PCB(Process Control Block)나 스레드의 TCB(Thread Control Block)에 저장된다.
    2. 새로운 스레드의 상태 로드:
      1. 새로운 스레드가 실행될 때, 운영 체제는 새로 실행될 스레드의 컨텍스트를 메모리에서 읽고 CPU에 로드 한다.
      2. 이때, 새로운 스레드는 이전에 저장된 상태 정보를 바탕으로 실행을 재개한다.
    3. 스케줄링:
      1. 운영 체제는 스레드 또는 프로세스를 스케줄링하여 CPU에서 실행할 순서를 결정한다.
      2. 각 스레드는 일정 시간 동안 실행되고, 시간이 다되거나, 다른 이벤트가 발생하면 다음 스레드로 전환된다.

    위와 같은 행위를 통해 다음과 같은 비용이 발생하게 된다.

    1. 저장 및 로드: 스레드 간의 상태를 저장하고 로드하는 데 시간이 걸린다. 이 과정에서 CPU 레지스터나 메모리 접근이 필요하다.
    2. 캐시 미스: 각 스레드가 실행될 때마다 CPU 캐시를 다시 채워야 할 수 있다. 이는 캐시 미스를 초래하며 성능에 영향을 줄 수 있다.
    3. 스케줄링 오버헤드: 스레드의 실행을 제어하는 스케줄러도 추가적인 오버헤드를 발생시킨다.

    Context 전환 비용이 발생한 만큼 CPU는 작업을 하지 못하게 되고, 이런 컨텍스트 스위칭이 많이 발생할수록 CPU의 전체 대기시간은 길어지기 때문에 성능이 저하된다.

     

    과다한 메모리 사용으로 오버헤드가 발생

    새로운 스레드가 실행되면 JVM에서는 해당 스레드를 위한 정보를 스택 메모리에 저장하게 된다. 기본 쓰레드 정보는 1024KB(64bit) 정도를 소모하며, 단순한 계산으로 동시 사용자 1만명이라고 하면, 대략적으로 10GB 정도의 메모리를 소모하게 된다.

     

    스레드 풀에서의 응답 지연

    스레드 생성/제거의 단점을 보완하기 위해 Thread Pool을 사용하게 된다고 하더라도, 유휴 쓰레드가 없다면, 이로 인해 응답 지연이 발생할 수 있다.

     

    함수형 인터페이스 (Functional Interface)

    Java 8부터 추가된 기능으로, 단 하나의 추상 메소드만을 가진 인터페이스를 의미한다. 함수형 인터페이스는 람다 표현식이나 메서드 참조와 함께 사용되며, 주로 클로저나 콜백 함수의 형태로 자주 활용된다.

    @FunctionalInterface 어노테이션의 경우, 함수형 인터페이스임을 명시적으로 표현하기 위하여 선택적으로 사용된다.

    예시 코드

    @FunctionalInterface

    public interface MyFunctionalInterface {

    // 추상 메서드

    void doSomething();

     

    // default 메서드 (선택적)

    default void defaultMethod() {

    System.out.println(“This is a default method.”);

    }

     

    // static 메서드 (선택적)

    static void staticMethod() {

    System.out.println(“This is a static method.”);

    }

    }

     

    사용 코드 – Lambda 식으로 사용된 예시

     

    public class Main {

    public static void main(String[] args) {

    // 람다 표현식으로 함수형 인터페이스 구현

    MyFunctionalInterface myFunc = () -> System.out.println(“Doing something!”);

     

    // 람다 표현식 호출

    myFunc.doSomething(); // 출력: Doing something!

     

    // default 메서드 호출

    myFunc.defaultMethod(); // 출력: This is a default method.

     

    // static 메서드 호출

    MyFunctionalInterface.staticMethod(); // 출력: This is a static method.

    }

    }

     

    사용 코드 – Clouser로 사용된 예시

     

    public class ClosureExample {

    public static void main(String[] args) {

    int outerValue = 20; // 외부 변수

     

    // 익명 클래스는 외부 변수 ‘outerValue’를 캡처하여 사용할 수 있다.

    MyFunctionalInterface closureExample = new MyFunctionalInterface() {

    @Override

    public void printValue() {

    System.out.println(“Outer value: ” + outerValue); // 외부 변수 ‘outerValue’를 사용

    }

    };

     

    closureExample.printValue();

    }

    }

     

    interface MyFunctionalInterface {

    void printValue(); // 함수형 인터페이스 정의

    }

     

    Callback으로 사용된 예시

     

    public class CallbackExample {

    public static void main(String[] args) {

    TaskExecutor taskExecutor = new TaskExecutor();

     

    // 람다 표현식으로 콜백 구현

    taskExecutor.executeTask(result -> System.out.println(“Callback received: ” + result));

    }

    }

     

    Lambda 표현식

    Lambda 표현식(Lambda Expression)은 자바 8에서 도입된 기능으로, 익명 함수(Anonymous Function)를 정의하는 간결한 방법이다. 이는 주로 함수형 프로그래밍(Functional Programming) 스타일의 코드를 작성할 때 사용된다. 람다 표현식을 통해 메서드를 간단하게 표현하거나 객체를 생성할 수 있으며, 코드의 가독성이나 유지보수성도 향상시킬 수 있다.

    (parameters) -> expression

    (String a, String b) -> a.equals(b);

     

    Method Reference

    Method Reference는 자바 8에서 도입된 기능으로, 람다 표현식을 좀 더 간결하게 사용할 수 있도록 돕는 문법이다. 람다 표현식이 특정 메서드를 호출하는 경우, 해당 메서드를 직접 참조하는 방식으로 코드를 간결하게 작성할 수 있다. 즉, 메서드를 람다 표현식처럼 호출하되, 메서드 이름을 직접 참조하는 방법이다.

    ClassName::methodName

     

    정적 메소드 참조

    // 람다 표현식

    Function<Integer, Integer> squareLambda = (x) -> MathUtil.square(x);

    // Method Reference

    Function<Integer, Integer> squreMethodRef = MathUtil:squre;

     

    인스턴스 메소드 참조

    StringUtil util = new StringUtil();

    // 람다 표현식

    Function<String, String> toUpperCaseLambda = (str) -> util.toUppserCase(str);

    // 메서드 레퍼런스

    Function<String, String> toUpperCaseMethodRef = util::toUpperCase;

     

    함수 디스크립터

    함수 디스크립터(Function Descriptor)는 함수의 매개변수와 반환 타입을 설명하는 데 사용된다. 일반적으로 함수형 인터페이스나 람다 표현식을 다룰 때, 함수의 시그니처(signature), 즉 어떤 타입의 입력을 받고 어떤 타입의 출력을 반환하는지에 대한 정보를 정의하는 데 사용된다.

    Function<T, R> (단일 입력, 출력 함수)

    1. 설명: Function<T, R>는 하나의 입력 매개변수 T를 받아서 결과값 R을 반환하는 함수형 인터페이스입니다.
    2. 함수 디스크립터: T -> R
    3. Function<Integer, String>에서 함수 디스크립터는 Integer -> String이다.

    import java.util.function.Function;

     

    public class FunctionDescriptorExample {

    public static void main(String[] args) {

    // Function<T, R> 인터페이스를 사용하는 예시

    Function<Integer, String> intToString = (i) -> “Number: ” + i;

    System.out.println(intToString.apply(5)); // “Number: 5”

    }

    }

    BiFunction<T, U, R> (두 입력, 출력 함수)

    1. 설명: BiFunction<T, U, R>는 두 개의 입력 매개변수 T와 U를 받아서 결과값 R을 반환하는 함수형 인터페이스입니다.
    2. 함수 디스크립터: (T, U) -> R
    3. BiFunction<Integer, Integer, Integer>에서 함수 디스크립터는 (Integer, Integer) -> Integer이다.

    import java.util.function.BiFunction;

     

    public class BiFunctionDescriptorExample {

    public static void main(String[] args) {

    // BiFunction<T, U, R> 인터페이스를 사용하는 예시

    BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;

    System.out.println(sum.apply(5, 3)); // 8

    }

    }

     

    Predicate<T> (조건 검사 함수)

    1. 설명: Predicate<T>는 하나의 입력 T를 받아서 boolean 값을 반환하는 함수형 인터페이스입니다. 주로 조건을 체크하는 데 사용됩니다.
    2. 함수 디스크립터: T -> Boolean
    3. Predicate<String>에서 함수 디스크립터는 String -> boolean이다.

    import java.util.function.Predicate;

     

    public class PredicateDescriptorExample {

    public static void main(String[] args) {

    // Predicate<T> 인터페이스를 사용하는 예시

    Predicate<String> isNotEmpty = str -> !str.isEmpty();

    System.out.println(isNotEmpty.test(“Hello”)); // true

    }

    }

    Consumer<T> (입력만 받는 함수)

    1. 설명: Consumer<T>는 하나의 입력 T를 받아서 반환값 없이 작업을 수행하는 함수형 인터페이스입니다. 주로 출력 작업이나 부수 효과(side effect)를 수행하는 데 사용됩니다.
    2. 함수 디스크립터: T -> void
    3. Consumer<String>에서 함수 디스크립터는 String -> void이다.

    import java.util.function.Consumer;

     

    public class ConsumerDescriptorExample {

    public static void main(String[] args) {

    // Consumer<T> 인터페이스를 사용하는 예시

    Consumer<String> printMessage = msg -> System.out.println(msg);

    printMessage.accept(“Hello, world!”); // “Hello, world!”

    }

    }

    Supplier<T> (출력만 하는 함수)

    1. 설명: Supplier<T>는 입력 없이 결과값 T를 반환하는 함수형 인터페이스입니다. 주로 생성이나 값 제공의 역할을 합니다.
    2. 함수 디스크립터: () -> T
    3. Supplier<String>에서 함수 디스크립터는 () -> String이다.

    import java.util.function.Supplier;

     

    public class SupplierDescriptorExample {

    public static void main(String[] args) {

    // Supplier<T> 인터페이스를 사용하는 예시

    Supplier<String> getMessage = () -> “Hello, world!”;

    System.out.println(getMessage.get()); // “Hello, world!”

    }

    }

    UnaryOperator<T> (단일 입력, 동일 타입 출력 함수)

    1. 설명: UnaryOperator<T>는 T 타입을 입력받아 같은 T 타입을 반환하는 함수형 인터페이스입니다. 즉, 입력과 출력의 타입이 동일한 함수입니다.
    2. 함수 디스크립터: T -> T
    3. 실행 함수: apply(T)
    4. UnaryOperator<Integer>에서 함수 디스크립터는 Integer -> Integer이다.

    import java.util.function.UnaryOperator;

     

    public class UnaryOperatorDescriptorExample {

    public static void main(String[] args) {

    // UnaryOperator<T> 인터페이스를 사용하는 예시

    UnaryOperator<Integer> doubleValue = n -> n * 2;

    System.out.println(doubleValue.apply(5)); // 10

    }

    }

    BinaryOperator<T> (두 입력, 동일 타입 출력 함수)

    1. 설명: BinaryOperator<T>는 두 개의 입력 T를 받아서 동일한 T 타입을 반환하는 함수형 인터페이스입니다. 즉, 두 개의 입력이 동일한 타입일 때 사용됩니다.
    2. 함수 디스크립터: (T, T) -> T
    3. 실행 함수: apply(T, T)
    4. BinaryOperator<Integer>에서 함수 디스크립터는 (Integer, Integer) -> Integer이다.

    import java.util.function.BinaryOperator;

     

    public class BinaryOperatorDescriptorExample {

    public static void main(String[] args) {

    // BinaryOperator<T> 인터페이스를 사용하는 예시

    BinaryOperator<Integer> add = (a, b) -> a + b;

    System.out.println(add.apply(5, 3)); // 8

    }

    }

    Runnable (입력 없이 실행만 하는 함수)

    1. 설명: Runnable은 입력 없이 출력 없이 실행만 하는 함수형 인터페이스입니다. 주로 쓰레드 실행에서 사용됩니다.
    2. 함수 디스크립터: () -> void
    3. 실행 함수: run()
    4. Runnable에서 함수 디스크립터는 () -> void이다.

    public class RunnableDescriptorExample {

    public static void main(String[] args) {

    // Runnable 인터페이스를 사용하는 예시

    Runnable printHello = () -> System.out.println(“Hello, world!”);

    printHello.run(); // “Hello, world!”

    }

    }

     

    마블 다이어그램

    마블 다이어그램(Marble Diagram)은 주로 리액티브 프로그래밍에서 데이터 흐름이나 시간 기반의 연산을 시각적으로 표현하는 도구이다. 마블 다이어그램은 리액티브 스트림의 연산 결과가 시간에 따라 어떻게 변하는지, 그리고 데이터가 흐르는 과정을 쉽게 이해할 수 있도록 돕는다. 이 다이어그램은 주로 RxJS(Reactive Extensions for JavaScript)나 Project Reactor(Java의 리액티브 라이브러리)에서 많이 사용된다.

    마블 다이어그램은 다음과 같은 요소들로 구성된다:

     

    1. 마블(Marble): 데이터 스트림에서 흐르는 각 이벤트나 값을 나타내는 기호. 보통 문자나 숫자로 표시되며, 이들은 시간의 흐름에 따라 나열된다.

      예: –1–2–3–|는 1, 2, 3이라는 값들이 순차적으로 발행되고 스트림이 종료되는 상태를 나타낸다.

    2. 타임축(Time axis): 시간이 흐르는 방향을 나타내는 선. 이 선을 따라 데이터가 흐르고, 연산이 어떻게 적용되는지 보여준다.
    3. 연산자(Operator): 스트림에서 발생하는 변환, 필터링, 결합 등을 나타내는 연산을 시각적으로 나타낸다. 연산자는 특정 시점에서 값들을 어떻게 변환할지 정의한다.
    4. 구독(Subscription): 데이터 스트림을 구독하는 소비자를 나타낸다. 이 구독자는 스트림에서 발행된 값을 수신하고 이를 처리한다.

    예시 Filter 연산자

    –1–2–3–4–5–| (filter x -> x % 2 == 0)

    —–2—–4—–|

     

    Sequence

    Cold Sequence와 Hot Sequence는 리액티브 프로그래밍에서 데이터 스트림의 특성을 설명하는 개념이다. 이들은 데이터 흐름의 생성 시점과 구독 시점에 따른 동작 방식을 구분하는 데 사용된다. 간단히 말하면, Cold Sequence는 구독 시점에 데이터를 생성하는 반면, Hot Sequence는 이미 데이터가 존재하며 구독자와 상관없이 데이터를 제공하는 방식이다.

    Cold Sequence (콜드 시퀀스)

    설명: Cold Sequence는 구독이 시작될 때마다 데이터를 새로 생성하는 스트림을 말한다. 즉, 각 구독자는 해당 스트림의 데이터 흐름을 처음부터 끝까지 독립적으로 받는다.

    특징:

    1. 구독자가 구독을 시작할 때마다, 데이터는 다시 처음부터 생성된다.
    2. 스트림이 구독자에 의해 “시작”되며, 각 구독자는 독립적인 데이터 흐름을 갖는다.
    3. 스트림에 대한 시작점은 구독이 일어날 때마다 초기화된다.

    예시: 파일 읽기, 네트워크 요청 등.

     

    Observable<Integer> coldObservable = Observable.create(emitter -> {

    System.out.println(“Emitting values”);

    emitter.onNext(1);

    emitter.onNext(2);

    emitter.onComplete();

    });

     

    coldObservable.subscribe(value -> System.out.println(“Subscriber 1: ” + value)); // “Emitting values” 출력 후, 값 발행

    coldObservable.subscribe(value -> System.out.println(“Subscriber 2: ” + value)); // “Emitting values” 출력 후, 값 발행

    동작

    위 코드에서 coldObservable는 각 구독자에게 독립적으로 데이터를 새로 생성한다. 즉, 첫 번째 구독자가 데이터를 받을 때마다 Emitting values 메시지가 출력되고, 두 번째 구독자도 데이터를 새로 받는다.

    Hot Sequence (핫 시퀀스)

    설명: Hot Sequence는 구독 여부와 관계없이 데이터를 미리 생성하는 스트림이다. 즉, 데이터를 이미 생성해 놓고, 구독자가 구독할 때 데이터를 지속적으로 제공한다. 구독자는 이미 존재하는 데이터 흐름에 참여하는 형태이다.

    특징:

    1. 구독자가 구독을 시작하더라도, 데이터가 이미 발행되었거나 발행 중일 수 있다.
    2. 스트림은 구독자와 관계없이 계속해서 데이터를 방출하고, 구독자는 실시간으로 데이터의 일부만 받을 수 있다.
    3. 구독자가 나중에 구독을 하더라도, 이미 지나간 데이터는 받지 못한다.

    예시: 실시간 데이터 스트림, 웹소켓, 주식 거래 데이터, 센서 데이터 등.

     

    ConnectableObservable<Integer> hotObservable = Observable.create(emitter -> {

    System.out.println(“Emitting values”);

    emitter.onNext(1);

    emitter.onNext(2);

    emitter.onNext(3);

    emitter.onComplete();

    }).publish();

     

    hotObservable.subscribe(value -> System.out.println(“Subscriber 1: ” + value)); // “Emitting values” 출력 후, 값 발행

    hotObservable.connect(); // 스트림 시작

     

    hotObservable.subscribe(value -> System.out.println(“Subscriber 2: ” + value)); // Subscriber 2는 이미 발행된 값 중에서만 수신

    동작

    hotObservable은 connect()를 호출하여 구독자들과 관계없이 데이터를 발행한다. 두 번째 구독자는 이미 발행된 값을 받을 수 없으며, 실시간으로 방출되는 데이터만 받는다.

     

    Backpressure

    Backpressure는 리액티브 스트림에서 구독자가 처리할 수 있는 속도보다 더 빠르게 데이터가 방출될 때 발생하는 문제를 다루는 개념이다. 이는 특히 리액티브 프로그래밍에서 중요한 문제로, 데이터를 너무 많이 보내서 소비자가 처리할 수 없는 상황을 방지하기 위해 사용된다.

    해결 방법

    리액티브 스트림에서 Backpressure를 다루는 주요 방법은 구독자가 처리할 수 있는 양을 알려주고, 생산자가 이에 맞춰 데이터를 보낼 수 있도록 조정하는 것이다. 이를 위해서는 Publisher와 Subscriber가 상호작용할 수 있는 Backpressure 메커니즘이 필요하다.

    리액티브 스트림은 **Reactive Streams**라는 표준을 따르며, 이를 통해 Backpressure를 제어할 수 있다. 구체적으로, onBackpressureXXX 메서드를 사용하여 스트림의 흐름을 제어한다.

     

    Reactive Streams 표준에서 Backpressure

    Reactive Streams는 구독자와 발행자 간의 백프레셔를 처리하는 표준 인터페이스를 정의하고 있다. 이 표준은 Publisher, Subscriber, Subscription, Processor를 사용하며, onBackpressureXXX 메서드를 통해 Backpressure를 다룬다.

    1. Subscriber는 onNext, onComplete, onError 메서드를 통해 데이터를 처리하고, request(n) 메서드를 통해 데이터 처리량을 제어한다.
    2. Publisher는 onSubscribe 메서드로 구독자와 연결되며, 구독자가 처리할 수 있는 데이터 양을 제어하는 방식으로 데이터를 발행한다.

     

    Backpressure 처리 방법

    1. onBackpressureBuffer(): 데이터가 구독자의 처리 속도를 초과할 때, 버퍼에 데이터를 저장하고 구독자가 처리할 수 있을 때까지 대기한다. 또한 그래도 계속 데이터가 들어올 경우, 다음 규칙에 따라 데이터를 Drop 시킨다.
      1. DROP_OLDEST
        1. 버퍼가 가득 차면 가장 오래된 데이터를 버리고 새로운 데이터를 추가한다.
        2. 최신 데이터를 우선시하며, 시스템의 메모리 사용을 최적화할 수 있다.
      2. DROP_LATEST
        1. 버퍼가 가득 차면 가장 최근의 데이터를 버리고 기존 데이터를 계속 보존합니다.
        2. 이 전략은 기존의 데이터를 우선시하며, 최신 데이터를 버리게 됩니다.
      3. BLOCK
        1. 버퍼가 가득 차면 구독자가 데이터를 처리할 때까지 기다립니다.
        2. 처리할 공간이 생길 때까지 스트림을 일시적으로 차단합니다.
      4. IGNORE
        1. 버퍼가 가득 차면 새로운 데이터를 무시합니다. 구독자는 데이터 흐름이 끊기게 됩니다.
    2. onBackpressureDrop(): 구독자가 처리할 수 없는 데이터를 버리도록 설정한다. 즉, 구독자가 처리할 수 없으면 그 데이터를 무시하고 계속 진행한다.
    3. onBackpressureLatest(): 구독자가 처리할 수 없을 때, 가장 최신 데이터만 유지하고 이전 데이터를 버린다.
    4. onBackpressureError(): 구독자가 처리할 수 없는 데이터를 발견하면 오류를 발생시킨다. 이 방법은 Backpressure를 해결하기 위해 더 이상 데이터를 발행할 수 없다는 의미로 사용된다.

     

    코드 예시

    1. 아래 예제에서는 Flux.create()를 통해 계속해서 데이터를 생성합니다.
    2. onBackpressureBuffer(10)을 사용하여 구독자가 처리할 수 없으면 최대 10개의 데이터까지 버퍼에 저장하도록 설정합니다.
    3. doOnNext()를 사용하여 각 데이터에 대해 100ms의 딜레이를 주어 처리 속도를 늦추는 효과를 시뮬레이션합니다.

     

    import org.springframework.web.bind.annotation.GetMapping;

    import org.springframework.web.bind.annotation.RestController;

    import reactor.core.publisher.Flux;

     

    @RestController

    public class BackpressureController {

     

    @GetMapping(“/backpressure”)

    public Flux<Integer> handleBackpressure() {

    // 데이터를 방출하는 Flux

    return Flux.create(emitter -> {

    for (int i = 1; i <= 100; i++) {

    emitter.next(i); // 계속해서 데이터를 생성

    }

    emitter.complete();

    })

    .onBackpressureBuffer(10) // 구독자가 처리할 수 없는 경우 최대 10개의 데이터를 버퍼에 저장

    .doOnNext(data -> {

    try {

    Thread.sleep(100); // 처리 시간이 긴 작업 시뮬레이션

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    });

    }

    }

     

    Sinks

    Sinks는 데이터를 발행하거나 수신하는 역할을 하는 리액티브 스트림의 Producer와 Consumer 사이의 중간 객체로 동작한다. 이는 Flux나 Mono와 같은 리액티브 타입에 데이터를 전송하는 데 중요한 역할을 하며, 여러 스트림(Flux, Mono)을 구독하거나 이벤트를 발행할 수 있다.

    Sinks는 Sink 인터페이스를 사용하여 여러 가지 방식으로 데이터를 다룰 수 있는 다양한 구현을 제공한다. 주로 Sinks.Many와 Sinks.One 두 가지 주요 구현이 있다.

    1. Sinks.Many: 여러 개의 데이터를 발행할 수 있는 Sink이다. 이 타입은 Flux처럼 여러 개의 항목을 비동기적으로 발행할 수 있다.
      1. UnicaseSpec
        1. 설명: unicast()는 하나의 구독자에게만 데이터를 발행할 수 있도록 설정하는 방식입니다. 즉, 구독자가 하나일 때만 데이터를 전달하며, 다른 구독자들은 해당 데이터를 받지 못합니다. 구독자가 없으면 데이터를 발행하지 않으며, 구독자가 추가되면 그때부터 데이터를 발행합니다.
        2. 특징
          1. 하나의 구독자에게만 데이터를 발행
          2. 구독자 개별적 처리가 필요할 때 사용
          3. 구독자가 없으면 데이터가 발행되지 않음
          4. 단일 구독자에 적합

          Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();

      2. Multicase()
        1. 설명: multicast()는 데이터를 여러 구독자에게 동시에 발행할 수 있도록 설정하는 방식입니다. 데이터를 한 번 발행하면, 그 데이터를 모든 구독자에게 동시에 전달합니다. 이 방식은 여러 구독자가 같은 데이터를 처리해야 할 때 유용합니다.
        2. 특징
          1. 여러 구독자에게 데이터를 동시에 발행
          2. 모든 구독자가 동일한 데이터를 수신
          3. 구독자가 추가되더라도, 이미 발행된 데이터는 받지 않음 (단, 구독 시작 이후 발행된 데이터는 모두 받음)
          4. 멀티 구독자 환경에 적합

          Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();

    2. Sinks.One: 하나의 데이터만 발행할 수 있는 Sink이다. 이 타입은 Mono처럼 한 번의 데이터 스트림을 처리하는 데 사용된다.

    Sinks 클래스는 데이터를 발행하고 완료 처리하는 다양한 메서드를 제공한다. 대표적인 메서드는 다음과 같다:

    1. emitNext(T value, EmitFailureHandler failureHandler): 데이터를 발행한다. 주어진 값을 스트림에 방출한다.
    2. emitComplete(EmitFailureHandler failureHandler): 스트림을 완료 상태로 설정한다. 더 이상 데이터가 발행되지 않음을 나타낸다.
    3. emitError(Throwable error, EmitFailureHandler failureHandler): 에러를 발생시킨다.

    주요 예제

    AsFlux를 통한 생성

    // Sinks.Many 생성
    Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
    // Flux로 데이터 발행하기
    Flux<String> flux = sink.asFlux();

     

    Schedule

    Reactive 프로그래밍에서 Scheduler는 비동기 작업이나 스트림의 실행 타이밍과 실행 스레드를 제어하는 중요한 역할을 한다. Reactive 시스템에서는 여러 작업이 비동기적으로 처리되므로, 작업이 언제 실행될지, 어떤 스레드에서 실행될지를 명시적으로 관리해야 한다. 이를 위해 Reactive 프로그래밍은 Scheduler라는 개념을 사용하여 작업 흐름을 제어한다.

    subscribeOn, publishOn, runOn등의 메소드를 통해 제어할 수 있다.

    목적

    1. Reactive 프로그래밍에서는 여러 작업이 병렬로 실행될 수 있다. 이를 제어하기 위해 스케줄링을 사용한다.
    2. 스케줄링을 통해 각 작업이 실행될 타이밍과 스레드를 제어하며, 데이터 흐름을 관리한다.
    3. 이를 통해 스레드 안전을 보장하고, 백프레셔(backpressure)와 같은 문제를 해결할 수 있다.

    역할

    1. 작업의 실행 위치를 결정: 작업이 메인 스레드, I/O 스레드, 또는 다른 스레드에서 실행될지를 지정한다.
    2. 작업의 실행 시점을 결정: 예를 들어, 특정 시간 이후에 작업을 실행하거나, 바로 실행되도록 스케줄링할 수 있다.
    3. 비동기 처리의 흐름을 제어: 비동기 작업들이 서로 충돌하지 않도록 실행 타이밍을 조절한다.

    Scheduler의 종류

    Immediate Scheduler

    작업을 현재 스레드에서 즉시 실행하도록 스케줄링한다.

    예를 들어, Schedulers.immediate()는 작업을 즉시 실행하며, 현재 스레드에서 실행된다.

    Mono.just(“Hello”)

    .subscribeOn(Schedulers.immediate()) // 즉시 실행

    .doOnNext(System.out::println)

    .subscribe();

     

    Single Scheduler

    모든 작업을 단일 스레드에서 실행하도록 스케줄링한다.

    예를 들어, Schedulers.single()은 작업을 단일 스레드에서 처리하게 되며, 주로 UI 스레드나 단일 쓰레드 처리가 필요한 경우 사용된다.

    Mono.just(“Hello”)

    .subscribeOn(Schedulers.single()) // 단일 스레드에서 실행

    .doOnNext(System.out::println)

    .subscribe();

     

    Elastic Scheduler

    유연한 스레드 풀을 사용하여 작업을 스케줄링한다. 이 스케줄러는 I/O 작업이나 비동기적인 작업에 적합하다.

    Schedulers.elastic()은 백그라운드 스레드를 사용할 때 유용하며, 여러 작업을 유연하게 분산시킬 수 있다.

    Mono.just(“Hello”)

    .subscribeOn(Schedulers.elastic()) // I/O 처리를 위한 유연한 스레드 풀 사용

    .doOnNext(System.out::println)

    .subscribe();

     

    Bounded Elastic Scheduler

    Schedulers.boundedElastic()은 Elastic 스케줄러의 개선된 버전입니다. 이 스케줄러는 동적으로 스레드를 생성하되, 최대 스레드 수를 제한하여 자원 소비를 효율적으로 관리합니다.

    스레드 풀의 크기 제한: boundedElastic()은 스레드를 동적으로 생성하지만, 최대 스레드 수를 제한합니다. 이를 통해 과도한 스레드 생성으로 인한 자원 낭비를 방지할 수 있습니다.

    블로킹 작업 처리: 주로 파일 I/O, 네트워크 I/O, 데이터베이스 쿼리와 같이 블로킹 작업을 처리하는 데 유용합니다.

    성능 최적화: 기본적으로 200개의 스레드를 생성하고, 이를 초과하면 새 작업을 대기시키며 자원 관리가 이루어집니다. 이 방식은 스레드 풀이 과도하게 커지는 것을 방지합니다.

    Mono.fromCallable(() -> {

    // 블로킹 작업 예시 (예: 파일 읽기, DB 쿼리 등)

    return “Hello from blocking operation”;

    })

    .subscribeOn(Schedulers.boundedElastic()) // 블로킹 작업에 적합한 스케줄러

    .doOnNext(System.out::println)

    .subscribe();

     

     

    Parallel Scheduler

    멀티 스레드에서 작업을 실행할 때 사용한다. 작업을 여러 스레드에서 병렬로 처리하려면 Schedulers.parallel()을 사용한다.

    이 스케줄러는 병렬 처리를 위해 스레드 풀을 사용하고, CPU 바운드 작업에 적합하다.

    Mono.just(“Hello”)

    .subscribeOn(Schedulers.parallel()) // 병렬 스레드에서 실행

    .doOnNext(System.out::println)

    .subscribe();

     

    NewThread Scheduler

    새로운 스레드에서 작업을 실행할 때 사용된다. Schedulers.newSingle()은 새로운 단일 스레드를 생성하여 작업을 실행한다. 두번째 인자를 통해 Daemon Thread (Main Thread와 Lifecycle을 같이 하는)형태로 생성이 가능하다.

    Mono.just(“Hello”)

    .subscribeOn(Schedulers.newSingle(“new-thread”, true)) // 새로운 스레드에서 실행

    .doOnNext(System.out::println)

    .subscribe();

     

    New XXX Scheduler

    이외의 New로 시작되는 모두 새로운 쓰레드 타입들을 생성하여 스케줄러를 실행한다. (newSingle, newBoundedElastic, newParallel)

     

    ExecutorService Scheduler

    주어진 ExecutorService를 기반으로 작업을 실행하는 스케줄러를 생성하는 메소드이다. 이 메소드는 사용자 정의 스레드 풀이나 기존의 스레드 풀을 활용하여 작업을 실행하고자 할 때 유용하다.

    기존의 ExecutorService 사용: 이 메소드는 사용자가 제공하는 ExecutorService를 기반으로 작업을 실행한다. 따라서 스레드 풀을 이미 설정하고 있다면, 이를 Reactor의 스케줄링에 통합할 수 있다.

    스레드 풀 관리: ExecutorService의 스레드 풀 크기와 관리 정책을 그대로 사용할 수 있다. 예를 들어, CachedThreadPool, FixedThreadPool, SingleThreadExecutor와 같은 다양한 ExecutorService 구현체를 사용할 수 있다.

    동시성 및 비동기 처리: ExecutorService에서 제공하는 비동기 작업 실행 및 동시성 기능을 그대로 활용하여 작업을 처리한다.

    // 사용자 정의 ExecutorService (예: FixedThreadPool)

    ExecutorService executorService = Executors.newFixedThreadPool(4);

     

    Mono.just(“Hello, Reactor!”)

    .subscribeOn(Schedulers.fromExecutorService(executorService)) // 사용자 정의 ExecutorService 사용

    .doOnNext(System.out::println)

    .subscribe();

    // ExecutorService 종료 (리소스 해제)

    executorService.shutdown();

     

    스케줄러 동작 타입

    subscribeOn()

    1. 스트림의 구독 시점에 스케줄러를 지정한다.
    2. 스트림 전체가 구독될 때, 구독을 처리하는 스레드를 제어한다.
    3. 첫 번째 스케줄링에 영향을 미친다.

    publishOn()

    1. 중간 연산자에서 스케줄러를 변경한다.
    2. 연산자 실행 이후의 스레드를 제어한다.
    3. 후속 연산에 영향을 미친다.

    parallel().runOn()

    1. 특정 연산을 실행할 스케줄러를 지정한다.

     

    스케줄러에 따른 동작

    기본 동작

    Flux.fromArray(…)    main thread

    .filter(…)        main thread

    .map(…)        main thread

    .subscribe(…)        main thread

     

    PublishOn

    Flux.fromArray(…)    main thread

    .publishOn(…)

    .filter(…)        A thread

    .map(…)        A thread

    .subscribe(…)        A thread

     

    2 PublishOn

    Flux.fromArray(…)    main thread

    .publishOn(…)

    .filter(…)        A thread

    .publishOn(…)

    .map(…)        B thread

    .subscribe(…)        B thread

     

    SubscribeOn

    Flux.fromArray(…)    A thread

    .subscribeOn(…)    A thread

    .filter(…)        A thread

    .map(…)        A thread

    .subscribe(…)        A thread

     

    SubscribeOn with PublishOn

    Flux.fromArray(…)    A thread

    .subscribeOn(…)    A thread

    .filter(…)        A thread

    .publishOn(…)

    .map(…)        B thread

    .subscribe(…)        B thread

     

    Context

    컨텍스트(Context)는 프로그래밍에서 주로 상태나 환경 정보를 관리하는 객체나 개념을 의미한다. 특히 리액티브 프로그래밍에서는 각각의 비동기 흐름이나 연산이 수행될 때, 해당 흐름의 상태나 정보를 유지하는 데 사용된다. 간단히 말하면, 프로그램의 실행 중에 특정한 정보나 상태를 관리하고 전달하는 방법이다.

    Context의 주요 역할

    1. 상태 전달: 비동기 작업에서 연산이 서로 다른 스레드에서 실행될 때, 해당 작업에 필요한 상태나 값을 전달하는 역할을 한다.
    2. 디버깅 및 추적: 각 작업의 상태를 추적하거나, 로그를 남길 때, 컨텍스트를 활용하여 작업 간의 연관된 정보를 추적할 수 있다.
    3. 보안 정보 전달: 인증된 사용자의 정보나 보안 토큰을 연산 흐름에 전달할 때 사용될 수 있다.
    4. 성능 최적화: 작업 간의 상태를 컨텍스트에서 관리하여, 필요할 때만 해당 정보를 로딩하거나 전달하도록 최적화할 수 있다.

    코드 예시

    Mono.deferContextual(context -> {

    String user = context.get(“user”); // Context에서 ‘user’ 정보를 가져옴

    return Mono.just(“Hello, ” + user);

    }).contextWrite(context -> context.put(“user”, “John Ddoe”));

    // 컨텍스트에 ‘user’ 추가

     

    Context에 데이터 쓰기

    contextWrite()메소드를 통해서 Context에 데이터를 쓸 수 있다.

    .contextWrite(context -> context.put(“lastName”, “Jobs”)

     

    Context에서 데이터 읽기

    Context에서 데이터를 읽는 방식은 두가지가 있다. 원본 데이터 소스 레벨에서 읽는 방식과, Operator 체인의 중간에서 읽는 방식이 있다.

    원본 데이터 소스 레벨

    원본 데이터 소스 레벨에서 데이터를 읽으려면, deferContextual()를 통해 읽으면 된다.

    Operator 체인의 중간 레벨

    중간 레벨에서 접근 하려면, transformDefferedContexual()을 통해 접근할 수 있다.

     

    Spring boot에서의 Web Flux

    Spring Boot에서는 다음과 같은 과정을 거쳐 Web Flux 비동기 Controller를 생성할 수 있다.

  • Spring boot – Quartz

    Quartz

    Java 기반의 오픈소스 “작업 스케줄링 라이브러리”이다. 이를 사용하여 특정 시간에 작업을 실행하거나 특정 간격으로 작업을 수행할 수 있다.

    이와 같이 많이 쓰이는 Spring Scheduler와의 차이점은 다음과 같다.

    Quartz

    • 매우 강력하고 유연한 스케줄링 라이브러리로, 복잡한 스케줄링 작업을 처리하는데 특화되어 있다. 예를 들어, Cron 표현식을 지원하고, 반복적인 작업, 일정한 시간 간격으로 작업을 실행하거나, 특정 시간대에 작업을 실행하는 등의 고급 기능을 제공한다.
    • Quartz는 독립적으로 사용할 수도 있으며, 자체적인 데이터베이스나 클러스터링 기능도 지원한다.
    • 데이터베이스를 통한 클러스터링이나, 분산 작업이 가능하다.

    Spring Scheduler

    1. Spring Framework의 일부로 비교적 단순한 스케줄링 작업을 다루기 위해 설계되었다. 기본적으로 @Scheduled 어노테이션을 사용하여 간단한 주기적인 작업을 설정할 수 있다. 주기적인 실행, 딜레이 및 고정된 속도 기반의 작업에 적합하다.
    2. Quartz에 비해 기능이 단순하고 복잡한 작업을 처리 하는 데는 한계가 있다.

     

    주요 클래스 및 인터페이스

    1. Job (인터페이스): 실행할 작업을 정의하는 인터페이스
    2. JobDetail (인터페이스): 실행될 작업을 정의하고 구성하는 인터페이스
    3. JobBuilder (클래스): JobDetail인스턴스를 생성하는데 사용되는 유틸리티 클래스
    4. JobListener (인터페이스): 작업의 생명 주기 동안 발생하는 이벤트를 처리하는 인터페이스
    5. JobDataMap (클래스): 작업 실행시 필요한 데이터를 저장하는 맵
    6. Trigger (인터페이스): 작업의 실행 시간을 결정하는 인터페이스
    7. CronTrigger (인터페이스): 복잡한 실행 스케쥴을 정의할 수 있는 Trigger 인터페이스
    8. TriggerBuilder (클래스): Trigger 인스턴스를 생성하는 데 사용되는 유틸리티 클래스
    9. SimpleScheduleBuilder (클래스): 간단한 실행 크세줄을 정의할 수 있는 클래스
    10. CronScheduleBuilder (클래스): 복잡한 실행 스케줄을 cron표현식으로 정의할 수 있는 클래스
    11. Schedule (인터페이스): 작업과 트리거를 관리하고 실행하는 인터페이스
    12. ScheudlerFactory (클래스): Scheduler 인스턴스를 생성하는 클래스

    Spring boot 상에서의 구현

    의존성 추가

    의존성은 org.springframework.boot:spring-boot-starter-quartz를 추가해 준다.

    설정 추가

    Spring boot의 설정 파일 (application.yml)에 다음의 설정 정보를 추가해 준다.

     

    상세 설정 정보

    1. spring.quartz.job-store-type=jdbc

      설명: Quartz가 작업과 트리거 정보를 저장할 방식으로 JDBC를 사용하도록 지정한다. 이 설정은 Quartz가 메모리 대신 데이터베이스에 정보를 저장하게 한다. 이로 인해 애플리케이션이 재시작되더라도 작업 정보를 유지할 수 있다.

       

    2. spring.quartz.jdbc.initialize-schema=always

      설명: JDBC저장소를 사용하면 다음 예와 같이 시작 시 스키마를 초기화 할 수 있다. 기본적으로 제공되는 스크립트를 사용하여 감지되고 초기화 된다. 이렇게 설정하게 되면, 기존 테이블을 삭제하고 모든 재시작 시 모든 트리거를 자동 삭제한다.

       

    3. spring.quartz.scheduler-name=SpringBootQuartzScheduler

      설명: Quartz 스케줄러의 이름을 설정한다. 이 이름은 Quartz 인스턴스를 구별할 때 사용된다. 예를 들어, 여러 개의 스케줄러가 동시에 실행되는 경우, 각 스케줄러를 구분할 수 있도록 이름을 지정할 수 있다.

       

    4. spring.quartz.properties.org.quartz.scheduler.instanceName=MyScheduler

      설명: Quartz 스케줄러의 인스턴스 이름을 설정한다. MyScheduler는 이 인스턴스를 구별하는 이름으로 사용되며, Quartz 스케줄러가 여러 개의 인스턴스를 가질 경우 이를 식별하는 데 유용하다.

       

    5. spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO

      설명: instanceId는 Quartz 스케줄러의 인스턴스를 고유하게 식별하는 데 사용된다. AUTO로 설정하면 Quartz가 자동으로 인스턴스 ID를 할당한다. 이 설정은 클러스터 환경에서 각 스케줄러 인스턴스가 고유한 ID를 가질 수 있도록 돕는다.

       

    6. spring.quartz.properties.org.quartz.threadPool.threadCount=5

      설명: Quartz 스케줄러에서 사용할 스레드 수를 설정한다. 여기서는 5개의 스레드를 사용할 수 있도록 지정했다. 여러 작업을 동시에 처리하기 위해 사용되는 스레드의 개수를 제한하는 설정이다. 작업이 많을 때 이 수치를 늘리면 성능을 개선할 수 있다.

       

    7. spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX

      설명: JobStore는 Quartz에서 작업과 트리거를 어떻게 저장할지 결정하는 클래스다. JobStoreTX는 트랜잭션을 지원하는 JobStore 클래스로, 작업 저장과 관련된 데이터베이스 트랜잭션을 처리한다. 이 설정은 JDBC를 사용할 때 트랜잭션을 지원하는 JobStoreTX를 선택하도록 지정한 것이다.

       

    8. spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate

      설명: DriverDelegate는 Quartz의 JobStore가 사용하는 데이터베이스에 맞는 SQL 문을 생성하는 클래스다. StdJDBCDelegate는 일반적인 데이터베이스 (MySQL, PostgreSQL, Oracle 등)에서 사용되는 기본 Delegate 클래스다. 이 설정은 기본적인 SQL 구문을 사용하여 데이터베이스와 상호작용하도록 지정한다.

       

    9. spring.quartz.properties.org.quartz.jobStore.dataSource=QuartzDataSource

      설명: Quartz에서 사용할 데이터 소스의 이름을 설정한다. QuartzDataSource는 Quartz가 작업과 트리거 데이터를 저장할 데이터베이스 연결을 지정하는데 사용된다. 이 설정을 통해 QuartzDataSource라는 이름의 데이터 소스를 참조하도록 설정한다.

       

    10. spring.quartz.properties.org.quartz.dataSource.QuartzDataSource.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate

      설명: 위에서 언급한 QuartzDataSource에 대한 설정이다. 이 속성은 QuartzDataSource 데이터 소스가 사용할 SQL 구문을 정의하는 DriverDelegate 클래스를 설정한다. StdJDBCDelegate는 기본적인 SQL 작업을 처리할 수 있도록 지정하는 클래스이다.

       

    11. spring.quartz.properties.org.quartz.dataSource.QuartzDataSource.URL=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1

      설명: Quartz가 사용할 데이터베이스 URL을 설정한다. 여기서는 H2 데이터베이스를 메모리 모드에서 사용하도록 설정되어 있다. jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1은 H2 데이터베이스가 메모리에 생성되며, 애플리케이션이 종료되더라도 데이터베이스가 닫히지 않도록 설정하는 URL이다. 실제 운영 환경에서는 MySQL, PostgreSQL 등을 사용할 수 있다.

       

    12. spring.quartz.properties.org.quartz.dataSource.QuartzDataSource.user=sa

      설명: Quartz가 사용할 데이터베이스의 사용자 이름을 설정한다. 여기서는 H2 데이터베이스의 기본 사용자 이름 sa로 설정되어 있다. 실제 환경에서는 데이터베이스 연결에 사용될 적절한 사용자 이름을 지정해야 한다.

       

    13. spring.quartz.properties.org.quartz.dataSource.QuartzDataSource.password=password

      설명: Quartz가 데이터베이스에 접속할 때 사용할 비밀번호를 설정한다. password는 H2 데이터베이스에 대한 기본 비밀번호다. 운영 환경에서는 데이터베이스에 적합한 비밀번호를 설정해야 한다.

       

    14. spring.quartz.properties.org.quartz.dataSource.QuartzDataSource.maxConnections=5

      설명: Quartz가 사용할 최대 연결 수를 설정한다. 5로 설정하면 데이터베이스에 동시에 최대 5개의 연결을 유지할 수 있다. 데이터베이스 연결 풀의 크기를 제한하여 리소스 관리를 최적화하는 데 유용하다.

       

    15. spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_

      설명: Quartz가 자동 생성할 테이블의 접두사를 설정한다. QRTZ_로 설정하면, 생성되는 테이블이 다음과 같이 된다. QRTZ_JOB_DETAILS, QRTZ_TRIGGERS

       

    16. spring.quartz.properties.org.quartz.jobStore.isClustered=true

      설명: 클러스터링 환경을 사용할 경우, 설정한다. 이 설정을 사용하면, 여러 대의 서버나 인스턴스가 하나의 Quartz 스케줄러로서 작업과 트리거를 공유하고 중복 없이 실행할 수 있는 조건을 만들어 준다. 이를 통해 중복 실행 방지, 부하 분산, 고가용성을 확보할 수 있다.

       

    17. spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000

      설명: 각 Quartz 인스턴스가 주기적으로 자신을 데이터베이스에 “체크인” 하는 간격을 설정한다. 이를 통해 각 인스턴스들이 자신이 정상 상태인지를 서버에 알리게 된다. 기본값은 20초이며 단위는 ms 이다.

       

    18. spring.quartz.properties.org.quartz.jobStore.maxMisfiresToHandleAtATime=5

      설명: 미처리된 작업을 처리할 때, 한번에 처리할 수 있는 최대 개수를 지정하는 옵션이다.

    Java Application Config

    QuartzConfig를 담음과 같이 생성하여, Job Schedule을 생성할 수 있다.

     

    실제 Job 클래스

    실제 작업을 수행하는 클래스이다. execute에서 작업을 수행하는 코드를 작성하여 실행한다.

    @Component

    public class MyJob implements Job {

     

    @Override

    public void execute(JobExecutionContext context) throws JobExecutionException {

    System.out.println(“Quartz Job is running…”);

    }

    }

     

    JobDetail 및 Trigger 설정

    작업의 트리거 및 작업 세부 내용을 설정하는 클래스다. 작업을 언제 시작할지 설정하고, 어떤 정보를 통해 실행할 지 등의 설정을 수행한다.

    @Configuration

    public class QuartzConfig {

     

    @Bean

    public JobDetail jobDetail() {

    return JobBuilder.newJob(MyJob.class)

    .withIdentity(“myJob”)

    .storeDurably()

    .build();

    }

     

    @Bean

    public Trigger trigger() {

    return TriggerBuilder.newTrigger()

    .withIdentity(“myTrigger”)

    .withSchedule(CronScheduleBuilder.cronSchedule(“0/5 * * * * ?”)) // 매 5초마다 실행

    .forJob(jobDetail())

    .build();

    }

     

    @Bean

    public Scheduler scheduler() throws Exception {

    SchedulerFactory schedulerFactory = new StdSchedulerFactory();

    Scheduler scheduler = schedulerFactory.getScheduler();

    scheduler.scheduleJob(jobDetail(), trigger());

    scheduler.start();

    return scheduler;

    }

    }

     

    데이터베이스를 이용한 처리

    Quartz는 기본적으로 작업과 트리거를 데이터베이스 저장할 수 있다. 단! 주의할 것이 이렇게 DB를 통한다는 의미가, 코드 수정 없이 Job, JobDetail, Trigger를 추가한다는 것이 아니라, 해당 정보의 Metadata만을 저장하고, 이를 통해 어플리케이션이 종료되거나 서버가 재시작 되더라도 스케줄링 작업을 계속 추적하고 실행할 수 있다는 것이다.

    JobDetail => JOB_DETAILS

    Trigger => TRIGGERS

    Quartz는 자체적으로 스케줄러의 상태를 저장하기 위해 필요한 테이블을 제공한다. 이를 위해Quartz의 데이터베이스 테이블을 생성하는 SQL스크립트를 제공하며, 이를 싱행하여 필요한 테이블을 설정해야 한다.

    데이터베이스는 기본적으로, H2, MySQL, PostgreSQL 등에 맞는 SQL 테이블 생성 스크립트를 제공한다.

    저장되는 정보와 동작 방식

    Job클래스는 Quartz 스케줄러가 실행할 작업을 정의한다. 이 클래스에는 실제 동작할 비즈니스 로직이 포함되어 있다.

    JobDetail객체와 Trigger객체는 Quartz 스케줄러에서 작업이 언제, 어떻게 실행될지를 정의하는 데 사용하게 된다.

    Quartz는 JobDetail 및 Trigger 정보를 데이터베이스에 저장한다. 하지만 클래스 정보 자체를 저장하지 않고, 메타데이터(작업 이름, 그룹, 클래스 이름 등)만을 저장한다.

     

    클러스터링

    클러스터링의 장점

    1. 확장성: 여러 서버가 동일한 Quartz 스케줄러를 공유하므로, 클러스터에 새로운 서버를 추가하여 처리 능력을 확장할 수 있다.
    2. 내결함성: 하나의 서버가 다운되더라도 다른 서버가 작업을 계속해서 처리할 수 있어 시스템의 고가용성을 제공한다.
    3. 작업 중복 실행 방지: 클러스터링된 환경에서, 동일한 작업이 여러 번 실행되지 않도록 보장한다.

    클러스터링 사용 시 주의점

    1. 데이터베이스: 클러스터 모드를 사용할 때는 모든 서버 인스턴스가 동일한 데이터베이스에 접근할 수 있어야 한다. 따라서, DB의 성능과 동시 처리 능력을 고려해야 한다.
    2. 네트워크 지연: 여러 서버가 분산 환경에서 통신해야 하므로, 네트워크의 지연이나 장애가 작업 처리에 영향을 미칠 수 있다.
    3. 잠금 관리: 분산 잠금 메커니즘을 통해 작업의 중복 실행을 방지하는데, 이 과정에서 잠금 경합(lock contention)이 발생할 수 있다.

    상황 별 정보

    멀티 스레드 관련

    작업이 종료되기 전에, 동일 작업에 대한 요청이 들어오게 되면, Quartz는 일단 이전 작업이 종료될 때 까지 대기하게 된다. 클러스터 환경에서도 동일하다.

    하지만, 동시 설정을 위하여 다음과 같이 설정하면, 설정한 정보를 활용하여 동시 수행이 가능하게 설정할 수 있다.

    spring.quartz.properties.org.quartz.threadPool.threadCount=[동시수행 Thread개수]

  • Spring boot – Log 설정

    Spring boot에서 사용할 수 있는 대표적인 로그 관리 프레임워크

    Log4j

    2001년도에 개발된 로그 관리 프레임워크로, XML 설정 파일을 사용하여 로깅 구성을 정의하고, 다양한 출력 형식을 지원한다.

    다음과 같은 단점을 가지며, 현재 많이 사용되지는 않는다.

    • 성능이 떨어질 수 있고, 멀티 스레드 환경에서의 안정성이 부족할 수 있다.
    • 보안 취약점 (CVE-2021-44228, “Log4Shel”)이 발견되어 사용이 권장되지 않는다.

    Log4j2

    2014년도 개발된 로그 관리 프레임워크로, Log4j2의 후속 버전으로 성능과 기능이 크게 개선되었다. 비동기 로깅을 지원하여 높은 성능을 발휘하며, 플러그인 아키텍처를 통해 유연한 확장이 가능하며, 다양한 설정 형식을 지원한다.

    주요 특징은 다음과 같다.

    • 높은 성능과 안정성, 더 나은 비동기 지원
    • API 호환성 덕분에 기존 Log4j 사용자들이 쉽게 마이그레이션 가능
    • 멀티 쓰레드 환경에서의 비동기 로거의 경우, 다른 로깅 프레임워크보다 많은 처리량과 짧은 대기 시간을 제공
    • 람다 표현식과 사용자 정의 로그 레벨을 지원
    • 필터링 기능과 자동 리로딩을 지원

    Logback

    2008년도에 개발된 로그 관리 프레임워크로, Log4j의 창시자가 개발하였다. SLF4J (Simple Logging Façade for Java)와의 통합이 잘 되어 있어, 다양한 로깅 프레임워크와 함께 사용할 수 있다.

    주요 특징은 다음과 같다.

    • 성능이 뛰어나고, 비동기 로깅을 지원
    • Log4j2보다 더 간단한 구성 및 설정을 제공
    • 로그 회전 및 압축 기능이 내장되어 있어 관리가 용이
    • Spring boot의 기본 로그 프레임워크로 내장되어 있음

    Log4j2 vs Logback

    Spring boot 반영

    Log 활성화

    Slf4j (Simple Logging Facade for Java)

    로깅 프레임워크 구현체에 대한 추상 레이어 역할을 수행하는 인터페이스로, slf4j를 사용하며 코드 수정 없이 로깅 프레임워크 구현체를 바꿀 수 있는 조건을 만들 수 있다.

    Spring boot Start – web 추가

    Spring boot Start는 기본적으로 사용되는 라이브러리들에 대한 의존성들이 포함되어 있다. spring-boot-starter-web에는 로그를 사용하기 위한 spring-boot-starter-logging 라이브러리가 포함되어 있다. 해당 라이브러리를 사용함으로 인해, 기본적으로 slf4j와 그 구현체인 logback이 포함된다.

    log4j2 의존성 수정

    spring-boot-starter-logging 라이브러리에는 logback이 포함되어 있기 때문에, 아래와 같은 설정으로 logback을 제거하고, log4j2 의존성을 추가해 준다.

    dependencies {
        implementation “org.springframework.boot:spring-boot-starter-log4j2”
    }
    configurations {
        all {
            exclude group: ‘org.springframework.boot’, module: ‘spring-boot-starter-logging’
        }
    }

    Jackson-dataformat-yaml (옵션)

    만약 설정을 yaml 파일 형태를 사용할 것이라면, 아래와 같이 yaml 파일 해석을 위한 라이브러리를 추가해 줘야 한다. 해당 설정은 필수 요소가 아니기에, 해당 의존성이 포함되어 있지 않는 것 같다. (추후 변경될 가능성 있음)

    implementation ‘com.fasterxml.jackson.dataformat:Jackson-dataformat-yaml’

    콘솔의 로그 레벨에 따른 색깔 표시

    로그 레벨에 따라 색을 칠하려면 다음과 같이 2가지 설정이 필요하다.

    1. log4j2설정에서 disableAnsi를 비활성화
      Configutaion.Appenders:Console.PatternLayout.disableAnsi: false
    2. 패턴에 highlight 패턴으로 감싸줌
      %highlight{} 패턴으로 다음과 같이 출력된 부분을 감싸 준다.
      %highlight{%-level}

    위와 같이 설정하면, 설정된 부분에 로그 레벨에 따라 미리 시스템에 의해, 혹은 사용자 정의로 설정된 색으로 구문을 표시해준다.

    Lombok을 통한 Logging

    Lombok 추가 후, @Slf4j 어노테이션을 추가하면, 별도의 추가 작업 없이 바로 log 명령어를 통해 로거에 접근할 수 있다.

    Logging.Properties를 통한 로깅 사용 자제

    Spring boot 에서는 logging.properties를 통한 로그 구성 파일을 제공하는 기능을 지원하지만, jar에서 실행할 때 문제를 일으키는 알려진 클래스 로딩 문제가 있기 때문에 해당 방법을 사용하는 것에 신중해야 한다.

    Log4j2 설정

    Configuration:
    Appenders:
    Console:
    name: Console_Appender
    target: SYSTEM_OUT
    PatternLayout:
    charset: UTF-8
    pattern: “%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”
    disableAnsi: false
    RollingFile:
    name: RollingFile_Appender
    fileName: ./logs/auth-server.log
    filePattern: ./logs/auth-server-%d{yyyy-MM-dd}-%i.log
    PatternLayout:
    charset: UTF-8
    pattern: “%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”
    disableAnsi: false
    Policies:
    TimeBasedTriggeringPolicy:
    interval: 1
    modulate: true
    SizeBasedTriggeringPolicy:
    size: 10MB
    DefaultRolloverStrategy:
    max: 10
    Loggers:
    Root:
    level: debug
    AppenderRef:
    ref: Console_Appender
    ref: RollingFile_Appender

    Async Logger 활성화

    AsyncLogger설정을 통한 Async Logger 활성화를 수행한다.

    Configuration:
    

    ...
    Loggers:
    AsyncLogger:
    name: asnyc-logger
    level: info
    additivity: false
    AppenderRef:
    - ref: RollingFile_Appender

    에러 처리

    AsyncLogger를 추가했을 경우, 아래와 같은 에러가 발생할 수 있다. 이 경우엔, 필요로 하는 라이브러리를 참조 추가하여 해결한다.

    에러)
    Caused by: java.lang.ClassNotFoundException: com.lmax.disruptor.EventHandler
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)

    수정 사항)

    // Log4j2 설정
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'
    implementation 'com.lmax:disruptor:3.4.2'

     

     

    성능 개선 확인 

    Log4j2 – sync 방식의 성능 

    관련 코드 

        /* 

         * 테스트 로그를 쌓음 

         */ 

        @GetMapping(“/writeLogAsync”) 

        @Async 

        public CompletableFuture<String> writeLogAsync() { 

            var startDateTime = LocalTime.now(); 

            for (int i = 0; i < 100000 ; i++) { 

                asyncLogger.warn(“LOG – {}”, i); 

            } 

            var endDateTime = LocalTime.now(); 

            asyncLogger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

            return CompletableFuture.completedFuture(“{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”); 

        } 

     

    성능 확인 

    15:52:07.402 [http-nio-8200-exec-2] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9076 ms] 

    15:52:27.411 [http-nio-8200-exec-3] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9017 ms] 

    16:35:45.800 [http-nio-8200-exec-5] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9439 ms] 

     

    Log4j2 – async 방식의 성능 

    관련 코드 

        /* 

         * 테스트 로그를 쌓음 

         */ 

        @GetMapping(“/writeLog”) 

        public String writeLog() { 

            var startDateTime = LocalTime.now(); 

            for (int i = 0; i < 100000 ; i++) { 

                syncLogger.warn(“LOG – {}”, i); 

            } 

            var endDateTime = LocalTime.now(); 

            syncLogger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

            return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

        } 

    성능 확인 

    15:49:37.397 [http-nio-8200-exec-2] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 665 ms] 

    15:49:56.652 [http-nio-8200-exec-3] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 632 ms] 

    15:50:18.316 [http-nio-8200-exec-4] INFO  com.wemade.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 617 ms] 

     

    특이 사항 

    1. Async 방식의 특징에 따라, 결과 리턴 후, 계속 로그가 쌓여가는 현상이 발생함

     

    Logback – sync 방식의 성능 

    관련 코드 

        /* 

         * 테스트 로그를 쌓음 

         */ 

        @GetMapping(“/writeLog”) 

        public String writeLog() { 

            var startDateTime = LocalTime.now(); 

            for (int i = 0; i < 100000; i++) { 

                logger.warn(“LOG – {}”, i); 

            } 

            var endDateTime = LocalTime.now(); 

            logger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

            return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

        } 

    성능 확인 

    10:24:47.661 [http-nio-8200-exec-2] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11259 ms] 

    10:25:06.786 [http-nio-8200-exec-3] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11742 ms] 

    10:25:27.619 [http-nio-8200-exec-4] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 11049 ms] 

     

     

    Logback – async 방식의 성능 

    관련 코드 

        /* 

         * 테스트 로그를 쌓음 

         */ 

        @GetMapping(“/writeLog”) 

        public String writeLog() { 

            var startDateTime = LocalTime.now(); 

            for (int i = 0; i < 100000; i++) { 

                logger.warn(“LOG – {}”, i); 

            } 

            var endDateTime = LocalTime.now(); 

            logger.info(“End DateTime : {} ==> {}”, startDateTime, endDateTime); 

            return “{ S : ” + startDateTime.toString() + “, E : ” + endDateTime.toString() + ” }”; 

        } 

     

    설정 파일 변경 내역 

    Gradle의 logback classic 라이브러리 추가 
    implementation ‘ch.qos.logback:logback-classic’; 

    AsyncAppender 로 기존 로그 Appender Wrapping 

            <!– Async Appender –> 

            <appender name=”FILE-ASYNC” class=”ch.qos.logback.classic.AsyncAppender”> 

                <appender-ref ref=”FILE” /> 

                <includeCallerData>false</includeCallerData> 

                <neverBlock>false</neverBlock> 

            </appender> 

    성능 확인 

    10:45:30.398 [http-nio-8200-exec-1] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9704 ms] 

    10:45:49.463 [http-nio-8200-exec-2] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 8968 ms] 

    10:46:07.141 [http-nio-8200-exec-3] INFO  c.w.cq.webapi.aspect.autoLogAspect – [<< END >> com.wemade.cq.webapi.controller.TestController::writeLog() – Duration elapsed : 9481 ms] 

     

    특이 사항 

    비동기 방식이지만, Log4j2 – async 방식과는 다르게 모든 로그가 출력된 이후, 결과가 리턴 되고 있음 

  • Spring Boot – CORS 설정

    Spring MVC상의 CORS 설정

    Bean 생성을 통한 설정

    /**
    * CORS
    관련
    설정
    정보
    클래스

    */
    @Configuration
    public class CorsConfig {
    /**
    * CORS
    설정
    *
    * @return CORS
    설정

    */
    @Bean
    public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
    @Override
    public void addCorsMappings(@SuppressWarnings(“null”) @NonNull CorsRegistry registry) {
    registry
    .addMapping(“/**”)
    .allowedMethods(“GET”, “POST”, “PUT”, “PATCH”, “DELETE”)
    .allowedOrigins(“*”);
    }
    };
    }
    }

     

    Spring Webflux 상의 CORS 설정

    Interface 상속을 통한 설정

    /**
    * CORS
    관련 설정 정보 클래스

    */
    @Configuration
    public class CorsConfig implements WebFluxConfigurer {
    /**
    * CORS
    설정
    * @param registry CORS
    설정

    */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry
    .addMapping("/**")
    .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
    .allowedOrigins("*");
    }
    }

     

  • Spring – 프로젝트 분할

    Spring 프로젝트를 분할하여 라이브러리 한 후, 불러오는 방법, 그리고, 외부 라이브러리 저장소 (Nexus)등으로 올린 후, 불러와서 사용하는 법에 대하여 다루어 본다.

    Maven 저장소 타입

    Java 프로젝트는 Meven저장소를 이용하여 자바 프로젝트의 의존성과 아티팩트 배포를 관리한다. 저장소는 크게 다음과 같은 저장소로 나눌 수 있다.

    1. 로컬 저장소 (Local Repository)
    • 개발자가 자신의 시스템에 보유한 Maven 저장소
    • 기본적으로 Maven은 ~/.m2/repository 또는 C:\Users\<username>\.m2\repository 경로에 로컬 저장소를 생성함
    • 프로젝트 빌드를 실행할 때, Maven은 먼저 로컬 저장소에서 의존성 라이브러리를 찾음
    1. 중앙 저장소 (Central Repository)
    • Maven의 기본 저장소로 Apache에서 관리하는 중앙 서버
    • 전 세계 개발자들이 만든 수많은 오픈 소스 라이브러리, 플러그인, 아티팩트 들이 저장되어 있음
    • Maven의 기본 설정에 포함되어 있으며, https://repo.maven.apache.orb/maven2/ 주소로도 접근 가능함
    1. 원격 저장소 (Remote Repository)
    • Apache에서 관리하는 중앙 저장소 외에 다른 서버에서 호스팅 되는 Maven 저장소로 기업 내부에서 사용하는 개인 저장소나, 다른 공개 저장소들이 이에 해당함
    • Nexus나 Artifactory 같은 시스템을 이용해 자체적으로 Maven 저장소를 운영
    • 프로젝트에서 필요한 의존성이 로컬 저장소에 없을 경우, 해당 서버에서 의존성을 다운로드 함

     

    Maven 저장소의 기본 역할

    Maven 저장소의 기본 역할은 다음과 같다.

    1. 의존성 관리 : 프로젝트에서 필요한 라이브러리, 플러그인 등의 아티팩트를 쉽게 관리할 수 있음, 라이브러리는 groupId, artifactId, version으로 고유하게 식별됨
    2. 아티팩트 배포 : Maven 저장소에 라이브러리를 업로드 하여 다른 프로젝트에서도 사용할 수 있도록 함
    3. 버전 관리 : 러이브러리의 여러 버전이 한 저장소에 같이 존재할 수 있으며, Maven은 필요에 맞는 버전을 자동으로 다운로드 하여 의존성을 주입할 수 있음

     

    빌드 자동화와 의존성 관리 도구

    Maven

    Maven은 빌드를 설정할 때 pom.xml파일을 사용하며, 이 파일은 선언적 방식으로 의존성, 플러그인, 빌드 설정 등을 명시한다.

    Maven은 다음과 같은 특징을 가지고 있다.

    1. Maven은 XML 기반의 선언적 빌드 도구이다.
    2. pom.xml 파일에 프로젝트의 의존성, 플러그인, 빌드 설정 등을 정의한다.
    3. 프로젝트 모델을 엄격하게 정의하며, 표준화된 디렉터리 구조와 생명 주기를 따른다.
    4. 의존성 관리는 중앙 저장소(Maven Central)에서 제공하며, XML 설정으로 의존성을 선언한다.
    5. 의존성을 groupId, artifactId, version의 형식으로 정의한다.
    6. 단 방향 빌드를 사용하기 때문에, 빌드 과정이 선형적으로 진행되며, 이로 인해 빌드 속도가 상대적으로 느릴 수 있다.

    코드 예시

    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.10</version>
    </dependency>
    </dependencies>

     

    Gradle

    Gradle은 build.gradle 또는 build.gradle.kts 파일을 사용하며, Groovy 또는 Kotlin DSL을 사용하여 빌드 과정을 명령형으로 구성할 수 있다. 이는 더 유연하고 프로그램적으로 제어할 수 있는 장점을 가진다.

    Gradle은 다음과 같은 특징을 가지고 있다.

    1. Gradle은 **DSL(Domain-Specific Language)**을 사용하는 빌드 도구이다. 기본적으로 Groovy 또는 Kotlin DSL을 사용해 build.gradle 또는 build.gradle.kts 파일을 작성한다.
    2. 동적이고 유연한 빌드 시스템으로, XML 기반의 선언적 방식과 달리 스크립트 기반의 명령형 빌드 설정을 사용한다.
    3. Gradle은 Maven보다 더 유연하고 확장성이 뛰어나며, 성능도 우수하다는 평가를 받는다. Gradle은 병렬 처리나 캐싱을 통해 빌드 속도를 최적화할 수 있다.
    4. Gradle은 Maven과 동일한 방식으로 의존성을 관리하지만, 더 유연한 방식으로 추가적인 저장소나 의존성 전략을 설정할 수 있다.
    5. 병렬 빌드와 캐시를 활용하여 빌드 성능을 크게 개선할 수 있다. Incremental Build와 Build Caching을 지원하여 동일한 작업을 반복 할 때 속도 개선 효과를 얻을 수 있다.

     

    코드 예시

    dependencies {
    implementation ‘org.springframework:spring-core:5.3.10’
    }

     

    도구 비교

    항목

    Maven

    Gradle

    설정파일

    pom.xml (XML)

    build.gradle (Groovy/Kotlin DSL)

    의존성 관리

    중앙 저장소 (Maven Central) 사용

    중앙 저장소와 다양한 커스텀 저장소 지원

    성능

    상대적으로 느림

    명렬 빌드, 캐싱, 성능 최적화

    유연성

    표준화된 방식, 덜 유연함

    높은 유연성, 커스터마이징 가능

    빌드 모델

    선언적 방식 (표준화된 생명주기)

    명령형 방식 (스크립트 기반)

    플러그인 시스템

    표준화된 플러그인 사용

    강력하고 유연한 플러그인 시스템

    커스터마이징

    제한적

    자유롭게 커스터마이즈 가능

    주요 사용 사례

    표준화된 대규모 프로젝트

    성능 최적화가 필요한 프로젝트, Android 개발 등

     

    프로젝트 분할

    Gradle을 이용하여 프로젝트 빌드 설정을 진행하며, 기존에 개발된 하나의 프로젝트를 기반으로 소스를 분리하고, 프로젝트를 분리하여 의존성을 설정하는 방법으로 프로젝트를 분리하려 한다.

    분할 목표 프로젝트 구조

    분할된 프로젝트는 루트 프로젝트서브 프로젝트로 나누어 볼 수 있다.

    1. 루트 프로젝트는 전체 프로젝트의 루트 디렉토리에 존재하는 프로젝트를 의미하며, 모든 서브 프로젝트의 설정을 포함한다.
    2. 서브 프로젝트는 루트 프로젝트 아래에 위치한 개별적인 모듈을 말하며, 별도의 build.gradle파일을 가지며, 여기에서 서브 프로젝트의 빌드 설정 및 의존성을 관리한다.

    기본 구조는 다음과 같다.

    my-multi-project/

    ├── settings.gradle // 프로젝트 설정
    ├── build.gradle // 루트 프로젝트의 빌드 설정

    ├── module1/ // 서브 프로젝트 1
    │ └── build.gradle

    ├── module2/ // 서브 프로젝트 2
    │ └── build.gradle

    └── module3/ // 서브 프로젝트 3
    └── build.gradle

     

     

     

    settings.gradle 파일 설정

    Settings.gradle파일은 Gradle이 멀티 프로젝트를 인식하고, 각 서브 프로젝트를 포함하도록 지정하는 파일이다. 이 파일에서 모든 서브 프로젝트를 정의한다.

    코드 예시

    // 루트 프로젝트의 이름
    rootProject.name = ‘my-multi-project’

    // 서브 프로젝트 지정
    include ‘module1’, ‘module2’, ‘module3’

     

    루트 프로젝트의 build.gradle 설정

    루트 프로젝트의 build.gradle에서는 공통 설정이나 모든 서브 프로젝트에 적용될 설정을 정의할 수 있다. 예를 들어, 모든 서브 프로젝트에 공통 의존성을 추가하거나 플러그인을 적용할 수 있다.

    코드 예시

    // 루트 프로젝트를 포함한 모든 프로젝트에 적용될 설정
    allprojects {
    repositories {
    mavenCentral() // 모든 서브 프로젝트에서 Maven 중앙 저장소를 사용
    }

    dependencies {
    // 모든 프로젝트에서 공통으로 사용할 의존성 설정 (예: logging 라이브러리)
    implementation ‘org.slf4j:slf4j-api:1.7.32’
    }
    }

    subprojects {
    // 서브 프로젝트들에 대해서만 적용되는 설정
    apply plugin: ‘java’

    // Java 플러그인 적용 후 공통 설정
    sourceCompatibility = ‘1.8’
    targetCompatibility = ‘1.8’

    // 서브 프로젝트에서만 사용하는 추가 의존성
    dependencies {
    implementation ‘org.springframework:spring-context:5.3.10’
    }
    }

     

    서브 프로젝트의 build.gradle 설정

    각 서브 프로젝트는 독립적인 build.gradle 파일을 가지며, 해당 파일에서는 해당 모듈에서만 필요한 빌드 설정을 추가한다.

    코드 예시

    apply plugin: ‘java’

    dependencies {
    // 이 모듈에만 필요한 의존성 추가
    implementation project(‘:module2’) // 다른 서브 프로젝트를 의존성으로 추가
    }

     

    IntelliJ에서의 프로젝트 설정

    IntelliJ에서는,

    1. 루트 프로젝트를 “New Project”로 생성
    2. 적절한 위치에, “New -> Module” 메뉴를 통해 서브 프로젝트 생성
    3. 서브 프로젝트 정리 시에, “gradle” 폴더와, “build.gradle” 파일은 제거하지 말 것

    빌드 방법

    위와 같은 방식으로 분할된 프로젝트를 빌드할 때는 다음과 같은 명령으로 다양한 형태로 빌드를 수행할 수 있다.

    1. 루트 프로젝트에서 전체 빌드할 때,

      ./gradlew build

    2. 특정 서브 프로젝트만 빌드할 때,

      ./gradlew :bodule1:build

       

    라이브러리 배포 설정

    이렇게 분할된 서브 프로젝트는, 위에서 설명한 Maven 중앙 저장소나, 로컬 저장소, 혹은 사내 커스텀 저장소에 올려 배포할 수 있다.

    해당 설정은 서브 프로젝트의 성격에 따라 루트 프로젝트나 서브 프로젝트에 설정을 추가할 수 있다.

    로컬 저장소에 배포

    코드 예시

    apply plugin: ‘java’
    apply plugin: ‘maven-publish’

    publishing {
    publications {
    mavenJava(MavenPublication) {
    from components.java
    }
    }
    repositories {
    maven {
    url = uri(“file://${buildDir}/repo”) // 로컬 저장소로 배포
    }
    }
    }

     

    사설 저장소에 배포

    다음과 같은 형태로, https://repo.repository.com/repository/maven-releases 주소를 가지는 사설 저장소에, user1 / password 계정 정보로 배포하는 설정은 다은과 같이 설정할 수 있다.

    코드 예시

    gradle.build

    apply plugin: ‘java’
    apply plugin: ‘maven-publish’

    repositories {
    mavenCentral()
    }

    dependencies {
    implementation ‘org.springframework:spring-context:5.3.10’
    }

    // 배포 설정
    publishing {
    publications {
    mavenJava(MavenPublication) {
    from components.java // Java 컴포넌트(라이브러리)를 배포
    }
    }

    repositories {
    maven {
    name = “MyPrivateRepo” // 저장소 이름 (임의 설정 가능)
    url = uri(“https://repo.repository.com/repository/maven-releases/”) // 사설 저장소 URL

    credentials {
    username = project.findProperty(“repoUser”) ?: “user01” // 사용자명 (프로젝트 속성 또는 기본값)
    password = project.findProperty(“repoPassword”) ?: “password” // 비밀번호 (프로젝트 속성 또는 기본값)
    }
    }
    }
    }

     

    repoUser, repoPassword 는 다음과 같이 외부로 추출하여 보안을 강화할 수도 있다.

    gradle.properties

    repoUser=user1
    repoPassword=password

     

    버전 설정 방법

    아래와 같이 버전을 Project Property로 받아올 수 있게 변경한 후, 빌드 명령 뒤에 –Pversion 형태로 인자를 전달하면 된다.

     

    [build.gradle]

    version = project.hasProperty(‘version’) ? project.version : ‘0.0.1’

     

    C:\> .\gradlew.bat build -Pversion=”[VERSION]”
    C:\> .\gradlew.bat publish -Pversion=”[VERSION]”

     

    배포 방법

    이렇게 분할되고, 배포될 저장소 설정 까지 마무리 되었으면, 아래 명령을 통해 배포를 수행할 수 있다.

    ./gradlew publish

     

    버전 및 배포 파일에 대한 설명 추가 방법

    배포 시, 해당 프로젝트의 정보를 추가하려면, 프로젝트에 다음과 같이 설정하면 된다.

    publishing {
    publication {
    mavenJava(MavenPublication) {

    // 1. Group ID 설정
    groupId = ‘com.company.lib’

    // 2. Artifact ID 설정
    artifactId = ‘module1’

    // 3. Version 설정
    version = ‘0.0.1’

    // 4. Artifact 파일 정의
    artifact(“$buildDir/build/libs/module.jar”)

    // 5. pom 설정
    pom {
    // 5-1. 프로젝트 이름 설명
    name.set(“Module 1”)

    // 5-2. 프로젝트 상세 설명
    description.set(“Common Module”)

    // 5-3. URL 설명
    url.set(https://common.campany.com/module1)

    // 5-4. Licnese설명
    licenses {
    license {
    name.set(“The Apache License, Version 2.0”)
    url.set(http://www.apache.org/licenses/LICENSE-2.0)
    }
    }

    // 5-5. 개발자 정보
    developers {
    developer {
    id.set(“johnkim”)
    name.set(“John Kim”)
    email.set(john@company.com)
    }
    }

    // 5-6. 저장소 정보
    scm {
    developerConnection.set(“scm:git:ssh://git@gitlab.company.com/common/module1.git”)
    url.set(https://gitlab.company.com/common/module1.git)
    }

    // 5-7. POM 정보 생성
    pom.withXml pomfileManualGenerator

    }
    }
    }
    }

  • Redis 서버 연동 방안

    Redis를 이용한 시스템 개선

    확인 사항

    Redis는 인 메모리 데이터 저장소로, 빠른 속도와 다양한 데이터 구조의 지원, 고 가용성과 복제, 분산 처리 기능들을 제공한다.

    CAP (Consistency, Availability, Partition Tolerance)

    CAP 개념은 분산 서버에서 일관성이 유지되지 못한다는 개념으로, CAP 즉, 일관성 (Consistency), 가용성 (Availability), 파티션 내구성(Partition Tolerance) 중에서 두 가지를 동시에 만족할 수 없다는 것을 제시하고 있다.

    1. 일관성 (Consistency)
      1. 모든 클라이언트가 항상 동일한 데이터를 보도록 보장
      2. 어떤 클라이언트가 데이터를 수정하면, 모든 클라이언트는 즉시 반영하여 항상 일관성을 유지해야 한다.
    2. 가용성 (Availability)
      1. 모든 요청에 대해 응답을 제공할 수 있는 상태이며,
      2. 서버는 클라이언트의 요청에 항상 유효한 데이터를 반환 받을 수 있어야 한다.
    3. 파티션 내구성 (Partition Tolerance)
      1. 네트워크 분할이 발생하더라도 시스템이 계속 운영될 수 있는 능력이며,
      2. 일부 노드가 서버로 통신할 수 없는 상황에서도 시스템이 계속 작동해야 한다.

    이에 따라 적절한 합의점을 도출하고 있으며,

    1. NoSQL 데이터베이스의 경우, 가용성과 파티션 내구성을 우선시하여, 일관성을 느슨한 모델을 사용하고 있으며,
    2. RDBMS의 경우, 강력한 일관성을 제공하지만, 네트워크 파티션이 발생하면, 일부 요청이 차단되는 모델을 사용하고 있다.

    분산 시스템의 일관성은 항상 유지되지 않을 수 있으며, 이는 CAP정리에 의해 설명된다. 시스템 설계 시 요구되는 특성에 따라 일관성, 가용성, 파티션 내구성 중 어떤 것을 우선시 해야 할지를 결정해야 한다.

    Redis의 CAP모델

    Redis는 주로 일관성과 가용성 모델을 따르며, 특히 최종 일관성 (Eventual Consistency)개념을 기반으로 운영된다.

    CAP정리에 따른 Redis의 특징은 다음과 같다.

    1. 일관성
      1. Redis는 기본적으로 단일 스레드 모델을 사용하여 요청을 순차적으로 처리합니다. 이를 통해 기본적인 일관성을 보장합니다. 그러나 클러스터 모드에서는 데이터 복제가 발생하며, 리더-팔로워 구조를 사용할 경우, 마스터 노드의 데이터 변경이 모든 팔로워에게 즉시 반영되지 않을 수 있어 eventual consistency를 따르게 됩니다
    2. 가용성
      1. Redis는 높은 가용성을 목표로 설계되었습니다. Redis Sentinel이나 Redis Cluster를 사용하여 장애 조치(failover) 기능을 제공하며, 장애 발생 시 다른 노드로 요청을 자동으로 전환할 수 있습니다. 그러나 노드가 분리되면 가용성이 저하될 수 있습니다.
    3. 분할 허용성
      1. Redis는 네트워크 파티션이 발생하면, 일부 노드와의 연결이 끊길 수 있습니다. 이 경우, Redis 클러스터는 특정 노드가 불능 상태일 때 해당 노드에 대한 쓰기 작업이 실패할 수 있으며, 읽기 작업 또한 제한될 수 있습니다. 따라서, 분할 허용성은 Redis의 중요한 고려사항 중 하나입니다.

    결론적으로, Redis는 CP (Consistency and Partition Tolerance) 또는 AP (Availability and Partition Tolerance) 모델로 운영될 수 있다.

     

    Redis 설치

    Redis Server – Docker 설치

    Docker Container 생성

    $ docker run –p 6379:6379 –name docker_redis redis

    Docker Container 상태 확인

    $ docker ps –a

    Docker를 통한 redis 접속

    $ docker exec –it docker_redis /bin/bash
    root@redis:/data# redis-cli

    Redis-cli를 통한 redis 접속

    Redis-cli는 독립적인 설치 파일을 제공하지 않는다. (Windows) 다만, 비슷한 기능을 제공하는 GUI툴을 통한 접속을 진행할 수 있다. 유사 기능을 제공하는 GUI툴은 다음과 같다.

    1. RedisInsignt : Redis Lab에서 제공하는 GUI 툴로, 데이터베이스를 시각적으로 관리할 수 있다. (Windows Store를 통해 설치 가능)
    2. Medis: Mac, Windows에서 사용할 수 있는 오픈 소스 Redis GUI클라이언트로, 직관적인 인터페이스가 장점이다.
    3. Another Redis DeskTop Manager (ARDM): Windows, Mac, Linux에서 사용할 수 있는 크로스 플랫폼 Redis GUI 클라이언트로, 다양한 기능을 제공하며 Redis 데이터를 쉽게 관리할 수 있다.

     

    Redis – Distributed Lock

    다중 스레드 환경에서, 특정 자원에 대해 동시에 한 작업만 접근이 가능하도록 강제하도록 설정하는 기능을 Lock (Mutex, 상호 배제) 이라고 한다.

    다만, 분산 환경에서 구현을 위해서는, 서버 간의 상호 배제성을 확보해야 한다. Redisson 은 이런 분산된 서버 환경에서, Redis 서버를 활용하여, 분산된 서버 환경 내에서의 상호 배제성을 확보할 수 있는 기능을 제공한다.

    Redisson 구현

    아래와 같이 Redisson 라이브러리를 implementation 구문을 통해 Dependency를 추가해 준다.

    implementation ‘org.redisson:redisson-spring-boot-starter’

    application.settings를 통한 Redis 설정

    Spring boot 3.X +

    spring:
    data:
    redis:
    database:
    host:
    port:
    password:
    ssl:
    timeout:
    connectTimeout:
    clientName:
    cluster:
    nodes:
    sentinel:
    master:
    nodes:

    Spring boot 2.7.X

    spring:
    redis:
    database:
    host:
    port:
    password:
    ssl:
    timeout:
    connectTimeout:
    clientName:
    cluster:
    nodes:
    sentinel:
    master:
    nodes:

    외부 파일을 통한 Redisson 설정

    spring:
    redis:
    redisson:
    file: classpath:redisson.yaml

    인라인 문장을 통한 Redisson 설정

    spring:
    redis:
    redisson:
    config: |
    clusterServersConfig:
    idleConnectionTimeout: 10000
    connectTimeout: 10000
    timeout: 3000
    retryAttempts: 3
    retryInterval: 1500

    설정 클래스를 통한 Redisson 설정

    /*
    * RedissonClient Configuration
    */
    @Configuration
    public class RedissonConfig {
    @Value(“${spring.redis.host}”)
    private String redisHost;

    @Value(“${spring.redis.port}”)
    private int redisPort;

    private static final String REDISSON_HOST_PREFIX = “redis://”;

    @Bean
    public RedissonClient redissonClient() {
    RedissonClient redisson = null;
    Config config = new Config();
    config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + “:” + redisPort);
    redisson = Redisson.create(config);
    return redisson;
    }
    }

    참고 사이트

    https://redisson.org/docs/integration-with-spring/#spring-boot-starter

     

     

    Redis Cache

  • Apache Avro

    Avro 정의

    Avro란 Apache에서 만든 데이터 직렬화 프레임워크로, Apache Hadoop 및 Apache Kafka와 같은 분산 시스템에서 데이터를 효율적으로 저장하고 전송하기 위해 설계되었다. Avro의 가장 큰 특징이자 장점은, 효율성, 호환성, 언어 지원 이다.

    주요 특징

    Avro는 다음과 같은 주요 특징을 가진다.

    1. 스키마 기반 : Avro의 가장 큰 특징을 스키마 기반의 직렬화 이다. 데이터는 먼저 스키마를 정의하고, 그 후 해당 스키마에 따라 직렬화 된다. 이를 통해 데이터의 구조를 명확하게 정의하고, 데이터를 읽고 쓸 때, 오류를 방지할 수 있다.
    2. JSON을 통한 스키마 정의: Avro의 스키마는 JSON 형식으로 작성된다. 이 스키마에는 필드의 이름, 타입, 필수 여부 들이 정의된다.
    3. 고속 직렬화 및 역직렬화: Avro는 바이너리 형식으로 데이터를 직렬화 하여 크기를 최적화하고 빠른 성능을 제공한다. 이를 통해 대규모 데이터 처리 시스템에서 높은 성능을 요구하는 경우 유리하다.
    4. 언어 독립성: Avro는 다양한 프로그래밍 언어에서 지원된다.
    5. 스키마 진화: Avro는 데이터 스키마가 변경되는 경우에도 호환성을 유지할 수 있는 기능을 제공한다. 즉, 데이터 형식이 변경되더라도, 이전 버전의 데이터를 함쎄 처리할 수 있다. 이를 스키마 진화 (Schema Evolution) 이라고 한다.
    6. 압축: Avro는 바이너리 형식이므로, 데이터 크기가 적고 디스크 공간을 절약하는 데 유리하다. 또한, 데이터 압축을 지원하여 저장 공간을 더욱 효율적으로 사용할 수 있다.
    7. RPC 지원: Avro는 원격 프로시저 호출 (RPC)도 지원하며, 이는 분산 시스템에서의 서비스 간 통신을 효율적으로 처리할 수 있게 한다.

     

    Avro의 예시

    스키마 정의 예시

    {
    “type”: “record”,
    “name”: “User”,
    “fields”: [
    {
    “name”:”name”,
    “type”:”string”
    },
    {
    “name”: “age”,
    “type”: “int”
    }
    ]
    }

     

    데이터 예시

    { “name”:”John”, “age”:30 }

     

    스키마 레지스트리 (Schema Registry)

    스키마 레지스트리는 Avro 스키마를 중앙에서 관리하는 시스템이다. 주로 Kafka와 같은 분산 시스템에서 사용되며, 프로듀서와 컨슈머가 데이터를 교환할 때 일관된 스키마를 사용할 수 있도록 보장한다. 대표적으로 Confluent Schema Registry를 사용한다.

     

    Java 에서의 활용

    자바에서는 다양한 형태로 Avro 라이브러리를 활용할 수 있는 방법을 제공한다.

     

    Avro (org.apache.avro:avro)

    Java에서, Avro 직렬화 및 역 직렬화 의 핵심 역할을 하는 라이브러리다. Avro 파일 포멧을 정의하고, Avro 스키마를 관리하는 데 필요한 모든 기능을 제공한다.

    사용 예시

    implementation ‘org.apache.avro:avro:$VERSION’

    Spring boot – Dependency Manager를 사용하더라도 버전을 명시해야 함.
    https://mvnrepository.com/artifact/org.apache.avro/avro

     

    Avro Kafka Serializer (org.apache.kafka:kafka-avro-serializer)

    Kafka에서 Avro 데이터 포멧을 직렬화 및 역 직렬화 하는 기능을 수행한다. Avro (org.apache.avro:avro)에 의존하고, 그 위에서 직렬화/역직렬화 기능을 제공한다. 하지만, Avro 라이브러리를 직접 포함하고 있지 않으므로, 사용 시 두 라이브러리를 모두 포함 해야 한다.

    사용 예시

    1. 의존성 추가

    implementation ‘io.confluent:kafka-avro-serializer’

    1. 저장소 추가

    repositories {
    maven { url ‘https://packages.confluent.io/maven’ }
    }

     

    Spring boot – Dependency Manager를 사용하더라도 버전을 명시해야 함.
    https://mvnrepository.com/artifact/io.confluent/kafka-avro-serializer/7.7.2

     

    Avro Maven Plugin (com.commercehub.gracle.plugin.avro)

    Avro 스키마를 Java 클래스로 변환하는 기능을 제공한다. 설정 구문을 통하여, Schema가 생성되는 위치나, 클래스가 생성되는 위치를 설정해 줘야 한다.

    avro {
    source(‘src/main/avro’) // Avro 스키마 파일 경로
    outputDir = file(‘build/generated-avro-java’) // 생성될 Java 클래스 위치
    fieldVisibility = ‘PUBLIC’ // 필드 접근성 설정
    }

     

    Java 클래스를 Avro Schema로 변환

    Java 클래스를 Avro Schema로 변경하는 방법은 다음과 같다.

    Java 코드에서 Avro Schema 생성

    Avro 라이브러리를 통해 코드 상에서 Avro Schema를 생성할 수 있다. 생성은 다음과 같은 코드로 가능하다.

    Schema schema = SpecificData.get().getSchema(MyAvroClass.class);
    System.out.println(schema.toString(true));

     

    Avro Schema Generation 도구

    Avro Schema Generation 도구를 통해 생성도 가능하다. 외부 jar 파일로 제공되며, 해당 파일을 다운로드 하여, 아래와 같은 명령을 통해 생성 가능하다.

    C#> java –jar avro-tools-1.10.2.jar complie schema <path-to-java-class> <output-directory>

     

    Gradle – Avro 플러그인

    Avro 플러그인을 사용하여 Java 클래스를 Avro 스키마로 변환하는 방법도 가능하다.

    플러그인 추가

    plugins {
    id ‘com.commercehub.gradle.plugin.avro’ version ‘0.21.0’
    }

     

    의존성 추가

    depeldencies {
    implementation ‘org.apache.avro:avro’
    }

     

    생성 설정

    avro {
    enableGenerateSchema = true
    source(‘src/main/java’) // 대상 클래스
    outputDir = file(‘build/avro-schemas’) // 생성된 스키마 파일 위치
    }

     

    Avro Schema를 Java 클래스로 변환

    역으로, Avro Schema를 Java 클래스로 변환하려면 다음과 같이 진행하면 된다.

    Gradle – Avro 플러그인

    Avro 플러그인을 사용하여 Java 클래스를 Avro 스키마로 변환하는 방법도 가능하다.

    플러그인 추가

    plugins {
    id ‘com.commercehub.gradle.plugin.avro’ version ‘0.21.0’
    }

     

    의존성 추가

    depeldencies {
    implementation ‘org.apache.avro:avro’
    }

     

    생성 설정

    avro {
    source(‘src/main/avro’) // 스키마 파일이 위치하는 디렉토리 설정
    outputDir = file(‘build/generated-avro-java’) // 생성된 스키마 파일 위치
    enableGenerateSchema = true // java 클래스로부터 Avro 스키마 자동 생성 활성화
    }

     

    웹 저장소 등 외부에 존재할 경우

    이때는, 스키마 다운로드를 위한 undercouch 플러그인을 추가하여 관리해 줄 수 있다.

    플러그인 추가

    plugins {
    id ‘com.commercehub.gradle.plugin.avro’ version ‘0.21.0’
    id ‘de.undercouch.download’ version ‘4.1.2’ // 파일 다운로드를 위한 플러그인
    }

     

    다운로드 태스크 추가

    task downloadSchema(type: Download) {
    src ‘https://example.com/your-schema.avsc’
    desc file(‘src/main/avro/your-schema.avsc’
    }

    task downloadSchemaOtuehr(type: Download) {
    src ‘https://example.com/your-schema-other.avsc’
    desc file(‘src/main/avro/your-schema-other.avsc’

    빌드 의존성 설정

    task generateAvroClasses {
    dependsOn downloadSchema, downloadSchemaOther, ‘generateAvro’
    }

     

    Avro 관련 플러그인 비교

    Avro는 QueryDSL과 같이 특정 정보를 이용하여 클래스를 자동 생성해 주는 플러그인을 제공해 준다. 다만, 여러가지 단체에서 다양한 형태로 플러그인을 제공하고 있어, 이를 비교해 보고, 어떤 것을 사용하는 것이 효과적인가를 확인해 본다.

     

    com.commercehub.gradle.plugin.avro

    Gradle 환경에서 Avro 스키마 파일을 처리하여 Java 클래스를 생성하는 플러그인이다. 상당히 오래된 플러그인으로, 현재 2021년 0.99.99 버전으로 업데이트된 이후, 더 이상 업데이트가 되고 있지 않다.

    주요 특징

    1. Gradle 환경에서 Avro 스키마 파일을 컴파일하여 Java 클래스를 생성한다.
    2. Avro maven 플러그인을 기반으로 하고 있으며, Avro 스키마를 Java 클래스로 변환하는데 최적화된 플러그인이다.

     

    com.github.davidmc24.gradle.plugin.avro – Gradle 추천

    Gradle 환경에서 Avro 스키마를 Java 클래스로 변환하는 작업을 담당한다. 현재도 지속적으로 업데이트 되고 있다. (2023년 10월 업데이트가 마지막임)

    기능적으로는 위에서 기술한 com.commercehub.gradle.plugin.avro와 유사하다.

    주요 특징

    1. Gradle에서 Avro 스키마 파일을 처리하는데 특화된 플러그인이다.
    2. Avro 스키마를 컴파일 하여 Java SpecificRecord 클래스를 생성한다.
    3. Gradle 빌드 시스템과 통합되어 빌드 시 자동으로 Avro 파일을 처리할 수 있도록 도와준다.

    설치 예시

    plugins {
    id “com.github.davidmc24.gradle.plugin.avro” version “1.3.0”
    }

    avro {
    source(“src/main/avro”)
    outputDir = file(“build/generated-main-avro-java”)
    }

    버전 확인은 아래 링크에서 확인
    https://mvnrepository.com/artifact/com.github.davidmc24.gradle.plugin/gradle-avro-plugin

    org.apache.avro.avro-maven-plugin – Maven 추천

    Maven을 사용하는 프로젝트에서 Avro 스키마 파일을 처리하여 Java 클래스를 생성하는 데 사용된다. Maven 기반의 프로젝트에서는 Maven 플러그인을 사용하여 빌드 시 Avro 스키마 파일을 Java 클래스로 변환한다.

    주요 특징

    1. Maven을 사용하여 Avro 스키마 파일을 Java로 변환한다.
    2. Avro의 Maven 플러그인으로, Maven 빌드 도구와 통합되어 Avro 파일을 처리할 수 있다.
    3. schema.registry와의 통합 및 Avro 스키마 버전 관리를 지원할 수 있다.

    설치 예시

    <plugin>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro-maven-plugin</artifactId>
    <version>1.10.2</version>
    <executions>
    <execution>
    <goals>
    <goal>generate></goal>
    </goals>
    </execution>
    </excutions>
    </plugin>

     

  • 안녕하세요!

    워드프레스에 오신 것을 환영합니다. 이것은 첫 글입니다. 바로 편집하거나 삭제한 다음 쓰기 시작하세요!