728x90
반응형
728x90
반응형
반응형

포인트

  • QPS: 43,000
  • max QPS = 215,000

 

지연시간

평균 지연 시간은 낮아야 하고 전반적인 지연 시간 분포는 안정적이어야 함.

  • 지연시간 = 중요 경로상의 컴포넌트 실행 시간의 합

지연시간을 줄이기 위해서는

  • 네트워크 및 디스크 사용량 경감
    • 중요 경로에는 꼭 필요한 구성 요소만 둔다. 로깅도 뺀다.
    • 모든 것을 동일 서버에 배치하여 네트워크를 통하는 구간을 없앤다. 같은 서버 내 컴포넌트 간 통신은 이벤트 저장소인 mmap를 통한다.
  • 각 작업 실행 시간 경감
    • 꼭 필요한 로직만
  • 지연 시간 변동에 영향을 주는 요소 조사 필요
    • gc...

 

속도

주문 추가/취소/체결 속도: O(1)의 시간 복잡도를 만족하도록

  • 추가: 리스트의 맨 뒤에 추가
  • 체결: 리스트의 맨 앞에 삭제
  • 취소: 리스트에서 주문을 찾아 삭제 

주문 리스트는 DoubleLinkedList여야 하고 Map을 사용하여 주문을 빠르게 찾아야 한다.

 

영속성

시장 데이터는 실시간 분석을 위해 메모리 상주 칼럼형 디비(KDB)에 두고 시장이 마감된 후 데이터를 이력 유지 디비에 저장한다.

 

  • KDB+는 Kx Systems에서 개발한 고성능 시계열 데이터베이스로, 대규모 데이터 분석에 최적화되어 있다. 주로 금융 업계에서 빠른 데이터 처리와 분석을 위해 사용된다.
  • 초고속 데이터 처리 능력
  • 시계열 데이터를 효율적으로 저장 및 분석
    • 시간에 따라 변화하는 데이터를 저장하고 관리하는 데 최적화된 데이터베이스
    • 시계열 데이터는 각 데이터 포인트가 타임스탬프와 함께 기록되는 데이터
  • 금융 거래 데이터, 센서 데이터 등 대규모 데이터를 실시간으로 처리 가능

 

 

어플리케이션 루프

  • while문을 통해 실행할 작업을 계속 폴링하는 방식
  • 지연시간 단축을 위해 목적 달성에 가장 중요한 작업만 이 순환문 안에서 처리
  • 각 구성 요소의 실행 시간을 줄여 전체적인 실행 시간을 예측 가능하도록 보장
  • 효율성 극대화를 위해 어플리케이션 루프는 단일 스레드로 구현하며 CPU에 고정
    1. context switch가 없어지고
    2. 상태를 업데이트하는 스레드가 하나뿐이라 락을 사용할 필요 없고 잠금을 위한 경합도 없다.
    3. 단 코딩이 복잡해짐
      • 각 작업이 스레드를 너무 오래 점유하지 않도록 각 작업 시간을 신중하게 분석해야 함

 

mmap

mmap은 메모리 맵핑 (Memory Mapping)을 의미하며, 운영체제에서 제공하는 기능으로, 파일을 프로세스의 메모리에 맵핑하여 파일의 내용을 메모리 주소 공간에서 직접 읽고 쓸 수 있도록 한다. 이를 통해 파일 입출력(I/O) 속도를 향상시키고, 프로세스 간의 메모리 공유를 효율적으로 처리할 수 있다.

/dev/shm 메모리 기반 파일 시스템으로 여기에 위치한 파일에 mmap를 수행하면 공유 메모리에 접근해도 디스크io는 발생하지 않는다.

 

이벤트 소싱

현재 상태를 저장하는 대신 상태를 변경하는 모든 이벤트의 변경 불가능한(immutable) 로그를 저장

이벤트를 순서대로 재생하면 주문 상태를 복구 가능(이벤트 순서가 중요)

지연시간에 대한 엄격한 요구사항으로 카프카 사용 불가, mmap 이벤트 저장소를 메세지 버스로 사용(카프카 펍섭 구조와 비슷)

  • 주문이 들어오면 publish되고 주문 데이터가 필요한 각 컴포넌트가 subscribe 한다.

이벤트는 시퀀스, 이벤트 유형, SBE(simple binary encoding; 빠르고 간결한 인코딩을 위해) 인코딩이 적용된 메시지 본문으로 구성되어 있다.

게이트웨이는 이벤트를 링 버퍼에 기록하고 시퀀서가 링 버퍼에서 데이터를 가져온다(pull). 시퀀서가 이벤트 저장소(mmap)에 기록한다(pub).

  • 링 버퍼(Ring Buffer), 또는 원형 버퍼(Circular Buffer)는 고정된 크기의 버퍼로, 마지막 위치에 도달하면 다시 처음 위치로 돌아가서 덮어쓰는 방식으로 동작하는 자료구조. 링 버퍼는 주로 고정 크기의 메모리 할당을 유지하면서 데이터를 효율적으로 관리하고, FIFO(First In, First Out) 방식으로 데이터를 처리하는 데 사용됨. 데이터를 넣고 꺼내기만 하고 생성이나 삭제하는 연산은 필요 없다. 락도 사용하지 않는다.

 

고가용성

서비스 다운 시 즉시 복구

  1. 거래소 아키텍처의 단일 장애 지점을 식별해야 한다.
  2. 장애 감지 및 백업 인스턴스로 장애 조치 결정이 빨라야

서버 재시작 후 이벤트 저장소 데이터를 사용해 모든 상태를 복구한다.

주 서버의 문제를 자동 감지해야 하는데, 위에서 단일 서버로 설계했기 때문에 클러스터로 구성해야 하며 주서버의 이벤트 저장소는 모든 부 서버로 복제해야 한다.

