오늘 주말이여서 그간 못했던 내용들을 정리해보고자 한다. 

사실 이전에 해야할것도 좀 밀려있는데 우선순위 높은것으로 추려냈다. 그래도 개발 일기 쓰자고 마음먹었는데 90일은 가야할거 같은 기분이다.. 

 

스프링 큐 (스레드 대기풀)

Thread Pool을 사용하면 Spring 내부에서 Queue 를 사용하는데 이거 어떻게 구현되어있는지 궁금해졌다. 한번 정리를 해볼 시간..

java.util.concurrnet.Executors 패키지는 스레드 풀과 관련된 유틸리티 메소드를 제공하고 이 클래스는 다양한 종류의 스레드 풀을 생성하고 관리하기 위한 편리한 방법을 제공하는 정적 메소드들로 구성되어 있다.

예를 들어 이 패키지를 뜯어보면 newFixedThreadPool은 고정된 수의 스레드를 가진 스레드 풀을 생성할 수 있다.

이거 말고도 newCachedThreadPool,  newSingleThreadExecutor 이런게 있다. 

https://golf-dev.tistory.com/70

예를 들어서 Runnable(반환x, 예외x), Callable (반환 o, 예외 o) 에서 작업을 정의해두면 ExecutorService에서 Runnable, Callable에서 정의한 작업을 스레드 풀에서 실행하게 되는거 같다. 

(뭐.. 참고로 얘기하지만 Runnable은 execute 메소드 사용하고 Runnable객체를 내뱉고, 스레드 풀로 보낼때는 반환값이 없다. Callable은 submit 메소드 사용해서 Callable 객체를 내뱉는데 실제 작업 스레드 풀로 보내고 반환받는 객체는 Future 객체를 반환한다. Future 는 특징적인게 작업이 완료되지 않아도 취소할 수 있다. 이거는 좀 더 보는게 좋을거 같다. 나중에... ) 

 

API에 스레드가 몰리면  이를 어떻게 해결 할지 커넥션  풀 증가 이유 등등 

이런 문제가 생기면 보통 어떻게 접근하는게 좋을까? 시스템에서는 이미 스케일 아웃, 로드 밸런싱이 충분한 상태이다. 캐시도 걸려있다. 그러면 생각해봐야할건 비동기인데 이건 좀 쉽지 않다..  왜냐하면 상품에 대한 정보를 가져와야 하는데 비동기로 실행되면 분명히 누락되는 item들이 존재할거 같다. 

이 파트에서 다뤄보고 싶은건 비동기 처리나 리소스 튜닝에 대한 부분이다. 

Spring 비동기 처리

가장 먼저 드는 생각은 @Async 어노테이션이다. 자바에서는 CompletableFuture 비동기 처리 위한 인터페이스를 제공한다. (클래스임)

그래서 Async는 비동기 메소드 실행이고 CompletableFuture 비동기 프로그래밍을 위한 것이다. 

그냥 쉽게 얘기해서 Spring에서 관리하냐 Java에서 관리하냐 뜻이다. 

그러면 한편으로 드는 생각은 각각 어떨 때 더 최적화일까? 분명 비동기 처리면 상황에 따라 다를것이다. 

간단하게 @Async의 내부 동작원리에 대해 알아보자.

  • @Async 메소드가 호출되면 Spring 은 내부적으로 TaskExecutor 를 사용해 비동기 실행을 한다. 
    • TaskExecutor 는 Executor 인터페이스를 구현하고 위에도 봤듯이 이것은 스레드 풀을 관리한다. 
  • 어노테이션 자체는 프록시 기반으로 동작한다. 실제 메소드 즉 타겟을 호출하는데 직접하는게 아니라 Proxy 를  통해서 호출하게 된다. 
  • 내부적으로 Callable을 쓰느냐 아니면 Runnable 을 쓰느냐에 따라서 반환타입이 있을 수도 있고 없을 수 있다. 

단순히 보면 CompletableFuture는 자바 8에서 도입된 클래스로 @Async를 사용하면서 Future 반환하고 이를 CompletableFuture 이용해 구현할 수도있다. 

ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<String> callable = () -> "Result";

