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

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

코루틴의 특징

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

언제 코루틴을 사용하나?

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

 

코루틴 자바 지원 X

 

자바의 virtual thread랑 비교?

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

 

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

 

 

코루틴

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

장점:

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

단점:

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

버추얼 스레드

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

장점:

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

단점:

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

전통적인 스레드와의 비교

  • 전통적인 스레드:
    • OS에서 관리되며, 각각의 스레드는 상당한 메모리와 자원을 차지
    • 많은 수의 스레드를 생성하면 성능 문제가 발생할 수
  • 버추얼 스레드:
    • JVM에서 관리되며, 매우 가벼움
    • 대량의 동시 작업을 처리할 때 효율적

 

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
반응형
반응형
  • 주키퍼(Zookeeper)는 분산 시스템의 코디네이션 서비스
  • ZNode 기반 트리구조로 데이터를 관리
  • 고가용성 고성능

 

주요 기능

  1. 분산 설정 관리:
    • 서버간 동기화를 위한 분산 코디네이션 기능
    • 분산 시스템에서 여러 애플리케이션이 동일한 설정 데이터를 공유해야 할 때 중앙 저장소 역할을 함
  2. 리더 선출(Leader Election):
    • 분산 시스템에서 리더를 선출할 때, 투표 과정을 관리하고 리더를 선출. 이를 지속적으로 유지 및 관리
    • 카프카 브로커들
  3. 동기화(Synchronization):
    • 분산 환경에서 여러 노드(서버 또는 프로세스)가 동일한 상태를 유지하도록 지원, 데이터 일관성 유지
    • 로컬 캐시 동기화 시
  4. 이벤트 감시(Watches):
    • Zookeeper의 데이터가 변경되면 이벤트 알림을 애플리케이션에 전달하여 실시간 업데이트를 함
    • watch 기능을 이용한 변경 이벤트 감지
  5. 분산 락 관리:
    • 분산 환경에서의 데이터 접근을 제어하고 동기화를 위한 락관리

 

특징

  • 일관성 보장:
    • Zookeeper는 CAP 이론에서 CP(Consistency와 Partition Tolerance)를 보장
    • 항상 데이터의 일관성을 우선
  • 높은 가용성:
    • 클러스터를 통해 고가용성을 제공하며, 노드 장애 시에도 서비스가 중단되지 않음
  • 간단한 API:
    • 클라이언트가 쉽게 사용할 수 있도록 간단한 API를 제공
  • 쓰기 지연, 읽기 최적화:
    • 쓰기 연산은 느릴 수 있지만 읽기 연산은 매우 빠름



기본 아키텍처

  • ZNode: 주키퍼에서 데이터를 저장하는 단위로, 파일 시스템과 유사한 구조를 가짐
  • 클라이언트: 주키퍼에 연결하여 데이터를 읽고 쓰는 애플리케이션
  • 서버: 주키퍼의 데이터 저장 및 처리를 담당하는 노드

장점

  • 분산 환경에서 복잡한 작업을 단순화함
  • 높은 신뢰성과 일관성 제공
  • 트랜젝션을 원자적으로 처리
  • 다양한 분산 시스템과의 통합 용이

단점

  • 쓰기 연산의 성능이 낮음(읽기 최적화)
  • 클러스터 크기가 커질수록 성능 저하 가능
  • ZooKeeper 장애 시 의존적인 애플리케이션에 문제가 발생할 수 있음

ZNode란?

 

  • ZNode는 ZooKeeper가 관리하는 데이터 구조의 단위
  • 트리 구조(파일 시스템과 유사)로 구성되며, 각 노드가 ZNode에 해당
  • ZNode는 데이터와 상태 정보를 저장하고, ZooKeeper의 API를 통해 접근 가능

 

ZNode 기반 트리 구조의 특징

  1. 트리 형태:
    • ZooKeeper 데이터는 / 루트에서 시작하여 트리 형태
      • 예: /app/config/db, /app/config/cache.
  2. 데이터 크기 제한:
    • 각 ZNode는 최대 1MB 크기의 데이터 저장
    • ZNode는 주로 작은 상태 정보를 저장하며, 대용량 데이터는 다른 저장소를 사용해야 함
  3. 노드 유형:
    • Persistent ZNode:
      • 노드가 생성된 후 삭제 요청이 있을 때까지 유지
    • Ephemeral ZNode:
      • 클라이언트 세션이 종료되면 자동으로 삭제
    • Sequential ZNode:
      • 노드 이름에 고유한 순번을 추가하여 생성
  4. 원자적 연산:
    • ZNode의 데이터 변경은 원자적으로 이루어지며, 동시성이 보장
  1.  