이때 reliable UDP를 사용하면 모든 부 서버에 이벤트 메시지를 효과적으로 broadcast 할 수 있다.

  • 모든 수신자가 동시에 시장 데이터를 받도록 보장
  • 멀티캐스트: 하나의 출처에서 다양한 하위 네트워크상의 호스트로 보냄, 그룹에 가입한 수신자들만 데이터를 수신. 브로드캐스트와 유니캐스트의 중간

 

부 서버도 죽으면? fault tolerant

DRM마냥 여러 지역의 데이터 센터에 복제 필요..

  1. 주 서버가 다운되면 언제, 어떻게 부서버로 자동 전환하는 결정을 내리나
    • request가 이상하면? 소스 자체가 문제라면? => 운영 노하우를 쌓을 동안 수동으로 장애 복구
  2. 부 서버 가운데 새로운 리더는 어떻게 선출
    • 검증된 리더 선출 알고리즘(주키퍼, raft..)
  3. 복구 시간 목표(Recovery Time Objective)는 얼마
    1. 어플리케이션이 다운되어도 사업에 심각한 피해가 없는 최댓값
  4. 어떤 기능을 복구(Recovery Point Objective) 해야 하나
    1. 손실 허용 범위, 거의 0에 가깝게

 

보안

  • 공개 서비스와 데이터를 비공개 서비스에서 분리하여 디도스 공격이 가장 중요한 부분에 영향을 미치지 않도록. 동일한 데이터를 제공해야 하는 경우 읽기 전용 사본을 여러 개 만들어 문제를 격리
  • 자주 업데이트되지 않는 데이터는 캐싱
  • 디도스 공격에 대비해 아래와 같이 쿼리 파람에 제한을 둔다. 이렇게 바꾸면 캐싱도 유리하다.
    • before: /data?from=123&to=456
    • after: /data/recent
  • 블랙리스트/화이트리스트 작성
  • 처리율 제한 기능 활용
728x90
반응형
반응형

포인트

  • 하루 100만건 = 초당 10건의 트랜젝션(TPS)
  • 10TPS는 별 문제 없는 양, 트렌젝션의 정확한 처리가 중요
  • 각 거래별 멱등성을 위한 트렌젝션 아이디 필요(UUID)
  • 금액은 double이 아닌 string으로 직렬화/역직렬화에 사용되는 숫자 정밀도가 다를 수 있기 때문(반올림 이슈)

디비

  • 성능(nosql)보다는 안정성, ACID를 위한 관계형 데이터 베이스 선호

데이터 저장 시 고려

  • 결제 -> 지갑; 결제 -> 원장 테이블에 저장 시 상태값을 계속 변경하여 관리..
  • 지갑/원장 테이블의 상태가 모두 바뀌어야 결제 테이블 상태 업데이트..

시스템 구성 요소가 비동기적으로 통신하는 경우 정확성 보장?

  • 관련 상태값이 특정 시간동안 부적절한 상태로 남는지 확인하는 배치 돌려서 개발자에게 알람
  • 조정: 관련 서비스 간의 상태를 주기적으로 비교하여 일치하는지 확인(마지막 방어선)
    • 약간 파일 대사 같은 느낌.. 원장과 외부 업체와 데이터 비교

조정 시 불일치가 발생하면

  1. 어떤 유형의 문제인지 파악하여 해결 절차를 자동화
  2. 어떤 유형의 문제인지는 알지만 자동화 할 수 없으면(작업 비용이 너무 높으면) 수동으로
  3. 분류가 불가(판단이 안됨) 수동으로 처리하면서 계속 이슈 트래킹필요

동기 통신

  • 성능 저하
  • 장애 격리 곤란
  • 높은 결합도/낮은 확장성(트래픽 증가 대응 힘듦)

비동기 통신: queue

  • 단일 수신자: 큐 하나를 한 수신자가 처리. 병렬 처리를 위해 스레드 여러개가 동시에 처리하게 함. 큐에서 구독 후 바로 삭제됨
    • 보통 rabbitMQ사용
  • 다중 수신자: 큐 하나를 여러 수신자가 처리. 동일한 메세지를 각기 다른 호흡으로 읽어감. 읽어도 삭제되지 않음. 하나의 메세지를 여러 서비스에 연결할 때
    • 보통 카프카 사용
  • 실패 시 재시도 큐(일시적 오류) / dead letter queue로 이동(모든 재시도 실패)

정확히 한 번 전달? exactly once

  • 최소 한 번 실행 at least once: (지수적 백오프 사용하여) 재시도; 시스템 부하와 컴퓨팅 자원 낭비
  • 최대 한 번 실행 at most once: 멱등성 보장; 고유 키를 멱등 키로 잡아야
    • 광클 시 키 값을 확인하여 이미 처리되었는지 확인하고 되었으면 재처리 하지 않고 이전 상태와 동일한 결과 반환
    • 결제가 되었지만 타임아웃 나서 결과가 시스템에 전달되지 못함 ->  다시 결제 시도 시 이중 결제로 판단하여 종전 결과 반환

 

분산 환경에서 데이터 불일치가 발생할 수 있는데 일반적으로 멱등성조정 프로세스를 활용한다.

디비 불일치는 master-replica 동기화 

 

728x90
반응형
반응형

환경: java17

 

자바11부터 로컬 변수에 대한 타입 추론이 가능해졌다. 즉 아래와 같이 구체적인 타입을 선언하지 않고 var로 선언이 가능해졌다.

var items = (List<SinyutnoriRanking>) chunk.getItems();

그리고 자바 17을 사용중이다. 그런데 아래와 같은 경고창을 만난다.

Unchecked cast: 'java.util.List<capture<? extends xx.Ranking>>' to 'java.util.List<xx.Ranking>'

위 경고는 컴파일러가 타입 추론을 못해서 발생하는데, 자바 17이면 당연히 자바 11의 내용을 알고 있기에 컴파일러 단에서 타입추론이 되는거 아니야? 하는 생각이 들었다.