Future<String> future = executorService.submit(callable);

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
    try {
        return future.get();
    } catch (InterruptedException | ExecutionException e) {
        throw new RuntimeException(e);
    }
}, executorService);

하지만 두개의 차이는 프레임워크 vs 표준 API 라는데 있다. 그래서 CompletableFuture 를 사용하게 되면 더욱 세밀하게 비동기를 제어할 수 있다. 

Spring 리소스 튜닝

Spring 리소스 튜닝이 처음에 뭔지 헷갈렸는데 간단하게 스레드 풀 크기 조정이라던지 DB 커넥션 풀 설정 같은것을 의미한다. 

그래서 순차적으로 보자 ! 

스레드 풀 크기 조정 

방법 1.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolConfig {
    public static void main(String[] args) {
        int corePoolSize = 10;
        int maximumPoolSize = 50;
        long keepAliveTime = 120;
        TimeUnit unit = TimeUnit.SECONDS;

        ExecutorService threadPool = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            new LinkedBlockingQueue<Runnable>()
        );

        // 스레드 풀 사용...
    }
}

방법 2.

spring:
  task:
    execution:
      pool:
        core-size: 10
        max-size: 50
        queue-capacity: 100
        keep-alive: 120s
  • core-size : 스레드 풀이 유지될 정도의 최소한의 스레드 수
  • max-size : 스레드 풀이 관리할 수 있는 최대 스레드 수
  • queue-capacity. : 스레드 풀에서 사용가능한 스레드가 없으면 새로운 작업은 여기서 대기
  • keep-alive : 최대 크기를 넘어서 생성된 추가 스레드가 유휴 상태로 유지되는 시간 

DB 커넥션 풀 설정

이건 적당히 잘해야 하는데 

너무 낮아도 문제고 DB에 대기시간이 길어져서 성능 떨어지고 

너무 높으면 리소스 낭비도 되고 서버에 부담을 주게 된다. 

시스템팀이랑 잘 논의해서 결정해보자 ! 

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database
    username: your_username
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10 //커넥션 풀의 최대 크기
      minimum-idle: 5 //커넥션 풀에 유지되어야 하는 최소한의 유휴 연결 수 
      idle-timeout: 30000
      max-lifetime: 2000000
      connection-timeout: 30000
      pool-name: SpringBootHikariCP

Null 값을 포함한 호출

이건 그냥 갑자기 든 생각인데 null로 호출된 api에서 null check 해주는것이 당연한데.. 다르게 처리할 수 있을지 궁금해서 넣어봤다. 

어노테이션 검증

public void processItem(@NotNull String item) {
   
}

옵셔널 파라미터 사용

public void processItem(Optional<String> itemOpt) {
    if (itemOpt.isPresent()) {
        String item = itemOpt.get();
        
    } else {
        
    }
}

 

클러스터링 인덱스

인덱스를 다루는 곳에서 한번 정리하겠지만 여기서는 간단하게 클러스터링 인덱스가 뭔지 확인해보고 넘어가자. 

클러스터링 인덱스는 테이블에서 행 데이터의 물리적인 저장 순서를 인덱스의 키 값 순서와 일치시키는 방식의 인덱스이다. 

당연하겠지만 이러면 데이터 검색 성능이 올라간다. (이미 한번 정렬하고 가는거니까...) 

클러스터링 인덱스도 내부적으로 B-트리, B+트리로 구현되는데 이걸 짚고 가는게 중요한거 같다. 

B- / + 이 구조를 한번 보자 (참고로 + 가 개선된 B 트리 구조이다. 링크드 리스트로 되어있다.) 

  • Root Page
    • 트리 구조에서 가장 상위에 위치한 노드, 인덱스 검색이 시작되는 지점 
  • Intermediate Levels
    • 루프와 리프 사이에 위치하는 레벨, 인덱스 키 기준으로 데이터를 빠르게 찾도록 도움
  • Leaf Pages
    • 실제 데이터 레코드에 대한 참조 또는 데이터 레코드 자체 포함
    • 여기에 실제 데이터 행이 포함되기 때문에 인덱스의 리프 레벨은 데이터 테이블 레코드(=행) 이랑 동일하게 된다. 