로컬 캐시 동기화 예시

이벤트를 감지하는 timestamp를 변경하여 (값을 수정하고) 변경 이벤트를 각 서버로 전달
https://youtu.be/BUV4A2F9i7w?si=c04ObhVx1skzBmDR

  • 디비 갱신
  • 레디스 만료
  • 주키퍼가 만료되었다는 이벤트를 각 서버에 전달
  • 각 서버는 이벤트를 받고 로컬 캐시 만료처리
  • 그 이후 새 요청이 들어오면 레디스/로컬 캐시를 최신 데이터로 갱신

세팅

자바 필요

주키퍼 압축 풀고 conf/zoo.cfg 설정 해야 함

# 기본 설정
tickTime=2000
initLimit=10
syncLimit=5

# 데이터 디렉토리 경로
dataDir=/var/lib/zookeeper

# 클라이언트 연결 포트
clientPort=2181

# 서버 설정 (클러스터 구성 시 필요)
# server.1=zookeeper1:2888:3888
# server.2=zookeeper2:2888:3888

 

  • server.3=zookeeper3:2888:3888 설정은 클러스터 내에서
    • 서버 3이 호스트 이름 zookeeper3, 데이터 통신 포트 2888, 리더 선출 포트 3888을 사용한다는 것을 의미
  • myid파일
echo "3" > /var/lib/zookeeper/myid

 

  • Zookeeper는 실행될 때 데이터 디렉토리(dataDir)에 위치한 myid 파일을 읽음
    • dataDir=/var/lib/zookeeper
  • myid 파일에 기록된 숫자를 사용하여, 설정 파일(zoo.cfg)의 server.X 항목 중 자신에게 해당하는 항목을 찾음
  • 이를 통해 클러스터 내에서 자신의 역할과 통신할 포트를 결정

 

728x90
반응형
반응형
ORDER BY NULL은 쿼리의 결과를 정렬하지 않도록 지정하는 구문

 

언제 쓸까?

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

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

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

 

728x90
반응형

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

[파티셔닝] 하는법, 쓰는법  (0) 2024.11.25
비관락/낙관락 쓰기락/읽기락 베타락/공유락  (1) 2024.11.09
2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (0) 2024.11.01
[p6spy] 설정 방법  (0) 2024.10.21
반응형

환경: 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)를 함께 가집니다. 이 점수에 따라 요소들이 자동으로 정렬됩니다.

특징:

  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
반응형
반응형

환경: springboot3.3

 

기존에 멀티 데이터베이스를 쓸 때 분산 트랜젝션을 위해 아래와 같이 ChainedTransaction을 사용하였는데..

@Configuration
public class ChainedTransactionConfiguration {

  @Primary
  @Bean(Constants.CHAINED_TRANSACTION_MANAGER)
  public PlatformTransactionManager transactionManager(@Qualifier(Constants.USER_TRANSACTION_MANAGER) PlatformTransactionManager userPlatformTransactionManager,
      @Qualifier(Constants.LOG_TRANSACTION_MANAGER) PlatformTransactionManager logPlatformTransactionManager,
      @Qualifier(Constants.STATIC_TRANSACTION_MANAGER) PlatformTransactionManager staticPlatformTransactionManager) {
    return new ChainedTransactionManager(userPlatformTransactionManager, logPlatformTransactionManager, staticPlatformTransactionManager);
  }
}

아래와 같이 Deprecated 되었다.

여러 대안을 찾다가 JtaTransaction이 있어 사용가능한지 확인해 본다.

 

조건은

1. 멀티 데이터베이스이기 때문에 각각에 대해 단일 Transactional을 설정할 수 있어야 하고

2. 필요에 따라 복합 트랜젝션도 가능해야 한다.

 

우선 JtaTransaction이 뭔지 간단히 알아보자.

JtaTransactionManager는 기본적으로 여러 데이터베이스에 걸쳐 트랜잭션을 처리하는 역할을 한다. 그러나 이를 제대로 활성화하려면 다음과 같은 조건을 충족해야 한다:

  • XA 데이터 소스 설정: 분산 트랜잭션을 사용하려면 XADataSource를 사용해야 한다. 예를 들어, MySQL을 사용할 경우 MysqlXADataSource를 사용해야 하며, 다른 데이터베이스도 XA 지원을 해야 한다.
  • 트랜잭션 관리자의 설정: JtaTransactionManager는 기본적으로 JTA를 사용하여 트랜잭션을 관리하지만, 분산 트랜잭션을 활성화하려면 여러 데이터 소스를 연결하고 이를 관리할 수 있는 TransactionManager 설정이 필요하다.