제네릭과 타입 추론의 한계

Java 11에서 var를 도입하면서 로컬 변수의 타입 추론이 가능해졌지만, 제네릭 타입의 안전성을 보장하기 위해 여전히 unchecked 경고가 발생할 수 있다.

  • 제네릭 타입을 사용할 때, Java 컴파일러는 타입 소거(Type Erasure)를 사용하여 런타임에 타입 정보를 제거한다. 이로 인해, 컴파일러가 타입 안전성을 완전히 보장할 수 없는 경우 경고를 발생시킨다.
  • 이런 경우 var를 사용하더라도 타입 캐스팅이 명시적이든 암시적이든 타입 안전성을 보장할 수 없으므로 @SuppressWarnings("unchecked")가 필요하다.
List<String> list = (List<String>) new ArrayList(); // Unchecked cast warning

var list = (List<String>) new ArrayList(); // 경고 발생

 

@SuppressWarnings("unchecked")

타입 안정성을 보장할 수 없는 상황에서 컴파일러 경고를 무시하도록 명시적으로 @SuppressWarnings("unchecked")를 추가한다. 이는 컴파일러에게 "나는 이 경고를 알고 있으며, 이 코드가 안전하다는 것을 보장할 수 있다"라는 신호를 주는 것이다.

 

참고로 최신 버전의 IntelliJ IDEA나 다른 IDE를 사용해도, 컴파일러 경고 자체는 여전히 발생할 수 있다는 점..!

728x90
반응형
반응형

2024.09.21 - [개발/java] - [java8+] 함수형 프로그래밍 @FunctionalInterface

 

일급 객체 (First-Class Object)

  1. 변수에 할당할 수 있다: 함수를 변수에 할당할 수 있다.
  2. 인자로 전달할 수 있다: 함수를 다른 함수의 인자로 전달할 수 있다.
  3. 반환값으로 사용할 수 있다: 함수를 다른 함수의 반환값으로 사용할 수 있다.
import java.util.function.Function;

public class FirstClassObjectExample {
    public static void main(String[] args) {
        // 함수가 변수에 할당됨
        Function<String, String> greet = name -> "Hello, " + name + "!";

        // 함수가 매개변수로 전달됨
        sayHello(greet, "Alice");

        // 함수가 반환 값으로 사용됨
        Function<String, String> greetFn = getGreetFunction();
        System.out.println(greetFn.apply("Bob"));
    }

    public static void sayHello(Function<String, String> fn, String name) {
        System.out.println(fn.apply(name));
    }

    public static Function<String, String> getGreetFunction() {
        return name -> "Hi, " + name + "!";
    }
}

고차 함수 (Higher-Order Function)

고차 함수는 다음 중 하나 이상의 조건을 만족하는 함수:

  1. 다른 함수를 인자로 받을 수 있다.
  2. 다른 함수를 반환할 수 있다.

Java 8의 Stream API는 함수형 프로그래밍의 개념을 적극 활용한다. 메서드 체이닝을 통해 고차 함수의 형태로 map, filter, reduce 등의 연산을 수행할 수 있다.

Java에서 콜백을 처리할 때 고차 함수가 자주 사용된다. 특정 작업이 완료되었을 때 실행할 동작을 함수로 전달할 수 있다.

import java.util.function.Consumer;
import java.util.function.Function;

public class HigherOrderFunctionExample {
    public static void main(String[] args) {
        // 함수를 매개변수로 받는 고차 함수
        repeat(3, i -> System.out.println("Hello, " + i));

        // 함수를 반환하는 고차 함수
        Function<Integer, Integer> doubleFn = createMultiplier(2);
        System.out.println(doubleFn.apply(5));  // 10
    }

    public static void repeat(int n, Consumer<Integer> action) {
        for (int i = 0; i < n; i++) {
            action.accept(i);
        }
    }

    public static Function<Integer, Integer> createMultiplier(int multiplier) {
        return value -> value * multiplier;
    }
}

용어가 비슷해서 헷갈리지만 다른....(객체 지향 관련 개념)

일급 컬렉션 (First-class Collection)

일급 컬렉션은 컬렉션을 직접 사용하지 않고 컬렉션을 래핑하는 클래스를 만들어, 해당 클래스를 통해서만 컬렉션을 조작하도록 하는 디자인 패턴이다. 이를 통해 코드의 명확성과 유지 보수성을 높이고, 불변성을 보장할 수 있다.

  1. 불변성 보장: 일급 컬렉션 클래스는 컬렉션에 대한 직접적인 접근을 방지하고, 불변성을 유지하도록 도와준다.
  2. 비즈니스 로직 캡슐화: 컬렉션에 대한 비즈니스 로직을 일급 컬렉션 클래스 내부에 캡슐화하여 코드의 응집도를 높임.
  3. 컬렉션 관련 메서드 제공: 컬렉션을 조작하기 위한 메서드들을 일급 컬렉션 클래스에서 제공하여, 코드의 명확성을 높임.
  4. 특정 타입의 컬렉션 강제: 일급 컬렉션은 특정 타입의 컬렉션만을 다루도록 강제할 수 있다.
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

public class Products {
    private final List<Product> products;

    public Products(List<Product> products) {
        this.products = new ArrayList<>(products);
    }

    // 불변성을 유지하기 위해 컬렉션 반환 시 복사본 제공
    public List<Product> getProducts() {
        return Collections.unmodifiableList(products);
    }

    // 전체 가격 계산 같은 비즈니스 로직 캡슐화
    public double totalPrice() {
        return products.stream()
                       .mapToDouble(Product::getPrice)
                       .sum();
    }

    // 제품 추가 메서드
    public Products addProduct(Product product) {
        List<Product> newProducts = new ArrayList<>(products);
        newProducts.add(product);
        return new Products(newProducts);
    }
}
728x90
반응형
반응형

코루틴 - 코틀린; 비동기

