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

환경: java17, springboot3.3.4, springbatch5

2024.08.16 - [개발/spring-batch] - [spring-batch] springboot3 mybatis 설정 그리고 mapper

 

[spring-batch] springboot3 mybatis 설정 그리고 mapper

환경: springboot3, spring batch5, mybatis그동안 jpa만 주구장창 사용했어서 올만에 Mybatis 설정이다! 1. 디비 정보 등록(application.yml)2. 빈 등록@Configuration@RequiredArgsConstructor@MapperScan( value = {"com.batch.ranking.ma

bangpurin.tistory.com

 

 

이슈: 매퍼 못 찾음

@Bean(TOP_SLIDE_READER)
@StepScope
public MyBatisCursorItemReader<NoticeVo> topSlideNoticeReader(@Qualifier(GameReplicaDataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
return new MyBatisCursorItemReaderBuilder<NoticeVo>().sqlSessionFactory(logDb)
    .queryId(Constant.SUDDA_NOTICE_MAPPER + "selectTopSlideNotices")
    .build();
}

위와 같이 springbatch + mybatis 조합으로 리더를 선언했는데 아래와 같이 mapper를 못 찾는 에러 발생

Caused by: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.xx.NoticeMapper.selectTopSlideNotices
	at org.apache.ibatis.session.Configuration$StrictMap.get(Configuration.java:1097)
	at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:875)
	at org.apache.ibatis.session.Configuration.getMappedStatement(Configuration.java:868)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectCursor(DefaultSqlSession.java:123)

 

datasource config 쪽에 아래와 같이 mapper scan을 달았고, 링크 어노테이션을 선언했음

@Configuration
@MapperScan(value = {"com.xx.gamereplica"},
            annotationClass = GameReplicaDataSource.class,
            ...
@SuddaGameReplicaDataSource
public interface SuddaNoticeMapper {

매퍼 인터페이스에 어노테이션 꼭 선언해야 함

 

구조

그럼에도 같은 에러가 계속 반복되었는데..

참고로 프로젝트 구조는 아래와 같다.

매퍼 인터페이스와 매퍼 xml 가 같은 main package 안에 있다. xml을 찾는 게 번거로워서 같이 두자는 의도였다.

 

해결: gradle 옵션 추가

그렇기 때문에 추가적인 작업이 필요했다.

build.gradle에 아래 추가

tasks.processResources {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

sourceSets {
    main {
        resources {
            srcDirs "src/main/java", "src/main/resources"
            include "**/*.xml"
            include "**/*.yml"
        }
    }
}

 

  • 기본 관행: 일반적으로 MyBatis 매퍼 XML 파일은 src/main/resources 디렉터리에 위치한다. 이는 Gradle 빌드 시스템에서 리소스로 자동으로 인식하고, 빌드 결과물(build/resources/main)에 포함한다.
  • 위치 변경: src/main/java에 XML 파일을 넣는 경우, 기본적으로 src/main/java는 소스 코드(Java 파일)의 경로로 간주되며, 리소스 파일로 처리되지 않는다. 따라서 빌드 과정에서 이 파일이 리소스로 인식되지 않아, MyBatis가 매퍼 파일을 찾지 못하게 된다.
  • 따라서 sourceSets 설정을 사용하여 Gradle에 src/main/java에 있는 XML 파일도 리소스로 취급하라고 명시적으로 설정한다.
  • 작동 원리: srcDirs "src/main/java", "src/main/resources" 설정을 통해 src/main/java 내의 XML 파일들을 리소스 디렉터리처럼 처리하도록 설정. 이 설정이 없으면 src/main/java는 Java 소스 코드만 포함하는 디렉터리로 인식되며, XML 파일은 빌드 결과물에 포함되지 않는다.
  • src/main/java와 src/main/resources 모두에서 XML 파일을 포함하게 되면 중복이 발생할 수 있으므로, tasks.processResources 옵션에 duplicatesStrategy를 설정하여 중복을 방지합니다.

 

 

record + xml mapper 작성 시 주의사항

record로 받을 시 resultMap이 필수며 javaType도 다 써줘야 한다(일반적으로는 optional이지만 writable property에 대해서는 javaType이 필수고 record는 불변 객체여서 writable 한 항목이 없다). javaType을 Integer로 명시했다면 레코드에서도 Integer로 받아야 한다(int 안됨)

<resultMap id="noticeVo"
    type="com.xx.notice.NoticeVo">
    <constructor>
        <idArg column="id" javaType="Integer" name="id"/>
        <arg column="type_code" javaType="Integer" name="typeCode"/>
        <arg column="startDate" javaType="LocalDateTime" name="startDate"/>
        <arg column="endDate" javaType="LocalDateTime" name="endDate"/>
        <arg column="time_interval" javaType="Integer" name="timeInterval"/>
        <arg column="title" javaType="String" name="title"/>
        <arg column="content" javaType="String" name="content"/>
        <arg column="extra_data" javaType="String" name="extraData"/>
        <arg column="regDate" javaType="LocalDateTime" name="regDate"/>
        <arg column="registrant" javaType="String" name="registrant"/>
    </constructor>
</resultMap>
public record NoticeVo(Integer id, Integer typeCode, LocalDateTime startDate, LocalDateTime endDate, Integer timeInterval, String title,
                       String content, String extraData, LocalDateTime regDate, String registrant) {

}

항목 순서, 타입 등 모든 게 중요하니 꼼꼼하게 봐야 한다. 아니면 아래 에러 발생...

Caused by: org.apache.ibatis.builder.BuilderException: Error in result map 'com.xx.gamereplica.SuddaNoticeMapper.noticeVo'. 
Failed to find a constructor in 'com.xx.notice.NoticeVo' with arg names [id, typeCode, startDate, endDate, timeInterval, title, content, extraData, regDate, registrant]. 
Note that 'javaType' is required when there is no writable property with the same name ('name' is optional, BTW). There might be more info in debug log.

 

참고로 int로 사용하고 싶으면 아래와 같이 _int로 사용하면 된다.

https://mybatis.org/mybatis-3/sqlmap-xml.html#result-maps

<resultMap id="noticeVo"
  type="com.xx.notice.NoticeVo">
  <constructor>
   <idArg column="id" javaType="_int" name="id"/>
   <arg column="type_code" javaType="_int" name="typeCode"/>
   <arg column="startDate" javaType="LocalDateTime" name="startDate"/>
   <arg column="endDate" javaType="LocalDateTime" name="endDate"/>
   <arg column="time_interval" javaType="_int" name="timeInterval"/>
   <arg column="title" javaType="String" name="title"/>
   <arg column="content" javaType="String" name="content"/>
   <arg column="extra_data" javaType="String" name="extraData"/>
   <arg column="regDate" javaType="LocalDateTime" name="regDate"/>
   <arg column="registrant" javaType="String" name="registrant"/>
  </constructor>
</resultMap>
728x90
반응형
반응형

포인트

  • 1,000,000 TPS
  • 높은 안정성
  • 트랜젝션
  • A -> B로 이체한다고 가정

 

인메모리 샤딩

계정-잔액(map) 저장

레디스 노드 한대로 100만 TPS는 무리

클러스터를 구성하고 계정을 모든 노드에 균등하게 분산시켜야(파티셔닝, 샤딩)

  • 분배하기 위해 키의 해시 값 mod 개수를 계산하여 분산

모든 레디스 노드의 파티션 수 및 주소는 주키퍼를 사용(높은 가용성 보장)

  • 이체 요청 시 각 클라이언트의 샤딩 정보를 물어봐서 클라이언트 정보를 담은 레디스 노드를 파악

데이터 내구성이 없다는 단점 존재..

분산 트랜젝션

레디스 사용 시 원자적 트랜젝션은 어떻게? RDB로 교체?

두 번째 이체(B로 입금) 시 서비스가 죽는다면?

1. 저수준: 데이터베이스 자체에 의존; 2PC

  • 실행 주체는 디비며 어플리케이션이 중간 결과를 알 수 없음
  • 모든 데이터베이스가 X/Open XA만족해야(JtaTransaction; mysql 지원)
  • 단점:
    • 두 단계가 한 트랜젝션
    • 락이 오랫동안 유지, 성능이 안 좋음
    • 조정자가 단일 장애 지점(SPOF)

 

2. try-confirm/cancel

  • -1 -> +1 해야
  • 실행 주체는 어플리케이션이며 독립적 로컬 트랜젝션의 중간 결과를 알 수 있음
  • 보상 트랜젝션이 별도로 구현되어 있고
  • 1단계 2단계가 각각 다른 트랜젝션으로 구성됨; 여러 개의 독립적인 로컬 트랜젝션으로 구성
  • 특정 데이터 베이스에 구애받지 않고 어플리케이션 단계에서 관리하고 처리
  • 실행 도중 coordinator 다운되는 것을 대비하여 각 단계 상태정보를 테이블에 저장(분산 트랜젝션 ID의 각 단계별 상태)
  • 취소를 대비해 실행 순서가 중요한데(+1을 하고 취소 시 -1을 해야 하는데 네트워크 이슈로 -1이 먼저 요청되는 경우)
    • 취소가 먼저 도착하면 디비에 마킹하고 다음에 실행 명령이 오면 이전에 취소 명령이 있는지 확인(그림 12.12)
  • 병렬가능

3. saga

  1. 모든 연산은 순서대로 정렬된다. 각 연산은 자기 디비에 독립적인 트랜젝션으로 실행된다.
  2. 연산은 첫 번째부터 마지막까지 순서대로 실행된다. 한 연산이 완료되면 다음 연산이 실행된다.
  3. 연산이 실패하면 전체 프로세스는 실패한 연산부터 맨 처음 연산까지 역순으로 보상 트랜젝션을 통해 롤백된다. 따라서 n개 연산을 실행하는 분산 트랜젝션은 보상트랜잭션 n개까지 총 2n개의 연산을 준비해야 한다.
  • choreography: 이벤트를 구독하여 작업 수행; 비동기
  • orchestration: 하나의 조정자가 모든 서비스가 올바른 순서로 작업하도록 조율

분산 트랜젝션의 경우 문제의 원인을 역추적하고 모든 계정에서 발생하는 연산을 감사(audit)할 수 없음

 

이벤트 소싱

  • command: 의도가 명확한 요청; 순서가 중요 FIFO 큐(카프카)에 들어감
    • A - $1 - C ->
  • event: 검증된 사실로 실행이 끝난 상태; 과거에 실제로 있었던 일
    • A -$1; C+$1 두 이벤트로 분리
  • state: 이벤트가 적용될 때 변경되는 내용(여기서는 잔액)
  • state machine: 이벤트 소싱 프로세스를 구동; 명령의 유효성을 검사하고 이벤트를 생성, 이벤트를 적용하여 상태를 갱신

시간 단축? (카프카 대신에)

1. 로컬 디스크

이벤트 소싱에 사용한 카프카(원격 저장소) 대신 로컬 디스크에 파일로 저장하여 네트워크 전송시간 줄일 수

순차적 읽기 쓰기가 이미 최적화되어 있어 빠름

잔액 정보를 RDB 말고 로컬 디스크에 저장: SQLite, RocksDB

  • RocksDB: LSM(log structured merge tree) 사용하여 쓰기 작업 최적화

2. mmap

최근 명령과 이벤트를 메모리에 캐시 할 수도.. 메모리에 캐시 하면 로컬 디스크에서 다시 로드하지 않아도

mmap는 디스크 파일을 메모리 배열에 대응하여 메모리처럼 접근할 수 있게 하여 실행 속도를 높일 수 있음

 

재현성 reproducibility

이벤트를 처음부터 재생하면 과거 잔액 상태 재구성 가능

이벤트 리스트는 불변이고 상태 기계 로직은 결정론적이므로 이벤트 이력을 재생하여 만든 상태는 언제나 동일

계정 잔액 정확성 재검하거나, 코드 수정 후에도 시스템 로직이 올바른지 replay로 확인 가능

 

명령 질의 책임 분리 CQRS

계정 잔액을 공개하는 대신 모든 이벤트를 외부에 보내서 외부 주체가 직접 상태를 재구축 가능(읽기에 유리하게)

읽기 전용 상태 기계는 여러 개 있을 수 있는데, 이벤트 큐에서 다양한 상태 표현을 도출할 수 있다(단순 뷰, 정산 등).

eventual consistency

 

스냅샷: 과거 특정 시점의 상태

모든 것이 파일 기반일 때 재현 프로세스의 속도를 높이는 방법?

이벤트 소싱은 항상 처음부터 다시 읽어서 상태를 파악하는데, 그 대신 주기적으로 상태 파일을 저장하여 시간을 절약 가능

그 시점부터 이벤트 처리 시작, 보통 0시

보통 하둡에 저장

모든 것을 로컬 디스크로(데이터를 한 곳에 두기엔)..  SPOF 위험..

 

높은 신뢰성을 보장할 유일한 데이터는 이벤트

높은 안정성을 제공하려면 이벤트 목록을 여러 노드에 복제해야 하는데 데이터 손실 없고 순서를 유지해야 한다.

합의 기반 복제(consensus based replication)

래프트 알고리즘 사용

  • 래프트 알고리즘(Raft Algorithm)은 분산 시스템에서 합의를 이루기 위한 분산 합의 알고리즘으로, 특히 리더 선출과 로그 복제를 단순하고 이해하기 쉽게 설계한 것이 특징
  • 일관성: Raft는 각 노드가 로그를 일관되게 유지하도록 보장하며, 리더 노드가 로그를 추가하거나 업데이트할 때, 이를 팔로워 노드들에게 전달한다. 모든 노드가 동일한 상태를 유지하도록 보장함으로써 데이터의 일관성을 유지
  • 고가용성: Raft는 단일 노드 실패 또는 리더 실패와 같은 장애를 처리할 수 있도록 설계. 클러스터의 과반수 이상이 살아 있으면, 시스템은 안정적이고 일관성 있게 동작함

 

  • Follower: 리더의 지시를 따르고, 리더의 Heartbeat를 수신하여 상태를 유지
  • Candidate: 리더가 되기 위해 투표를 요청하고, 과반수의 투표를 얻으면 리더가 됨
  • Leader: 클러스터를 관리하고, 클라이언트 요청을 처리하며, 로그 항목을 복제

 

  1. 리더 선출:
    • Follower는 Leader로부터 Heartbeat 메시지를 정기적으로 수신
    • 일정 시간 동안 Heartbeat가 없으면, Follower는 Candidate가 되어 새 리더 선출을 시도
    • 다수의 노드로부터 투표를 받아야 리더로 선출
  2. 로그 복제:
    • Leader는 클라이언트의 요청을 로그에 추가하고 이를 Follower에 복제
    • 과반수의 Follower가 로그를 수락하고 이를 확인하면 Leader는 해당 로그 항목을 커밋
  3. 일관성 유지:
    • Leader는 모든 노드가 동일한 로그 상태를 유지하도록 보장
    • 새로운 리더가 선출되면, 로그 일관성을 확보하기 위해 추가 작업을 수행

리더 장애 처리:

  1. 리더 장애 발생:
    • 리더가 장애를 겪어 더 이상 Heartbeat를 보내지 못하면, Follower는 일정 시간 동안 Heartbeat를 받지 못한 상태가 됨
    • Follower는 Election Timeout이 지나면 Candidate 상태로 전환
  2. 리더 선출 과정:
    • Candidate가 되면, 다른 노드에 투표를 요청하고 선거를 시작
    • 과반수의 투표를 얻으면 Candidate는 새로운 Leader가 됨
    • 새로운 Leader는 기존 로그의 일관성을 확인하고 필요한 경우 Follower에게 누락된 로그를 전송하여 동기화
  3. 장애 복구:
    • 장애가 발생한 리더가 복구되면, Follower 상태로 전환
    • 리더 선출 과정에서 새로운 리더가 선출되었기 때문에 복구된 노드는 더 이상 리더가 아님

팔로워 장애 처리:

  1. 팔로워 장애 발생:
    • Follower가 장애를 겪으면, 로그 복제 및 Heartbeat 수신이 중단
    • Leader는 계속해서 다른 Follower들과 로그를 복제하고 클러스터를 관리
  2. 팔로워 복구:
    • 장애에서 복구된 Follower는 Leader로부터 현재 로그 상태를 동기화
    • Leader는 AppendEntries 메시지를 통해 복구된 Follower에 누락된 로그를 보냄
    • 복구된 Follower는 로그를 복제하고, 현재 상태를 동기화한 후 정상 운영을 재개

장애 상황에 따른 동작:

  • 다수 노드 장애: 클러스터는 과반수의 노드가 살아있으면 정상 동작을 유지합니다. 과반수 이상이 실패하면 클러스터는 동작을 멈추고 장애 복구가 실시
  • 네트워크 파티션: 네트워크가 분할되면, 두 개 이상의 그룹으로 나뉨. 각 그룹은 독립적으로 리더를 선출할 수 있지만, 과반수를 차지하는 그룹만이 유효한 리더를 가질 수 있음. 네트워크가 복구되면, 하나의 리더만 유지되도록 통합

장애 허용을 위한 메커니즘:

  • Election Timeout: 리더의 장애를 감지하기 위한 타이머. 일정 시간 동안 Heartbeat가 없으면 선거가 시작
  • Majority Agreement: 리더가 되려면 과반수의 노드로부터 투표를 받아야. 이는 장애가 발생하더라도 시스템이 계속 운영될 수 있도록 보장
  • Log Consistency: 리더는 모든 팔로워가 일관된 로그 상태를 유지하도록 보장하며, 새로운 리더가 선출될 때 로그 일관성을 유지

 

CQRS에서 읽을 때(폴 vs 푸시)

풀 방식: 클라가 서버에게 request를 보낼 때 읽기 디비에서 가져옴

역방향 프록시: 캐시같이 디비에서 직접 가져가지 않고 만들어진 데이터를 가져가게 둔 중간 저장소

  • 클라이언트와 서버 간의 중간 계층으로, 클라이언트 요청을 백엔드로 전달하고, 백엔드의 응답을 클라이언트로 반환;
  • 이벤트 수신 후 역방향 프락시에 푸시하도록

프로세스 흐름:

  1. 이벤트 수신:
    • 읽기 전용 상태 기계는 외부 시스템으로부터 이벤트를 수신. 이 이벤트는 상태 업데이트를 요구하는 데이터일 수 있음.
  2. 상태 업데이트 및 푸시:
    • 상태 기계는 이벤트를 처리하고 내부 상태를 업데이트
    • 업데이트된 상태는 즉시 역방향 프록시로 푸시. 푸시된 데이터는 클라이언트가 요청하기 전에 프록시에 전달되어 준비.
  3. 클라이언트 요청 처리:
    • 클라이언트가 상태 데이터를 요청하면, 역방향 프록시는 백엔드 서버에 요청을 전달하는 대신, 이미 준비된 최신 상태를 클라이언트에 반환

비동기 이벤트 소싱 프레임워크를 동기식 프레임워크로 제공하기 위해 역방향 프록시(Reverse Proxy)를 추가하는 것은 클라이언트와 서버 간의 통신 방식의 차이를 조율하고, 비동기 시스템의 응답성을 개선하기 위한 전략

1. 비동기 이벤트 소싱 프레임워크의 특성:

  • 이벤트 소싱(Event Sourcing): 시스템 상태를 이벤트의 시퀀스로 기록하고, 현재 상태를 이벤트를 재생하여 복구하는 방식
  • 비동기 처리: 이벤트는 비동기적으로 생성되고 처리되며, 상태는 eventual consistency(최종 일관성)를 가짐. 이는 즉각적인 응답이 보장되지 않고, 처리 완료까지 시간이 소요될 수 있음

2. 동기식 프레임워크 제공의 필요성:

  • 즉각적인 응답 필요: 동기식 시스템은 클라이언트가 요청을 보내면, 즉시 결과를 반환받기를 기대. 비동기 시스템의 특성상 바로 응답을 제공하기 어려운 상황에서, 동기적 동작을 요구하는 클라이언트와의 간극을 줄일 필요가 있음
  • 클라이언트 요구: 많은 클라이언트는 동기적으로 동작하는 전통적인 API 사용에 익숙

3. 역방향 프록시의 역할: 역방향 프록시를 추가함으로써 비동기 시스템을 동기적으로 제공 가능

3.1. 응답 캐싱 및 버퍼링:

  • 이벤트 결과 캐싱: 프록시는 비동기 이벤트가 처리된 결과를 캐싱하여 클라이언트의 요청에 대해 즉시 응답. 이벤트가 아직 처리되지 않았으면, 프록시가 응답을 보류하거나 기본 응답을 반환

3.2. 동기화된 응답 시뮬레이션:

  • 상태 확인 및 응답 대기: 클라이언트의 요청이 들어오면 프록시는 이벤트 소싱 시스템에 상태를 확인하고, 동기적 방식으로 응답을 보류하다가 결과가 준비되면 반환. 이는 동기 호출로 클라이언트에 투명하게 처리되며, 실제로는 백엔드에서 비동기적으로 처리.

3.3. 비동기 이벤트의 프리로드 및 상태 추적:

  • 사전 이벤트 처리: 프록시는 예상되는 이벤트나 데이터를 미리 가져와(이벤트를 받아서) 클라이언트 요청이 들어왔을 때 빠르게 제공. 이를 통해 동기적 행동처럼 느껴지게 함.

4. 의미와 장점:

  • 사용자 경험 개선: 클라이언트는 비동기적 시스템의 지연 시간이나 일관성 문제를 느끼지 않고, 동기적으로 즉각적인 응답을 받음
  • 시스템 간 통합: 비동기 시스템을 사용하면서도 동기적 API가 필요한 클라이언트와 통합할 수 있어, 다양한 환경에서의 호환성이 증가
  • 복잡성 분리: 비동기 처리의 복잡성을 역방향 프록시에서 관리하고, 클라이언트와의 인터페이스를 단순하게 유지가능

5. 고려 사항:

  • 응답 시간 증가: 프록시에서 동기적 응답을 시뮬레이션하는 과정에서 처리 지연이 발생할 수
  • 상태 일관성 관리: 프록시가 비동기 이벤트 결과를 반환할 때, 상태 일관성을 관리하는 로직이 필요
  • 추가 인프라 비용: 역방향 프록시를 운영하는 데 추가적인 인프라와 관리 비용이 발생할 수

6. 근데 이 역할을 할 때 꼭 프록시를 써야하는가? 그냥 다른 중간 서버를 두면 되는거 아냐?

역방향 프록시를 사용함으로써 보안을 강화하거나 로드 밸런싱을 강화할 수 있음. 중간 서버는 유연성은 높지만 속도나 유지보수 등 필요..

 

분산 이벤트 소싱

TC/C 또는 사가 조정자가 단계별 상태 테이블에 각각의 작업 상태를 다 저장하여 트랜젝션 상태를 추적하는 게 포인트

 

  • 사가 또는 tc/c 적용
  • 유저가 서로 다른 위치의 디비에 있다고 가정
  • raft 알고리즘 적용
  • 역방향 프록시 적용

 

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

환경: mac

 

설치

brew install redis

 

접속

// 단일 노드 접근
redis-cli -h alpha-redis-master.abc.net -p 6006 -a {pwd}

// 클러스터 접근
redis-cli -c -h alpha-redis-master.abc.net -p 6001 -a {pwd}
728x90
반응형

'서버 세팅 & tool > vm on mac' 카테고리의 다른 글

[terminal] alias in mac terminal  (0) 2022.06.08
[parallels] local server not working on parallels  (0) 2022.04.11
[vm] nginx 설치  (0) 2022.02.24
[parallels] nox...... 99%.....  (0) 2022.02.23
[vm] axon server 설치  (0) 2022.01.12
반응형

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
반응형
반응형
  • 인당 3계좌까지 가능
  • 계좌 생성 시 코드 입력 시 나와 친구 모두 0.5% 추가 이율


코드:

  • 0125025418
  • 0125025417
  • 0125025416
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
반응형

+ Recent posts