@Configuration
public class JtaDbConfig {

    @Bean(name = "chainedTransaction")
    public JtaTransactionManager transactionManager() {
        JtaTransactionManager transactionManager = new JtaTransactionManager();
        // JTA 트랜잭션 매니저 설정
        return transactionManager;
    }

    // DataSource 1 설정 (XA DataSource)
    @Bean
    public DataSource dataSource1() {
        MysqlXADataSource dataSource = new MysqlXADataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db1");
        dataSource.setUser("user1");
        dataSource.setPassword("pass1");
        return dataSource;
    }

    // DataSource 2 설정 (XA DataSource)
    @Bean
    public DataSource dataSource2() {
        MysqlXADataSource dataSource = new MysqlXADataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db2");
        dataSource.setUser("user2");
        dataSource.setPassword("pass2");
        return dataSource;
    }

    // EntityManagerFactory 설정 (각각의 데이터베이스용)
    @Bean(name = "entityManagerFactory1")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory1(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(dataSource1())
                .packages("com.example.entity1")
                .build();
    }

    @Bean(name = "entityManagerFactory2")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory2(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(dataSource2())
                .packages("com.example.entity2")
                .build();
    }
}

JtaTransactionManager 도 단일 트랜젝션 관리 가능한가?

  1. JTA 트랜잭션 관리의 범위:
    • JtaTransactionManager는 분산 트랜잭션(XA 트랜잭션)을 관리하는 데 최적화되어 있지만, 단일 데이터 소스에서의 트랜잭션도 처리할 수 있음
    • 단일 데이터 소스만 사용할 경우에도 JTA 프로토콜을 통해 트랜잭션이 시작되고 종료됨
  2. 단일 트랜잭션 시 처리 동작:
    • 단일 데이터 소스에서 JtaTransactionManager는 해당 데이터 소스에서의 트랜잭션을 관리함
    • 단일 데이터 소스 환경에서는 JpaTransactionManager나 DataSourceTransactionManager처럼 작동함
    • 단일 트랜잭션 환경에서는 JpaTransactionManager나 DataSourceTransactionManager가 더 효율적일 수 있다. 이는 JTA 오버헤드가 없기 때문
    • 분산 트랜잭션이 필요 없는 경우 굳이 JtaTransactionManager를 사용할 필요는 없음

 

참고:

  1. JpaTransactionManager는 JPA에 특화되어 있으며, 트랜잭션이 하나의 데이터베이스일 경우에 적합
  2. JtaTransactionManager는 JTA를 지원하며, XA 데이터 소스를 사용하는 분산 트랜잭션을 관리할 수 있음

JtaTransactionManager는 단일 트랜잭션도 처리할 수 있지만, 분산 트랜잭션이 필요 없는 경우에는 더 가벼운 트랜잭션 매니저(JpaTransactionManager 또는 DataSourceTransactionManager)를 사용하는 것이 더 효율적임. 하지만 프로젝트 환경에서 단일 및 분산 트랜잭션이 모두 필요하다면 JtaTransactionManager를 사용해 통합적으로 관리 가능

그럼 단일 트랜젝션이 필요할 경우 더 가볍게 설정할 수는 없을까?

그거슨 불가..

JtaTransactionManager가 기본적으로 JTA 규격에 따라 동작하며, 트랜잭션의 범위는 리소스에 따라 자동으로 결정되기 때문이다..

정 필요하면 아래처럼 JpaTransactionManager / JtaTransactionManager 각각 만들어서 필요에 따라 transactionManager를 지정하는 방법뿐.. 이라는데 이건 좀 아닌 듯....

@Configuration
public class DataSourceConfig {

    // 첫 번째 데이터베이스 - 단일 트랜잭션용
    @Bean(name = "dataSource1Hikari")
    public DataSource dataSource1Hikari() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db1");
        dataSource.setUsername("user1");
        dataSource.setPassword("password1");
        return dataSource;
    }

    // 첫 번째 데이터베이스 - 분산 트랜잭션용
    @Bean(name = "dataSource1XA")
    public DataSource dataSource1XA() {
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUrl("jdbc:mysql://localhost:3306/db1");
        xaDataSource.setUser("user1");
        xaDataSource.setPassword("password1");
        return xaDataSource;
    }

    // 두 번째 데이터베이스 - 단일 트랜잭션용
    @Bean(name = "dataSource2Hikari")
    public DataSource dataSource2Hikari() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db2");
        dataSource.setUsername("user2");
        dataSource.setPassword("password2");
        return dataSource;
    }

    // 두 번째 데이터베이스 - 분산 트랜잭션용
    @Bean(name = "dataSource2XA")
    public DataSource dataSource2XA() {
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUrl("jdbc:mysql://localhost:3306/db2");
        xaDataSource.setUser("user2");
        xaDataSource.setPassword("password2");
        return xaDataSource;
    }
}
@Configuration
public class TransactionManagerConfig {