코루틴(Coroutine)은 비동기 프로그래밍동시성 처리를 위한 경량 실행 단위/함수
일반적으로 코루틴은 실행을 일시 중단하고(중단점 제공), 필요한 시점에 다시 시작할 수 있는 기능을 가지고 있음

  • 핵심 아이디어: 사용자 수준의 스케줄링
    • 코루틴은 명시적인 중단 지점을 통해 비동기 작업을 관리
    • 단일 스레드에서도 여러 코루틴을 실행할 수 있음
  • 동작 방식:
    • 비동기: 코루틴은 주로 비동기 작업을 처리하는 데 사용. 코루틴은 일시 중단과 재개가 가능하여, 비동기 네트워크 호출이나 파일 I/O 작업을 쉽게 처리할 수 있음
    • 런타임이 코루틴의 상태를 관리하고, 필요 시 다시 스케줄링
    • 예: suspend 함수 호출 시 작업을 중단하고 다른 작업을 실행

장점:

  • 명시적인 상태 관리로 복잡한 비동기 로직 처리에 강력.
  • 메모리 및 리소스 효율성이 높음.

단점:

  • 프로그래머가 중단 지점을 명시적으로 관리해야
  • 자바에서는 안됨(Kotlin에서 주로 사용)

코루틴의 특징

  1. 경량 스레드:
    • 코루틴은 스레드와 유사하게 보이지만, 실제 스레드를 생성하지 않고 실행되므로 더 적은 리소스를 사용함
    • 코루틴은 스레드보다 가벼운 구조로, 많은 수의 코루틴을 동시에 실행할 수 있어 메모리 사용량을 줄이고 성능을 향상시킴
  2. 비동기 작업 처리:
    • await 또는 yield 같은 키워드를 통해 작업의 흐름을 중단하고, 나중에 재개할 수 있음
    • 코루틴은 비동기적으로 실행되며, 다른 작업과 동시에 진행될 수 있어 CPU 자원을 효율적으로 사용할 수 있게 해줌
  3. 스케줄링 제어:
    • 코루틴은 프로그래머가 명시적으로 실행 순서를 제어할 수 있음
  4. 언제든 중단/재개 가능:
    • 작업의 중간에서 멈췄다가 나중에 다시 이어서 실행할 수 있어 효율적인 비동기 작업 처리가 가능
    • 코루틴은 실행 상태를 유지할 수 있어, 중단된 지점에서 다시 시작할 수 있음

언제 코루틴을 사용하나?

  1. I/O 작업:
    • 네트워크 요청, 파일 읽기/쓰기 등 시간이 오래 걸리는 작업에서 UI 스레드나 메인 스레드를 차단하지 않고 비동기적으로 처리.
  2. 동시성 프로그래밍:
    • 여러 작업을 병렬로 처리할 때 스레드보다 더 효율적으로 관리 가능.
  3. UI 프로그래밍:
    • 애니메이션, 이벤트 처리, 사용자 인터페이스 업데이트 등 비동기 작업을 자연스럽게 구현.
  4. 백그라운드 작업:
    • CPU 집약적인 작업이나 긴 대기 시간이 필요한 작업을 수행하면서 메인 스레드를 차단하지 않음.

 

코루틴 자바 지원 X

자바의 virtual thread랑 비교?

코루틴은 비동기 작업을 간편하게 처리하기 위해 설계된 반면, 버추얼 스레드는 높은 동시성을 요구하는 환경에서 효율적으로 스레드를 관리하기 위한 방법

  • 코루틴: 비동기 작업을 중단하고 재개할 수 있는 경량 구성 요소로, 비동기 작업 처리가 주 용도
  • 버추얼 스레드: 동기적인 프로그래밍 모델을 유지하면서도 높은 동시성을 처리할 수 있는 경량 스레드로, 비동기 코드의 복잡성을 줄임

 

버추얼 스레드 - 자바; 대규모동시성동기

  • 핵심 아이디어: JVM이 직접 관리
    • 전통적인 스레드는 OS 커널에서 관리되지만, 버추얼 스레드는 JVM 내부에서 관리되어 더 적은 자원을 소비
    • 각 작업은 자체적인 스레드처럼 동작하므로 프로그래머가 명시적으로 중단점을 관리할 필요가 없음
    • 버추얼 스레드는 동기적으로 작업을 수행. 전통적인 스레드와 유사한 방식으로 작동하지만, 더 가볍고 효율적
  • 동작 방식:
    • 동기적 코드 작성: 버추얼 스레드는 비동기 작업을 동기적인 코드 스타일로 작성할 수 있게 한다. 이는 비동기 코드의 복잡성을 줄이고, 동기적인 프로그래밍 모델을 유지. (동기 방식도 대규모 동시성 지원 가능! 많은 스레드를 동시에 처리 가능)
    • 차단 호출(예: I/O 작업) 발생 시 자동으로 OS 스레드에서 분리
      • IO 대기 상태에서는 스레드 반환해 대규모 작업 가능
    • 기존 스레드 풀보다 더 많은 수의 동시 작업 가능

장점:

  • 기존 스레드 API와 호환성 높음 (학습 곡선 낮음).
    • synchronized 블록, wait/notify 메서드 등을 그대로 사용 가능
  • 차단 호출도 자동으로 처리하므로 더 간단한 코드 작성 가능.

단점:

  • 자바 19 이상의 JVM에서만 사용 가능.
  • 특정 시나리오에서는 기존 스레드 풀만큼의 효율성 제공 어려움.

전통적인 스레드와의 비교

  • 전통적인 스레드:
    • OS에서 관리되며, 각각의 스레드는 상당한 메모리와 자원을 차지
    • 많은 수의 스레드를 생성하면 성능 문제가 발생할 수
  • 버추얼 스레드:
    • JVM에서 관리되며, 매우 가벼움
    • 대량의 동시 작업을 처리할 때 효율적
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(1000);
            return "Task completed!";
        });
    }
}