참고로 이렇게 찾는데 인덱스 키 값에 따라 탐색하는 성능이 달라진다. 

인덱스 키값이 균일하게 분포될수록 검색 성능이 올라가는것은 당연하다. 일부 키 값에 데이터가 집중되어 있으면 검색 쿼리시 성능이 떨어지게 된다. B+ 트리는 연결리스트로 되어 있기 때문에 B- 트리보다 범위 검색에 강점이 있긴하다.. 

간단하게 주문에서 order_status (주문 상태) 인덱스를 사용하는 주문 테이블이 있다고 가정해보자. (절대 좋은 방식이 아님 unique 하지 않으니까...) 

이 테이블에 주문이 "Shipping" 상태로 적재가 많이 되어있다고 해보자.

주문 상태 인덱스가 "Shipping" 외에도 "Completed", "Cancle", "Return" 여러 상태가 있을 수 있는데 대부분 주문이 "Shipping" 상태라고 한다면 "Shipping" 상태에 해당하는 인덱스 키 값의 레코드가 너무 많아진 상태입니다. 

여기서 주문 날짜나 고객 ID와 같은 복합 인덱스를 구성하지 않으면 검색 분산이 안되어서 B-트리 혹은 B+트리에서 "Shipping" 상태의 레코드가 위치하는 노드에 과부하가 발생하게 되고 해당 노드에 접근하는 시간이 늘어날 수 있게 되면서 결과적으로 성능이 떨어지게 된다. 

이걸 해결할 방법은 복합 인덱스를 구성하던지 아니면 테이블을 파티션을 둬서 특정 인덱스 키 값에 대한 데이터를 분산하는 방법을 생각해볼 수 있게 된다. 

일반적으로는 균형이 잘 잡혀있다면 검색, 삽입, 삭제 작업에서는 효율적이다. 우리가 잘 아는대로 O(logN) 만큼 수행된다. 

왈;

1월 21일 업데이트

오늘 보니까 clustered index 관련해서 좋은 내용이 있어서 추가한다.

clustered index는 이미 인덱스 키값으로 정렬되어 있는 상태이다. 

아래 사진의 블로그 내용을 참고해보면 이 인덱스는 테이블당 하나만 생성할 수 있다고 한다고 한다. 

특징적인것은 InnoDB clusted index 생성 규칙인데 다음을 따른다. 

  • 기본 키(PK) 컬럼 우선
  • Unique + Not Null 컬럼
  • GEN_CLUST_INDEX 생성 
    • 이건 별게 아니고 레코드에 대한 고유한 식별자를 제공하는것 

https://hudi.blog/db-clustered-and-non-clustered-index/

1월 23일 업데이트 

Clustered 와 non - Clustered 어느게 더 성능이 좋을 까 생각해보니까 데이터가 많으면 non-clustered index 를 그렇지 않으면 Clustered index를 쓰는거 같다. 단, non-clsutered index를 쓸때는 clustered index와 혼합해서 사용하는 경우가 많을거 같다.. 

그러면 두가지 장점을 다 쓸 수 있으니까?

Clustered vs Non-Clustered

Clustered index Non-Clustered index
항상 순서를 유지한다 순서와 상관 없다
한 테이블당 하나만 존재한다 (테이블 인덱스) 한 테이블에 여러개 생성할 수 있다
범위 검색에 유리하다 (군집화!) index를 저장할 추가적인 공간이 필요하다
데이터가 많아 질수록 Insert 성능이 나빠진다 Insert시 추가 작업 (인덱스 생성) 필요하다

https://gwang920.github.io/database/clusterednonclustered/ 

 

Clustered vs NonClustered (index 개념)

이번 포스팅에서는 Database의 index 와 관련된 Clustered와 NonClustered의 개념을 알아보자.

gwang920.github.io

근데 궁금한건 clustered index vs clustererd index + non-clustered index 이렇게 하면 어느게 더 좋을 지 모르겠네..