    // 첫 번째 데이터베이스 - 단일 트랜잭션
    @Bean(name = "transactionManager1")
    public DataSourceTransactionManager transactionManager1(
            @Qualifier("dataSource1Hikari") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 두 번째 데이터베이스 - 단일 트랜잭션
    @Bean(name = "transactionManager2")
    public DataSourceTransactionManager transactionManager2(
            @Qualifier("dataSource2Hikari") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // JTA 트랜잭션 매니저 (분산 트랜잭션 관리)
    @Bean(name = "jtaTransactionManager")
    public JtaTransactionManager jtaTransactionManager(
            @Qualifier("dataSource1XA") DataSource dataSource1XA,
            @Qualifier("dataSource2XA") DataSource dataSource2XA) {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        return new JtaTransactionManager(userTransactionImp, userTransactionManager);
    }
}
@Service
public class DbService {

    @Transactional(transactionManager = "transactionManager1")
    public void performDb1Operation() {
        // 첫 번째 데이터베이스 트랜잭션 작업
    }

    @Transactional(transactionManager = "transactionManager2")
    public void performDb2Operation() {
        // 두 번째 데이터베이스 트랜잭션 작업
    }

    @Transactional(transactionManager = "jtaTransactionManager")
    public void performMultiDbOperation() {
        // DB1과 DB2를 조율하는 분산 트랜잭션 작업
    }
}

JTA 프로토콜: 2단계 커밋 (2PC)

JTA는 2PC (Two-Phase Commit) 프로토콜을 사용하여 분산 트랜잭션의 원자성과 일관성을 보장한다. 이 프로토콜은 다음 두 단계를 포함한다.

  1. 1단계: Prepare
    • 트랜잭션 관리자(Transaction Manager)는 모든 자원 관리자(XAResource)에 "Prepare" 메시지를 보냄
    • 각 자원 관리자는 트랜잭션을 준비하고, 성공 여부를 반환(예: VoteCommit 또는 VoteRollback)
  2. 2단계: Commit or Rollback
    • 모든 자원이 VoteCommit을 반환하면 트랜잭션 관리자는 "Commit" 메시지를 보내 트랜잭션을 커밋
    • 하나라도 VoteRollback을 반환하면 모든 자원에 "Rollback" 메시지를 보내 트랜잭션을 롤백
728x90
반응형
반응형

ForkJoinPool은 Java 7에 도입된 병렬 처리 프레임워크로, 작업을 작은 단위로 분할(fork)하고 병렬로 처리한 후 다시 합치는(join) 방식으로 동작한다. 병렬 프로그래밍작업 스케줄링을 위한 강력한 도구로, 특히 대규모 데이터 처리나 계산 집약적인 작업에 유용하다.

이게 프레임워크?

  • 고수준의 작업 관리: ForkJoinPool은 작업 스케줄링, 워크 스틸링, 병렬 처리 등을 관리하는 메커니즘을 제공한다. 개발자가 세부적인 스레드 관리나 큐 처리 등을 직접 코딩하지 않아도 된다.
    • 제어 역전 (IoC, Inversion of Control): 작업 실행과 스레드 관리는 ForkJoinPool이 수행하며, 개발자는 작업의 논리만 작성
  • 작업 분할 및 병합 전략: RecursiveTaskRecursiveAction이라는 추상 클래스를 기반으로 작업을 설계하며, 내부적으로는 효율적인 작업 분할 및 병합을 자동으로 처리한다.
  • 워크 스틸링 (Work Stealing): 스레드 풀에서 작업 큐를 관리하며, 비활성 스레드가 다른 활성 스레드의 큐에서 작업을 가져와 실행하는 동적 작업 분배를 한다.(처리량을 최적화); 개발자가 구현할 필요 없이 forkjoinpool이 자동으로 처리
  • 표준화된 인터페이스: 개발자가 사용할 수 있는 명확한 API (invoke, submit, fork, join 등)를 제공. 이로 인해 복잡한 병렬 프로그래밍을 간단히 구현할 수 있음.

장점

  • 멀티코어를 활용하여 작업을 병렬로 처리하므로 CPU 사용률이 최적화
  • 워크 스틸링을 통해 비효율적인 작업 분배를 방지

단점

  • 작업 분할 및 병합에 대한 오버헤드가 존재
  • I/O 중심 작업에서는 비효율적이며, CPU 집약적인 작업에 적합

적합한 상황

  • 데이터가 많고, 병렬로 처리할 수 있는 작업
  • 재귀작업/반복적으로 작업을 나눌 수 있을 때 (예: 합계, 정렬)
  • CPU 집약적인 작업에서 최적의 성능을 얻고자 할 때

개발 시 전체적인 흐름

  1. 큰 작업이면 Fork하여 병렬 처리.
  2. 작은 작업이면 직접 계산으로 효율적 처리.
  3. 모든 계산이 끝나면 병렬 결과를 Join하여 최종 결과를 얻음.

작은 작업은 직접 계산하는 이유

  1. 작업 분할의 비용 문제:
    • Fork/Join Framework는 큰 작업을 작은 작업으로 나누고 각 작업을 병렬적으로 실행
    • 하지만 작업을 너무 많이 나누면 작업 분할과 작업 병합(merge)에 드는 오버헤드(비용)가 커질 수 있음
    • 작은 작업에 대해서는 작업 분할을 하지 않고 직접 계산하여 오버헤드를 줄임
  2. 효율성 최적화:
    • 일정 크기 이하의 작업은 더 이상 병렬로 처리할 필요가 없으므로 직접 계산이 더 효율적
    • 예를 들어, 배열의 일부를 합산하거나 특정 범위의 숫자를 더하는 간단한 작업이라면, 병렬처리 대신 반복문을 통해 순차적으로 계산하는 것이 빠름

 

ForkJoinPool의 주요 메서드

  • invoke(ForkJoinTask<?> task): 기다리고 결과를 받음
  • execute(ForkJoinTask<?> task): 작업을 비동기로 실행
  • submit(ForkJoinTask<?> task): 작업을 실행하고 Future를 반환

ForkJoinPool 개발 시 RecursiveTaskRecursiveAction의 역할

  1. RecursiveTask<V>:
    • 반환값이 있는 병렬 작업을 정의할 때 사용
    • 작업을 분할하고 결과를 합산하여 반환(compute() 메서드)
  2. RecursiveAction:
    • 반환값이 없는 병렬 작업을 정의할 때 사용.
    • 단순히 작업을 수행하고 결과를 반환하지 않는 경우 적합(compute() 메서드)

꼭 써야해?

RecursiveTask를 상속하지 않고도 직접 ForkJoinTask 또는 Runnable과 같은 인터페이스를 사용할 수 있다. 하지만 이는 더 복잡하고 비효율적이며 코드 복잡성을 증가시킨다.

ForkJoinPool

스레드 갯수를 생략하면, 기본적으로 가용한 CPU 코어 수에 따라 동작

  • 스레드 수 = Runtime.getRuntime().availableProcessors()
    즉, 현재 시스템의 CPU 코어 수(논리적 코어 포함)가 기본 스레드 수로 사용됨
ForkJoinPool pool = new ForkJoinPool(); //내부적으로 가용한 프로세서 수를 기반으로 스레드 풀 크기를 결정

ForkJoinPool pool = new ForkJoinPool(4); // 스레드 4개 사용

예시

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class SumTask extends RecursiveTask<Integer> {
        private final int[] arr;
        private final int start, end;
        private static final int THRESHOLD = 10;

        public SumTask(int[] arr, int start, int end) {
            this.arr = arr;
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= THRESHOLD) {
                // 작은 작업은 직접 계산
                int sum = 0;
                for (int i = start; i < end; i++) {
                    sum += arr[i];
                }
                return sum;
            } else {
                // 작업 분할
                int mid = (start + end) / 2;
                SumTask leftTask = new SumTask(arr, start, mid);
                SumTask rightTask = new SumTask(arr, mid, end);

                leftTask.fork(); // 병렬 처리
                int rightResult = rightTask.compute(); // 동기 처리
                int leftResult = leftTask.join(); // 병합

                return leftResult + rightResult;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < arr.length; i++) arr[i] = i + 1;

        ForkJoinPool pool = new ForkJoinPool();
        SumTask task = new SumTask(arr, 0, arr.length);
        int result = pool.invoke(task);

        System.out.println("Sum: " + result); // 출력: Sum: 5050
    }
}

1. leftTask.fork();