위 코드에서 10,000개의 Virtual Thread를 동기적으로 처리하지만, 비동기를 사용하지 않아도 효율적으로 실행됨

 

정리

 

2024.11.26 - [개발/java] - [병렬처리] Folkjoinpool & executorService

 

대규모 처리를 동시에 하려면 비동기 작업이 필수 아닌가?

 

그럼 비동기는 언제?

IO작업이 많을 때, 대기 시간을 최소화하고 자원을 효율적으로 활용할 수 있을 때

비동기는 주로 대기 시간이 많은 작업에서 활용됩니다. 이 대기 시간은 CPU가 직접 연산하는 시간이 아니라, 외부 시스템에서 응답을 기다리는 시간으로 인해 발생합니다. 비동기를 선택하는 주요 이유는 자원 효율성대규모 처리 능력입니다.

DB, file, API, 네트워크..

대규모 데이터 처리(ETL, 스트리밍)

작업 성격에 따른 선택

 

  • I/O 바운드 작업:
    대기 시간이 많은 작업에서 비동기를 사용하면 자원 활용이 극대화됩니다.
    • :
      • 외부 API 호출
      • 데이터베이스 쿼리
      • 파일 읽기/쓰기
      • 네트워크 요청
    • 추천 방식:
      • 비동기 프로그래밍: CompletableFuture, Kotlin Coroutines, async/await.
      • Reactive Streams: Project Reactor, RxJava.
      • Event-driven frameworks: Node.js.
  • CPU 바운드 작업:
    복잡한 계산, 데이터 변환 등 CPU가 주로 사용되는 작업에서는 동기나 멀티스레드 기반의 병렬 처리가 적합합니다.
    • :
      • 데이터 처리(대규모 연산)
      • 이미지 변환, 암호화
      • 머신 러닝 모델 실행
    • 추천 방식:
      • 동기 처리: 단일 스레드에서 작업.
      • 멀티스레드 기반 병렬 처리: Java ForkJoinPool, Parallel Streams, ExecutorService.

 

 

  • 멀티스레드는 작업을 물리적으로 병렬로 실행하는 방식이고,
  • 비동기는 작업을 논리적으로 대기 시간을 줄이며 논블로킹으로 처리하는 방식입니다.
  • 비동기는 멀티스레드를 사용하지 않을 수도 있지만, 필요하면 내부적으로 멀티스레드를 활용할 수도 있습니다. (예: Java NIO, Kotlin Coroutines).

성능 비교

https://tech.kakaopay.com/post/coroutine_virtual_thread_wayne/

728x90
반응형
반응형

환경: springbatch5, java17, mysql

 

MyBatisBatchItemWriter<GmahjongRanking> writer = new MyBatisBatchItemWriterBuilder<GmahjongRanking>().sqlSessionFactory(casualDb)
    .statementId(Constant.GAME_MAPPER + "insertGmahjongDayRank")
    .build();
<insert id="insertGmahjongTotalRank" parameterType="com.hangame.batch.casual.application.model.gmahjong.ranking.GmahjongRanking">

INSERT INTO GAME (regdate, memberid, wincnt, defeatcnt, slevel, rating, ranking, avatarid, nickname, oranking)
VALUES (#{registerDate}, #{memberId}, #{winCount}, #{defeatCount}, #{level}, #{rating}, #{ranking}, #{avatarId}, #{nickname}, #{oRanking})

</insert>

insert 문이 이렇게 있을 때 insert문이 1개만 나가는지, 청크 수만큼 나가는지 궁금해졌다.

insert 문이 1개만 나간다는 의미는 values 뒤로 n개 붙은 문이 한번 나가는 것이고

청크 수 만큼 나간다는 것은 insert 문 자체가 n 개 있다는 뜻.

 

MyBatisBatchItemWriter의 write 함수를 살펴보면 아래와 같다.

while 문으로 청크를 돌아서 sql을 만들고 들고 있다가 한 번에 실행한다.

ExecutorType.BATCH로 설정된 SqlSessionTemplate에서는, update() 메서드 호출 시 쿼리를 바로 실행하지 않고 내부 배치 큐에 저장하고 flushStatements()를 호출하면, 지금까지 배치 큐에 저장된 모든 SQL 문을 한 번에 실행

  • 장점:
    1. 네트워크 요청 최소화: 각 SQL 문을 개별적으로 실행하지 않고, 배치로 묶어서 처리
    2. 성능 향상: 배치 처리 시 JDBC 드라이버가 여러 쿼리를 내부적으로 최적화
  • 주의점:
    1. 메모리 사용량: 배치 큐에 저장된 쿼리가 많아질 경우 메모리 사용량이 증가
    2. 트랜잭션 관리: 배치 처리 중 하나의 쿼리가 실패하면, 전체 배치가 롤백

 

 

그럼 values 뒤로 쫙 붙여서 한번에 쏘고 싶다면?

우선 mapper를 수정하고

@Bean(INSERT_NINE_RATING_RANKING_WRITER)
@StepScope
public ItemWriter<BadukEnrichedRanking> insertNineRatingRankingWriter() {
    return chunk -> {
      @SuppressWarnings("unchecked") var items = (List<BadukEnrichedRanking>) chunk.getItems();
      var splittedNineRankings = ListUtil.splitList(items, SPLIT_LIST_SIZE);

      splittedNineRankings.forEach(badukNineRankingMapper::insertNineRankings);
    };
}

MyBatisBatchItemWriter를 안 쓰고 수동으로 itemWriter를 만든 후 

chunk를 sublist로 쪼갠 후 foreach 에 연결시킨다.

그러면 1 insert 의 values에 여러 개가 붙고 각 호출이 개별적인 SQL 실행을 하게 된다.

혹시 배치 방식으로 바꾸려면..

return chunk -> {
    @SuppressWarnings("unchecked")
    var items = (List<BadukEnrichedRanking>) chunk.getItems();
    var splittedNineRankings = ListUtil.splitList(items, SPLIT_LIST_SIZE);

    // Batch 처리 활성화
    try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        var mapper = sqlSession.getMapper(BadukNineRankingMapper.class);

        splittedNineRankings.forEach(mapper::insertNineRankings);

        // 배치 실행
        sqlSession.flushStatements();
        sqlSession.commit();
    }
};

 