  • 작업 분할:
    • leftTask를 병렬로 처리할 수 있도록 Fork-Join Pool에 작업 큐로 제출
    • fork() 메서드는 현재 작업을 Fork-Join Pool의 스레드가 처리하도록 요청하며, 비동기적으로 실행
    • 이 시점에서 leftTask는 아직 결과를 계산하지 않음

2. int rightResult = rightTask.compute();

  • 동기 실행:
    • rightTask는 직접 현재 스레드에서 동기적으로 계산
    • compute() 메서드는 RecursiveTask에서 작업을 처리하는 메인 로직
    • 이렇게 함으로써 하나의 스레드가 rightTask를 바로 계산하여, 자원을 최대한 활용

3. int leftResult = leftTask.join();

  • 결과 병합:
    • join() 메서드는 leftTask가 완료될 때까지 대기하고 결과를 반환
    • 만약 leftTask가 이미 완료되었으면, 바로 결과를 반환
    • 이를 통해 leftTask와 rightTask의 결과를 병합

왜 이런 방식으로 처리?

  • 자원의 효율적 활용:
    • leftTask는 병렬로 실행하도록 요청 (fork())
    • 한편, rightTask는 현재 스레드에서 처리 (compute())
    • 이렇게 하면 다른 작업 스레드가 leftTask를 처리하는 동안, 현재 스레드가 놀지 않고 rightTask를 계산하여 자원을 최대한 활용
  • 병렬성과 동기화의 조합:
    • fork()로 비동기 작업을 시작하고 join()으로 결과를 기다리며 동기화.
    • 병렬성과 동기화의 균형을 유지하면서 성능을 최적화

ForkJoinPool

  • 특징:
    • Java 7에서 도입.
    • 작업 분할(divide-and-conquer)을 기반으로 병렬 처리를 수행.
    • Work-Stealing 알고리즘을 사용해 작업이 끝난 스레드가 다른 스레드의 작업을 훔쳐 효율성을 높임.
    • 주로 재귀적인 작업 처리작업 분할에 사용.
    • RecursiveTask(결과 반환)와 RecursiveAction(결과 없음)을 통해 작업 정의.
  • 사용 사례:
    • 큰 작업을 작은 작업으로 나눠 처리하는 경우.
    • 예: 대규모 데이터 처리, 배열 합산, 병렬 검색.
  • 장점:
    • 스레드 수를 효율적으로 관리 (스레드 풀 크기 설정 가능).
    • Idle(대기) 상태인 스레드가 다른 작업을 훔쳐 병렬 처리 최적화.
  • 단점:
    • 작업 분할이 필요 없는 간단한 병렬 작업에는 적합하지 않을 수 있음.
    • Work-Stealing 비용이 단순 작업에서는 오히려 비효율적.

ExecutorService

  • 특징:
    • Java 5에서 도입.
    • 병렬 작업을 스레드 풀에서 실행하여 스레드 관리를 자동화.
    • Java의 스레드 풀을 관리하는 인터페이스로, 스레드의 생성, 실행, 종료를 간편하게 처리.
    • 개발자는 스레드 풀을 직접 관리할 필요가 없음!
    • 스레드 풀이 다양한 종류로 제공:
      • FixedThreadPool: 고정된 크기의 스레드 풀.
      • CachedThreadPool: 동적으로 크기가 변하는 스레드 풀.
      • ScheduledThreadPool: 예약 및 지연 실행 작업용.
      • SingleThreadExecutor: 단일 스레드로 작업 처리.
  • 사용 사례:
    • 병렬 작업이 분할되지 않거나 작업 분할을 수동으로 처리해야 할 때.
    • 예: 웹 서버 요청 처리, 비동기 작업 관리.
  • 장점:
    • API가 간단하고 다양한 스레드 풀 종류 제공.
    • 반복적이고 독립적인 병렬 작업에 적합.
    • 작업 분할 없이 단순 병렬 실행 가능.
  • 단점:
    • ForkJoinPool만큼 작업 분할에 최적화되지 않음.
    • 대규모 데이터 병렬 처리에는 적합하지 않을 수 있음.
import java.util.concurrent.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //4개의 스레드로 구성된 고정 크기 풀 생성
        ExecutorService executor = Executors.newFixedThreadPool(4);

        Callable<Integer> task1 = () -> {
            Thread.sleep(1000);
            return 1;
        };
        Callable<Integer> task2 = () -> {
            Thread.sleep(1000);
            return 2;
        };

        Future<Integer> result1 = executor.submit(task1);
        Future<Integer> result2 = executor.submit(task2);

		//Future.get() 호출로 각각의 결과를 대기하고 출력
        //task1과 task2는 1초 동안 대기 후 각각 1과 2를 반환
        System.out.println("Result 1: " + result1.get());
        System.out.println("Result 2: " + result2.get());

		//스레드 풀 종료
        executor.shutdown();
    }
}

 


728x90
반응형

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