MyBatisBatchItemWriter(ExecutorType.BATCH):

  • ExecutorType.BATCH 모드에서는 하나의 SqlSession을 열고 여러 쿼리를 실행한 후 한 번에 flushStatements()를 호출하여 쿼리들을 모아서 데이터베이스에 전송
  • 이 모드는 SQL 세션을 한 번만 열고, 여러 개의 쿼리를 하나의 트랜잭션 내에서 실행. 세션을 닫기 전에 모든 쿼리가 메모리에 쌓이고, flushStatements()를 호출하여 한 번에 실행되므로 성능 면에서 효율적

forEach 방식 (기본 SqlSession):

  • 반면에 forEach를 사용하여 각각의 항목을 처리하는 경우, 매번 update 또는 insert가 실행될 때마다 SqlSession을 생성
  • 이 방식은 각각의 쿼리가 별도의 세션을 사용하거나, 적어도 별도의 쿼리 실행이 이루어지는 방식. 즉, SQL 세션을 매번 열고 update 또는 insert를 실행한 후 세션을 닫고, 다시 열어서 쿼리를 실행하는 방식
728x90
반응형
반응형
ORDER BY NULL은 쿼리의 결과를 정렬하지 않도록 지정하는 구문

 

언제 쓸까?

데이터베이스에서 정렬 작업은 비용이 많이 드는 작업이다. 결과를 정렬할 필요가 없는 경우, ORDER BY NULL을 사용하여 불필요한 정렬 작업을 피할 수 있다.

정렬이 필요 없다면 성능 향상 가능!

GROUP BY와 함께 사용할 때: MySQL은 GROUP BY를 실행할 때 암묵적으로 정렬을 수행한다. 하지만 특정 상황에서는 이 정렬이 불필요할 수 있다. ORDER BY NULL을 사용하면 MySQL에 정렬을 생략하도록 지시하여 성능을 향상할 수 있다.

 

728x90
반응형

'개발 > sql' 카테고리의 다른 글

[mysql] 복합 인덱스와 explain  (0) 2025.02.03
[파티셔닝] 하는법, 쓰는법  (0) 2024.11.25
비관락/낙관락 쓰기락/읽기락 베타락/공유락  (1) 2024.11.09
2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (0) 2024.11.01
반응형

환경: springboot3.4, java17

 

최신 버전의 스프링부트를 쓰면 어느 새부터 아래와 같은 워닝을 만나는데 상당히 신경 쓰인다. 그동안 Page 인터페이스를 아주 많이 사용했던 터라 혹시 안되거나 deprecated 된다면 난감하기 때문이다..

찾아보니 springboot3.3부터 변경되었다고 한다!

2024-12-12 13:57:23 WARN  [ration$PageModule$WarningLoggingModifier.changeProperties    : 156] Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
	For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
	or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.

내용은 직렬화 시 안정적이지 않으니 Spring Data의 PagedModel 또는 PagedResourcesAssembler를 사용하여 안정적인 JSON 구조를 생성하라는 것이다.

그동안 직렬화할 때 PageImpl을 직접 직렬화하였는데, 더이상 안정적인 방식이 아니니 아래 두 방식 중 하나를 고르라는 뜻

  • HATEOAS를 쓰면 PagedResourcesAssembler, 그렇지 않으면  PagedModel

프로젝트 전역으로 설정하는 방식은 아래와 같다.

@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)

https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables

PageSerializationMode 에는 아래와 같이 두 가지가 있다.

DIRECT

PageImpl 객체를 직접 JSON으로 직렬화함

  • 직렬화된 JSON 구조는 PageImpl의 내부 구조에 따라 다를 수 있음
  • JSON 구조가 API의 변경이나 버전 업그레이드에 따라 변할 수 있으므로, 안정성이 떨어질 수 있음
  • 이 모드는 이전 버전의 Spring Data에서 기본적으로 사용되던 방식

DTO

DTO(Data Transfer Object)를 사용하여 페이지 데이터를 직렬화

  • 안정적이고 일관된 JSON 구조를 제공
  • DTO를 사용함으로써 페이지 데이터의 구조가 API 변경에 영향을 덜 받음
  • 이 모드는 PagedModel 또는 PagedResourcesAssembler와 함께 사용되며, 이를 통해 클라이언트가 예측 가능한 형식의 데이터를 수신할 수 있다.

 

설정하고 기존과 똑같이 페이징하면 된다.

@GetMapping
public Page<BaseResponse> getPrices(

참고로 HATEOAS

HATEOAS는 REST API 설계의 원칙 중 하나로, 클라이언트가 서버 응답에 포함된 하이퍼미디어(hypermedia)를 통해 애플리케이션 상태를 동적으로 탐색할 수 있도록 하는 방식이다. 이 원칙은 REST의 자기 설명(self-descriptive) 특성을 강화한다

HATEOAS의 구성 요소

  • 링크(Link): API 응답에 포함된 URL. 다음 가능한 액션을 안내.
  • 상태(State): 현재 리소스의 상태.
  • 동작(Action): 링크를 따라가면 수행할 수 있는 작업.

HATEOAS의 사용 이유

  • API 탐색성 증가:
    • 클라이언트는 추가적인 문서 없이 서버 응답에 포함된 링크를 통해 어떤 작업이 가능한지 동적으로 파악할 수 있음
  • 클라이언트-서버 결합도 감소:
    • 클라이언트는 서버가 제공하는 링크를 따라가기 때문에 특정 엔드포인트에 강하게 의존하지 않음
  • 유연한 확장성:
    • 서버에서 새로운 액션이나 엔드포인트를 추가하더라도, 클라이언트는 변경 없이 새로운 기능을 사용할 수 있음
  • 자기 설명적 API:
    • 서버 응답에 포함된 하이퍼미디어가 클라이언트에게 리소스 상태 및 가능한 작업을 설명하므로 API의 문서화와 유지보수가 용이
728x90
반응형
반응형

 

Redis의 Sorted Set은 특정 요소들의 집합으로, 각 요소는 고유한 값(value)과 정수나 부동소수점 형식의 점수(score)를 함께 가집니다. 이 점수에 따라 요소들이 자동으로 정렬됩니다.

key -> [(member1, score1), (member2, score2), (member3, score3), ...]

특징:

  1. 자동 정렬: 요소들이 점수에 따라 오름차순으로 자동 정렬됩니다.
  2. 빠른 조회: 특정 범위에 있는 요소들을 빠르게 조회할 수 있습니다.
    • 범위 조회 지원: 점수나 멤버 인덱스를 기반으로 부분 집합을 효율적으로 검색 가능합니다.
  3. 순위 계산: 요소들의 순위(rank)를 쉽게 계산할 수 있습니다.
  4. 중복 불가: 멤버 값은 중복될 수 없습니다. (단, 점수는 중복 가능)
  5. O(log(N)) 복잡도: 추가, 삭제, 조회 연산의 시간 복잡도는 O(log(N))입니다.

주요 명령어:

  • ZADD: Sorted Set에 요소를 추가합니다.
  • ZRANGE: 지정한 범위의 요소를 가져옵니다.
  • ZREM: 요소를 삭제합니다.
  • ZSCORE: 요소의 점수를 확인합니다.
  • ZRANK: 요소의 순위를 확인합니다.
//ZADD key score member [score member ...] 추가
ZADD le/aderboard 100 alice
ZADD leaderboard 200 bob 150 charlie

//ZSCORE key member 조회
ZSCORE leaderboard alice  # 결과: 100

//ZRANGE key start stop [WITHSCORES] 인덱스기반(시작; 0) 범위 조회 [점수도 같이 반환]
ZRANGE leaderboard 0 -1 WITHSCORES  # 전체 조회

//ZRANGEBYSCORE key min max [WITHSCORES] 점수 기반 범위 조회
ZRANGEBYSCORE leaderboard 100 200 WITHSCORES

//ZRANK key member 0시작 순위 반환(오름차순)
ZRANK leaderboard bob  # 결과: 2

//ZREVRANK key member 내림차순 순위 반환
ZREVRANK leaderboard bob  # 결과: 0

//ZREM key member [member ...] 특정 맴버 삭제
ZREM leaderboard alice

활용 사례

(1) 리더보드

게임에서 점수에 따라 순위를 관리할 때 유용합니다.

  • 점수를 score로, 사용자 이름이나 ID를 member로 저장.
  • 순위 조회, 점수 범위 내 사용자 검색 등이 가능.

(2) 태스크 스케줄링

  • 점수를 타임스탬프로 사용하여 작업을 스케줄링.
  • 특정 시간 범위의 작업을 조회하거나 삭제 가능.

(3) 우선순위 큐

  • 점수를 우선순위로 사용하여 작업을 관리.

주의점

  • 메모리 사용량: 점수와 멤버를 함께 저장하므로 메모리 사용량이 단순 Set보다 큽니다.
  • 점수 정밀도: 점수는 부동소수점(Floating Point)이므로 정밀도에 주의해야 합니다.

 

TTL 설정

Redis의 Sorted Set 자체에는 직접적인 TTL(Time To Live) 설정이 지원되지 않습니다. Redis는 Key-Value 기반으로 동작하므로, TTL은 키(key) 단위로 설정됩니다. 따라서 zset 안의 각 맴버에게 만료시간을 설정하는 것이 아닌 Sorted Set 전체에 대해 TTL을 설정해야 합니다.

  • TTL은 키 전체에 적용됩니다. Sorted Set의 개별 멤버에 TTL을 설정할 수는 없습니다.
    • 개별 멤버 TTL 관리가 꼭 필요하다면, 점수를 TTL처럼 사용하거나 별도 키로 TTL 관리하는 방법이 가장 실용적입니다.
  • TTL 설정 후, 키이 삭제되면 Sorted Set에 저장된 모든 멤버도 함께 삭제됩니다.
  • TTL은 주로 일시적인 데이터에 사용됩니다. 예를 들어, 일일 리더보드나 임시 작업 큐 등에 적합합니다.
  • 키가 이미 만료된 상태에서 접근하면, Redis는 키를 자동으로 삭제하고, 해당 키에 대한 작업은 무시됩니다.

레디스로 구현하려면..

sorted set으로 랭킹 저장하고 각 맴버별로 hash만들어서 ttl 설정한 후 노티피케이션이나 주기적으로 확인하여 set에서 삭제하는 로직 작성하여 수동으로 관리해야함..

# Sorted Set에 멤버 추가
ZADD myset 100 user1
ZADD myset 200 user2

# user1의 TTL이 필요하면 별도 키 생성 후 TTL 적용
SET user1_temp_value some_value
EXPIRE user1_temp_value 3600  # user1_temp_value 키에 TTL 1시간 설정

 

728x90
반응형

'개발 > cache' 카테고리의 다른 글

분산락 - redis 사용  (0) 2024.11.08
[redis] 기초  (0) 2023.02.08
반응형

환경: springboot3.3

url의 path variable로 enum을 받을 경우 아래와 같이 한 번에 받을 수 있지만, enum이 대문자로 관리될 경우, url에 대소문자가 혼용되게 된다.

이 경우 localhost:8080/AGAME/member-no 이렇게 된다(이는 StringToEnumConverterFactory 때문이다).

대소문자가 혼용되는 게 싫어서 아래와 같이 변환을 해서 검증을 하곤 하는데, 이게 또 계속 반복되는 문제점이 생긴다.

path variable의 값을 string의 코드로 보내도 자동으로 enum으로 변환할 수 없을까?!

 

HandlerMethodArgumentResolver 사용해 보기

resolver? Spring MVC에서 HTTP 요청 데이터를 Controller의 메서드 매개변수로 바인딩함

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class GamePathVariableResolver implements HandlerMethodArgumentResolver {

 @Override
  public boolean supportsParameter(MethodParameter parameter) {
    boolean hasAnnotation = parameter.hasParameterAnnotation(PathVariable.class);
    boolean usedEnum = GameType.class.isAssignableFrom(parameter.getParameterType());
    return hasAnnotation && usedEnum;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
      
    String value = (String) delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath(value);
  }
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new GamePathVariableResolver());
    }
}