coroutine vs virtual thread  (0) 2024.12.24
[동기화] 뮤텍스/세마포어  (0) 2024.11.24
DB로 분산락 구현  (2) 2024.11.21
[test] org.mockito.exceptions.misusing.PotentialStubbingProblem  (1) 2024.11.15
자바와 스프링에서 thread pool  (0) 2024.11.11
반응형

환경: MySql

예전에 거래내역의 데이터를 파티셔닝 해서 조회한 적이 있는데 직접 설정했던 게 아니라서 구체적으로 알아본다.

파티셔닝 (Partitioning)

파티셔닝하나의 데이터베이스 내에서 데이터를 논리적으로 나누는 방법. 특정 컬럼을 기준으로 데이터를 여러 파티션으로 분할하여 성능을 향상시킬 수 있다.

파티셔닝의 특징:

  • 단일 데이터베이스 인스턴스 내에서 분할
  • 쿼리 성능 향상: 데이터를 작은 블록으로 나누어 특정 파티션만 조회할 수 있어 성능이 향상
  • 트랜잭션 처리: 트랜잭션의 일관성을 유지할 수 있음
  • 관리 용이성: 데이터를 분할해도 동일한 데이터베이스 인스턴스를 사용하므로 관리가 상대적으로 단순

파티셔닝을 선택할 때:

  • 단일 서버에서 성능을 향상시키고 싶을 때
  • 데이터가 일정한 기준으로 나누어지고, 쿼리가 특정 범위 (예: 날짜, 지역 등)로 자주 조회될 때
  • 트랜잭션 일관성 및 데이터 무결성을 유지해야 할 때

예시:

  • 날짜 기반 파티셔닝: order_date가 날짜 범위에 따라 파티셔닝되어, 특정 날짜 범위만 조회하면 해당 파티션만 스캔하여 빠른 성능을 제공

 

파티션 생성

CREATE TABLE orders (
    id INT NOT NULL,
    order_date DATE NOT NULL,
    amount DECIMAL(10, 2),
    PRIMARY KEY (id, order_date)
)
PARTITION BY RANGE (YEAR(order_date) * 100 + MONTH(order_date)) (
    PARTITION p202301 VALUES LESS THAN (202302),
    PARTITION p202302 VALUES LESS THAN (202303),
    PARTITION p202303 VALUES LESS THAN (202304),
    PARTITION pMax VALUES LESS THAN MAXVALUE
);

PARTITION BY RANGE는 각 파티션의 VALUES LESS THAN 조건에 따라 데이터를 할당

MySQL에서는 파티션 조건이 겹치지 않도록 설계되어 있으므로, 데이터가 특정 파티션에만 할당됨

 

  • YEAR(order_date) * 100 + MONTH(order_date) 값이 202302 미만인 데이터가 이 파티션에 들어감
  • 예: 2023년 1월의 데이터(202301), 2022년 12월 이전의 데이터

파티션 확인 가능?

MySQL에서는 내부적으로 파티션이 관리되지만, 사용자 관점에서는 단일 테이블로만 동작하며 클라이언트에서 개별 파티션을 노출하지 않는다. 파티션 정보를 조회하거나 특정 파티션만 쿼리 하는 기능은 있지만, 파티션에 직접 들어가서 작업하는 방식은 지원되지 않는다.

하지만 오라클에서는 파티션이 클라이언트나 관리 도구에서 명시적으로 노출되며, 개별 파티션에 대해 직접 접근하고 작업할 수 있다.

내부 동작 방식

물리적 파일 분리

  • MySQL은 각 파티션을 내부적으로 별도의 파일 형태로 저장
  • 데이터 파일(.ibd 파일, InnoDB 기준)이 각 파티션별로 생성
  • 이로 인해 파티셔닝 된 테이블은 디스크 I/O 및 데이터 관리를 효율적으로 수행

논리적 테이블

  • 사용자는 하나의 테이블로 모든 데이터를 다룸
  • SQL 문장에서 특정 파티션을 명시적으로 참조할 필요가 없음
  • MySQL은 파티션 조건에 따라 자동으로 적절한 파티션에 데이터를 삽입하거나 조회

 

플로우

1. 삽입 시 MySQL은 파티션 조건을 평가하여 해당 파티션에 데이터를 저장

2. 조회 시에도 사용자는 특정 파티션을 신경 쓰지 않아도 됨. MySQL이 자동으로 필요한 파티션만 읽음(파티션 프루닝).

3. 필요하다면 특정 파티션만 직접 조회할 수도 있음

INSERT INTO orders (id, order_date, amount) VALUES (1, '2023-01-15', 100.00);
-- 이 데이터는 p202301 파티션에 저장됨