근데? Resolver를 타지 않음

resolver가 스프링에 등록된 것은 확인했다.

그런데 url을 String game으로 호출하면 올바른 enum값이 아니라는 에러가 난다.

org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type

GamePathResolver에 가보지도 못한 건가? 그럼 호출 순서는 StringToEnumConverterFactory -> GamePathResolver??

 

우선 방식을 파악하기 위해 enum값으로 호출하고 resolver에 디버그를 걸어 호출 시 resolver를 타는지 확인해 본다.

그런데 resolver를 아애 타지 않는다..? 디버깅이 안 걸린다.

조금 더 다양한 방법으로 테스트해 본다.

  • 아래와 같이 PathVariable도 아니고 RequestParam도 아닌 일반 객체가 있을 때는 탄다.
  • 근데 잘 타다가도 아래 memberNo에 어노테이션(RequestParam 나 ModelAttribute)을 걸면 또 안탄다.
  • 디버깅을 걸어 MethodParam의 값을 확인해보면 어노테이션 PathVariable로 선언된 GameType이 없다....
@GetMapping("/{game}")
public ResponseEntity<?> checkGameFallbackToInGame(@PathVariable GameType game, String memberNo) {

왜?

먼저 등록된 기본 PathVariableMethodArgumentResolver가 처리할 수 있는 파라미터를 모두 소화하기 때문에 커스텀 리졸버가 호출되지 않을 수 있다... 흐름은 아래 참고..

  • 어노테이션이 있는 매개변수는 기본적으로 Spring의 내장 리졸버에 의해 처리됨
    • RequestParamMethodArgumentResolver는 @RequestParam을 처리.
    • PathVariableMethodArgumentResolver는 @PathVariable을 처리.
    • ModelAttributeMethodProcessor는 @ModelAttribute
  • 어노테이션이 없는 경우 아래의 룰로 기본 매핑된다. 
    • 단순 타입(String, int, boolean 등): 단순한 데이터 타입이면 @RequestParam으로 간주
      • RequestParamMethodArgumentResolver
    • 객체 타입 (사용자 정의 객체): 객체 타입이면 @ModelAttribute로 간주
      • ModelAttributeMethodProcessor
    • 그 외의 룰은 커스텀 어노테이션을 개발하여 매핑할 수 있음

궁금한 사항..

근데! WebMvcConfigurer에 커스텀 리졸버가 등록되면, Spring의 기본 리졸버보다 우선 적용되어 커스텀 리졸버에서 기존 동작을 완전히 덮어쓴다고 한다(그래서 확장하거나 위임해야 한다). 근데 아래와 같이 해보니 그냥 실행 자체가 안된다.. 왜?!

우선 PathVariableMethodArgumentResolver 자체를 override 할 수 있는지 궁금해서 임시로라도 해보려고 했는데 그저 안된다.. 

public class GamePathResolver implements HandlerMethodArgumentResolver {

  PathVariableMethodArgumentResolver resolver;

  public GamePathResolver() {
    this.resolver = new PathVariableMethodArgumentResolver(); // 기본 구현 생성
  }

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    if (resolver.supportsParameter(parameter)) {
      PathVariable path = parameter.getParameterAnnotation(PathVariable.class);
      return "game".equals(path.value());
    }
    return false;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
    // 기본 처리 로직 호출
    Object resolvedValue = resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath((String) resolvedValue);
  }
}

그럼 어떻게 해결? ConverterFactory 구현

우선 시간이 없어서 위 resolver 방법은 잠시 중단하고, 커스텀 어노테이션을 이용하여 컨버터와 연결하였다.

public class GamePathConverter implements ConditionalGenericConverter {

  @Override
  public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    return targetType.hasAnnotation(ConvertGamePath.class);
  }

  @Override
  public Set<ConvertiblePair> getConvertibleTypes() {
    return Set.of(new ConvertiblePair(String.class, GameType.class));
  }

  @Override
  public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    return GameType.findByPath((String) source);
  }
}
 
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

...

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new GamePathConverter());
  }
}

그러면 enum의 코드(string)로 보내도 enum으로 바로 가져올 수 있다.


참고로 포매터와 컨버터는 비슷하지만 아래와 같은 용어적 차이가 있다.

리졸버랑 컨버터는.. 여기서는 사실 두 개의 짬뽕을 하고 싶었고 그걸 리졸버에서 구현하고 싶었는데 안돼서 컨버터를 달아버린..


 

why not AOP?

1. AOP로 파라미터 값을 직접 변경 불가

  • Java의 AOP는 메서드 실행 전에 메서드 파라미터 값을 직접 변경할 수 없음

2. string으로 game argument관리하여 실수 잦음 + 순서도 중요..

@Before("@annotation(org.springframework.web.bind.annotation.PathVariable) && args(game,..)")
  • @PathVariable로 선언된 메서드 파라미터를 가지는 메서드 중, 첫 번째 파라미터 이름이 game인 경우를 대상으로 함..
728x90
반응형

+ Recent posts