SELECT * FROM orders WHERE order_date = '2023-01-15';
-- MySQL은 p202301 파티션만 스캔

SELECT * FROM orders PARTITION (p202301);
-- 명시적인 조회도 가능..

 

 

조회 시 파티션 프루닝

MySQL의 파티션 프루닝(Partition Pruning)은 쿼리 실행 시 WHERE 절의 조건에 따라 필요한 파티션만 읽도록 최적화하는 기법

파티션 프루닝이 작동하는 조건

  1. WHERE 절에 파티션 키가 포함될 것
    • MySQL은 WHERE 절에 파티션 키(파티셔닝 기준이 되는 컬럼)가 있을 때만 프루닝을 수행
  2. 상수 또는 단순 연산 사용
    • MySQL은 조건이 상수 값이거나 단순 연산으로 평가될 수 있을 때만 프루닝을 적용
    • 예를 들어, order_date = '2023-01-15'는 가능하지만 order_date = NOW()는 모든 파티션을 탐색
  3. 범위 조건
    • BETWEEN, <, >, = 등의 조건도 프루닝이 가능
  4. IN 조건
    • IN 조건도 가능한 경우 특정 파티션만 선택

그럼 아래와 같이 여러 월에 걸쳐진 데이터도 프루닝이 적용될까?

SELECT * FROM orders WHERE order_date >= '2023-02-01' AND order_date < '2023-03-01';

mysql의 경우 가능, oracle의 경우 불가능..

1. mysql

  • 위 쿼리는 order_date 컬럼이 파티션 키이므로 프루닝이 가능
  • >=, <, BETWEEN과 같은 범위 조건은 프루닝이 지원
    • -> p202301과 p202302 파티션만 검색
  • MySQL에서 프루닝 여부를 확인하려면 EXPLAIN PARTITIONS를 사용
  • 여기서 partitions 열에 p202301, p202302와 같은 값이 표시되면 프루닝이 성공적으로 적용된 것
EXPLAIN PARTITIONS SELECT * FROM orders WHERE order_date >= '2023-01-15' AND order_date < '2023-02-21';

id | partitions    | type  | possible_keys | key  | key_len | ref  | rows  | Extra
---|---------------|-------|---------------|------|---------|------|-------|------
 1 | p202301,p202302 | ALL  | NULL          | NULL | NULL    | NULL | 1000  |

단, 아래와 같은 쿼리는 프루닝 불가

WHERE YEAR(order_date) = 2023 AND MONTH(order_date) = 1; -- 프루닝 불가

 

2. Oracle

  • 조건이 파티션의 경계값과 비교 가능해야 한다.
    • order_date= TO_DATE('2024-01-15', 'YYYY-MM-DD')
      • 파티션 p202401만 접근
    • order_date BETWEEN TO_DATE('2024-01-01', 'YYYY-MM-DD') AND TO_DATE('2024-01-31', 'YYYY-MM-DD')
      • 파티션 p202401만 접근
    • order_date >= TO_DATE('2023-01-15', 'YYYY-MM-DD') AND order_date < TO_DATE('2023-02-10', 'YYYY-MM-DD');
      • 조건이 파티션 경계와 명확히 매칭되므로 Oracle은 p202301과 p202302 두 개의 파티션만 읽습니다.

 

  • 파티션 키에 함수나 연산을 적용하면 Oracle이 프루닝을 할 수 없다.
SELECT * FROM sales WHERE TRUNC(order_date) = TO_DATE('2024-01-15', 'YYYY-MM-DD');
-- 파티션 프루닝 작동하지 않음

 

  • 파티션 키에 연산을 하는 것은 위험하다. 모든 파티션을 스캔할 수 있으니 확인해야한다.
SELECT * FROM orders 
WHERE TRUNC(order_date) >= TO_DATE('2023-01-15', 'YYYY-MM-DD')
  AND TRUNC(order_date) < TO_DATE('2023-02-21', 'YYYY-MM-DD');

 

  • 프루닝 여부 확인
EXPLAIN PLAN FOR
SELECT * FROM orders 
WHERE order_date >= TO_DATE('2023-01-15', 'YYYY-MM-DD') 
  AND order_date < TO_DATE('2023-02-21', 'YYYY-MM-DD');
  
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

--------------
PARTITION START  | PARTITION STOP
-----------------|---------------
202301           | 202302
728x90
반응형

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

[mysql] order by null  (0) 2024.12.19
비관락/낙관락 쓰기락/읽기락 베타락/공유락  (1) 2024.11.09
2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (0) 2024.11.01
[p6spy] 설정 방법  (0) 2024.10.21

+ Recent posts