728x90

스레드 정보

Thread 클래스에서 제공하는 메서드를 이용하여 스레드의 정보를 확인할 수 있음

public class ThreadInfoMain {

    public static void main(String[] args) {
        // main 스레드
        Thread mainThread = Thread.currentThread();
        log("mainThread = " + mainThread);
        log("mainThread.threadId()=" + mainThread.threadId());
        log("mainThread.getName()=" + mainThread.getName());
        log("mainThread.getPriority()=" + mainThread.getPriority());
        log("mainThread.getThreadGroup()=" + mainThread.getThreadGroup());
        log("mainThread.getState()=" + mainThread.getState());

        // myThread 스레드
        Thread myThread = new Thread(new HelloRunnable(), "myThread");
        log("myThread = " + myThread);
        log("myThread.threadId()=" + myThread.threadId());
        log("myThread.getName()=" + myThread.getName());
        log("myThread.getPriority()=" + myThread.getPriority());
        log("myThread.getThreadGroup()=" + myThread.getThreadGroup());
        log("myThread.getState()=" + myThread.getState());
    }
}

 

스레드 생명주기

스레드 생명주기

 

스레드 상태

 

상태 설명 상태 전이
NEW 스레드가 생성되었으나 아직 시작되지 않은 상태 • NEW → RUNNABLE: start() 호출
RUNNABLE 스레드가 실행 중이거나 실행될 준비가 된 상태 • RUNNABLE → BLOCKED: 스레드가 lock을 얻지 못한 경우

• RUNNABLE → WAITING: wait() , join() 호출

• RUNNABLE → TIMED_WAITING: sleep(long millis) , wait(long timeout) , join(long millis) 호출

• RUNNABLE → TERMINATED: 스레드 실행 완료
BLOCKED 스레드가 동기화 락을 기다리는 상태 • BLOCKED → RUNNABLE: lock 얻은 경우
WAITING 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태 • WAITING → RUNNABLE: notify() 또는 notifyAll() 호출, join() 이 완료될 때
TIMED_WATING 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태 • TIMED_WATING → RUNNABLE: 설정한 시간 경과 후
TERMINATED 스레드의 실행이 완료된 상태  

 

 

join()

특정 스레드가 완료될 때 까지 기다려야 하는 상황에서 사용

  • 특정 스레드의 작업이 완료될 때까지 기다리는 작업을 sleep()을 통해서 구현하면 정확한 종료 시점을 예측할 수 없음
  • join()을 사용하면 특정 스레드가 종료될 때까지 기다릴 수 있음

다만 단순히 join()을 사용하면 다른 스레드가 완료될 때까지 무기한 기다림

  • 이러한 경우 join(long millis)를 사용하여 타임아웃을 지정할 수 있음

 

interrupt(), isInterrupted(), interrupted()

특정 스레드의 작업을 중간에 중단하는 방법

  • 가장 쉬운 방법은 변수로 flag를 설정하여 스레드가 종료되도록 하는 방법
  • 스레드 내부에서 flag를 확인하기 때문에 스레드 내 로직(e.g. sleep()) 또는 flag 검사 주기에 따라 바로 종료되지 않을 수 있음

WAITING, TIMED_WAITING 상태의 스레드를 직접 깨워서 RUNNABLE 상태로 전환시키는 방법

  • 예를 들어 sleep()에 의해 TIMED_WAITING 상태에 있는 스레드를 깨우기 위해 interrupt()를 호출하면 아래 동작 실행
    • InterruptedException 발생
      • interrupt() 를 호출했다고 해서 즉각 InterruptedException 이 발생하는 것은 아님
      • sleep()처럼 InterruptedException을 던지는 메서드가 호출 또는 실행될 때 예외가 발생
    • TIMED_WAITING 상태에서 RUNNABLE 상태로 전이
    • catch 블록 내 코드가 실행 (RUNNABLE 상태여야 catch 블록 내 코드가 실행될 수 있음)
  • interrupt 상태는 interrupt()를 호출 시 true로 변경되고, InterruptedException이 발생하면 false로 변경됨

InterruptedException에 대한 catch 구문으로만 스레드를 중단하는 경우 InterruptedException을 던지지 않는 부분(e.g. log 출력)은 그대로 실행되고 다음 코드로 진행

  • 더욱 빠르게 작업을 중단하고 싶다면 Thread.interrupted()를 통해 인터럽트 상태를 확인하여 스레드를 종료할 수 있음
    • 인터럽트 상태인 경우 true를 반환하고, 인터럽트 상태를 false로 변경
    • 인터럽트 상태가 아닌 경우 false를 반환, 인터럽트 상태 변경 없음
  • 만약 isInterrupted() 값을 통해 스레드의 인터럽트 상태를 확인하여 스레드를 종료하는 경우 InterruptedException이 발생하지 않기 때문에 인터럽트 상태가 변경되지 않음 (isInterrupted() = true)
    • 이 경우 인터럽트 상태이기 때문에 이후 코드에서 sleep()처럼 InterruptedException을 던지는 메서드가 실행되면 해당 위치에서 기대하지 않았던 InterruptedException이 발생할 수 있음
public class Main {

  public static void main(String[] args) {
    MyTask task = new MyTask();
    Thread thread = new Thread(task, "work");
    thread.start();

    sleep(100); // 시간을 줄임
    log("작업 중단 지시 thread.interrupt()");
    thread.interrupt();
    log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
  }

  static class MyTask implements Runnable {

    @Override
    public void run() {
      // while (!Thread.currentThread().isInterrupted()) { // 인터럽트 상태 변경X
      while (!Thread.interrupted()) { // 인터럽트 상태 변경O
        log("작업 중");
      }
      log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());

      try {
        log("자원 정리");
        Thread.sleep(1000);
        log("자원 종료");
      } catch (InterruptedException e) {
        log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
        log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
      }
      log("작업 종료");
    }
  }
}

 

yield

스레드의 상태를 유지(RUNNABLE 상태)하면서 다른 스레드에게 CPU를 양보(OS에 힌트를 제공)하는 메서드

  • sleep()을 사용하는 경우 RUNNABLETIMED_WAITINGRUNNABLE로 상태가 전이되는 과정을 거치고, 특정 시간동안 스레드가 실행되지 않음
  • 반면 yield()를 사용하는 경우 RUNNABLE 상태로 유지(스케줄링 큐 대기)되고, 다른 스레드에게 CPU를 양보하고 다시 스케줄링 큐에 들어가서 CPU를 할당받음

만약 무의미한 작업이 반복돼서 CPU를 점유하는 경우 yield()를 호출을 통해 다른 스레드에게 CPU를 양보하여 성능을 향상시킬 수 있음

728x90

'Java > 멀티스레드' 카테고리의 다른 글

[JAVA] 스레드 생성과 실행  (1) 2024.09.18
728x90

스레드 생성

  • Thread 클래스를 상속받아 run() 메서드를 오버라이딩하여 스레드를 생성
  • Runnable 인터페이스를 구현하여 스레드를 생성

 

Thread 클래스 상속

스레드 클래스 정의

public class HelloThread extends Thread {

    @Override
    public void run() {
        doSomething();
    }
}

 

스레드 생성 및 실행

  • start() 메서드를 호출하여 스레드를 실행
    • main 스레드와 별도의 스레드를 실행
  • run() 메서드를 직접 호출하면 main 스레드에서 실행
    • main 스레드는 HelloThread 인스턴스에 있는 run() 메서드를 호출하는 형태
public class HelloThreadMain {

    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();
    }
}

 

 

데몬 스레드

스레드는 사용자(user) 스레드와 데몬(daemon) 스레드로 구분

차이는 JVM 종료 시점

  • 모든 user 스레드가 종료되면 JVM도 종료
  • 데몬 스레드는 user 스레드가 모두 종료되면 데몬 스레드도 종료 (why? JVM이 종료돼서)

setDaemon(true) 메서드를 호출하여 데몬 스레드로 설정

  • 데몬 스레드 여부는 start() 실행 전에 결정 (이후에 수정 불가)
public class DaemonThreadMain {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true); // 데몬 스레드 여부
        daemonThread.start();
        System.out.println(Thread.currentThread().getName() + ": main() end");
    }

  static class DaemonThread extends Thread {

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ": run()");
      try {
        Thread.sleep(10000); // 10초간 실행
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      System.out.println(Thread.currentThread().getName() + ": run() end");
    }
  }
}

 

 

run() 메서드 안에서 Thread.sleep() 를 호출할 때 checked exception 인 InterruptedException 을 밖으로 던질 수 없고 반드시 잡아야 함

  • why? run() 메서드는 부모 클래스 Thread의 인터페이스인 Runnable 인터페이스에서 throws를 선언하지 않고 있음
    • 자바 오버라이드 규칙에 따라 부모 메서드 또는 인터페이스가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없음
  • InterruptedException은 checked exception 이기 때문에 try-catch 블록으로 예외를 잡아야 함

 

 

Runnable 인터페이스 구현

Runnable 인터페이스를 구현하는 방식으로 스레드를 생성

public interface Runnable {
     void run();
}
 public class HelloRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println(Thread.currentThread().getName() + ": run()");
     }
}

 

Thread 상속 vs Runnable 구현

스레드 사용할 때는 Thread 를 상속 받는 방법보다 Runnable 인터페이스를 구현하는 방식을 사용하자

 

  • 자바는 다중 상속이 안 된다 -> Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없음
  • 코드의 분리가 안 된다 -> 굳이 Thread 클래스를 상속받아서 디펜던시 및 추가적인 메모리를 할당 할 필요가 없음
  • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다 -> Runnable 인스턴스를 여러 스레드에서 사용할 수 있음 (개별 스레드에서 run() 참조 값이 같음)
728x90

'Java > 멀티스레드' 카테고리의 다른 글

[JAVA] 스레드 제어와 생명주기  (0) 2024.09.18
728x90

프로세스와 스레드

  • 멀티태스킹과 멀티프로세싱
  • 프로세스와 스레드
  • 스레드와 스케줄링
  • 컨텍스트 스위칭

 

멀티태스킹과 멀티프로세싱

구분 관점 설명
멀티태스킹 소프트웨어
(운영체제)
하나의 컴퓨터 시스템이 동시에 여러 작업을 수행
(CPU 사용 시간을 분할하여 동시에 작업이 수행하는 것처럼 보임)
멀티프로세싱 하드웨어 둘 이상의 프로세서(여러 개의 CPU 코어)를 사용하여 여러 작업을 동시에 처리

 

프로세스와 스레드

구분 설명
프로세스 실행중인 프로그램을 프로세스라고 함
(자바로 비유하면 클래스는 프로그램, 인스턴스는 프로세스)
스레드 프로세스 내에서 실행되는 작업의 단위

 

 

프로세스의 메모리 구성

  • 코드 섹션: 실행할 프로그램의 코드가 저장되는 부분
  • 데이터 섹션: 전역 변수 및 정적 변수가 저장되는 부분(그림에서 기타에 포함)
  • 힙 (Heap): 동적으로 할당되는 메모리 영역
  • 스택 (Stack): 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역(스레드에 포함)

그렇다면 JVM은 프로세스 구성 요소 중 어디로 올라가는가? → Heap 영역

 

 

스레드와 스케줄링

작업(스레드)이 실행되기 위해서는 CPU를 할당받아서 연산을 실행해야 함

  • 실제로 CPU에 의해 실행되는 단위는 스레드)
  • 프로세스는 스레드들의 컨테이너 역할을 수행 (스레드들의 실행 환경을 제공)

이를 위해 OS는 스케줄링을 통해 스레드를 어떻게, 얼마나 오랫동안 CPU를 할당할지 결정 (CPU 시간을 여러 작업에 나누어 배분)

 

 

컨텍스트 스위칭

멀티태스킹을 위해 CPU가 여러 작업(스레드)을 번갈아가며 수행

스레드를 번갈아가며 호출하기 위해 CPU는 스레드의 상태를 저장하고 복원하는 작업을 수행

이러한 과정을 컨텍스트 스위칭이라고 함

  • 멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아님

 

 

최적의 CPU 수, 최적의 스레드 수

일반적으로 스레드가 하는 작업은 CPU 바운드 작업, I/O 바운드 작업으로 구분

  • CPU 바운드 작업: CPU를 많이 사용하는 작업
    • 복잡한 수학 연산, 데이터 분석, 비디오 인코딩
  • I/O 바운드 작업: I/O 작업(파일 읽기/쓰기, 네트워크 통신 등)이 많은 작업
    • 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리

일반적으로 웹 애플리케이션 서버는 CPU 바운드 작업 보다는 I/O 바운드 작업이 많음

I/O 바운드 작업이 많다면 CPU를 거의 사용하지 않고 대기

  • 따라서 CPU 코어 수에 맞게 스레드를 생성하면 CPU를 효율적으로 사용할 수 없음
  • 이 때는 스레드 수를 증가시켜 CPU를 최대한 활용할 수 있도록 함

 

CPU-바운드 작업: CPU 코어 수 + 1개

  • CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화

I/O-바운드 작업: CPU 코어 수 보다 많은 스레드를 생성

  • CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성 CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
  • 단 너무 많은 스레드를 생성하면 컨텍스트 스위칭 비용도 함께 증가 - 적절한 성능 테스트 필요
728x90
728x90

https://www.jaegertracing.io/

 

Jaeger ?

Jaeger, inspired by Dapper and OpenZipkin, is a distributed tracing platform created by Uber Technologies and donated to Cloud Native Computing Foundation. It can be used for monitoring microservices-based distributed systems:

  • Distributed context propagation
  • Distributed transaction monitoring
  • Root cause analysis
  • Service dependency analysis
  • Performance / latency optimization

ref. https://github.com/jaegertracing/jaeger

  • Jaeger는 마이크로 서비스라는 상호 연결된 소프트웨어 구성 요소를 모니터링하고 문제를 해결하는 데 사용할 수 있는 소프트웨어

 

Distributed Tracing(분산 추적)의 기본 아이디어

  • 실행되는 컨포넌트마다 실행 시간과 추가 정보 수립
  • 수집한 정보를 DB에 저장
  • DB에 저장된 정보를 가지고 컨포넌트간의 연관관계를 재조합해서 Visualization 도구로 표시함

 

Distributed Tracing Specification

OpenTracing

  • CNCF project로 현재 deprecated 됨
  • OpenTracing Observability 백엔드 서버에 telemetry data (metrics, log, traces)를 전송하기 위해 vendor-netural 표준화된 API를 제공함
  • 개발자는 OpenTracing API 표준화에 맞게 직접 라이브러리를 구현해야 함

OpenCensus

  • Google의 오픈 소스 커뮤니티 프로젝트
  • OpenCensus는 개발자가 자기 어플리케이션을 계측해서 백엔드로 telemetry data를 전송할 수 있도록 언어별 라이브러리 세트를 제공함

OpenTelemetry (OTel)

  • OpenTracing + OpenCensus 프로젝트가 하나로 merge됨
  • 2019년에 CNCF Incubation 프로젝트로 채택됨
  • Trace, metric, log 와 같은 원격 측정 데이터를 기기, 생성, 수집 및 내보내기 위한 공급 업체-중립 오픈 소스 관찰 프레임워크

 

MSA 환경과 OpenTracing이란

모놀리식(monolithic)과 MSA(Micro Service Architecture)에 대해서 간단하게 설명하겠습니다. 모놀리식의 경우 하나의 서버가 서비스의 전반적인 기능을 모두 제공합니다. 그로 인해 복잡도가 증가하고 역할을 나누기 어려운 등 많은 문제가 발생하지만, 클라이언트의 요청을 받으면 하나의 스레드에서 모든 요청을 실행하므로 로그를 확인하기 쉽다는 장점이 있습니다. 그에 반해 MSA의 경우에는 각 서비스의 복잡도가 낮아지고 역할 분담이 용이하지만 클라이언트의 요청을 받았을 때 여러 개의 마이크로 서비스 간에 통신이 발생해 로그를 확인하기 어려운 문제가 있습니다.

이런 문제를 해결하기 위한 방법으로 OpenTracing이 알려져 있습니다.

ref. https://engineering.linecorp.com/ko/blog/line-ads-msa-opentracing-zipkin/

  • 마이크로서비스의 복잡한 동작으로 인해, 분산 시스템에서는 문제를 조사하기가 어려움
    • 요청은 독립적으로 동시에 발생할 수 있으며 꼭 순차적으로 발생하지 않음
  • 문제가 생기면, 개발자들은 어떤 마이크로서비스가 그 문제를 야기했는지 확인해야 함
    • 기존의 문제 추적 방식은 요청의 일부에 대한 정보만 제공하므로 마이크로서비스 문제를 해결하는 데 시간이 많이 걸림
  • 마이크로서비스 상호 작용 내의 요청 경로를 따라가거나 추적하기 위한 도구가 필요함

 

Distributed Tracing vs Open Tracing

Distributed Tracing

a method of tracking application requests as they flow from frontend devices to backend services and databases.

  • 프론트 엔드 장치에서 백엔드 서비스 및 데이터베이스로 이동하는 응용 프로그램 요청을 추적하는 방법

Open Tracing

an open-source project aimed at providing vendor-neutral APIs and instrumentation for distributed tracing.

  • 분산 추적을 위한 특정 오픈 소스 프로젝트를 칭함

 

 

Jaeger 주요 개념 및 용어(Terminology)

Span

A span represents a logical unit of work in Jaeger that has an operation name, the start time of the operation, and the duration. Spans may be nested and ordered to model causal relationships.

  • 작업 이름, 작업 시작 시간 및 기간을 가진 논리적 작업 단위
  • 관계를 모형화하기 위해 범위를 내포하고 순서를 지정

Trace

A trace is a data/execution path through the system, and can be thought of as a directed acyclic graph of spans.

  • 시스템을 통과하는 데이터/실행 경로를 나타내는 그래프

 

Jaeger 구성 요소(Components)

Jaeger client libraries: OpenTelemetry Distro (SDK)

  • Jaeger는 Go, JavaScript, Java, Python 등 다양한 프로그래밍 언어로 클라이언트를 제공
  • 개발자는 분산 추적을 위한 소스 코드를 작성하지 않고, SDK에서 제공하는 API를 사용하여 Jaeger Span을 생성

Agent

  • Jaeger Agent는 요청에 대한 추적을 위해 백그라운드에서 실행되는 네트워크 데몬 또는 프로세스
  • UDP를 통해 클라이언트가 전송하는 Span을 수신
  • Agent는 Amazon EKS와 같은 컨테이너 환경에서 클라이언트에 연결을 지원하고, 에이전트 그룹은 Span을 배치 단위로 생성하여 Collector로 trace 정보를 전송

Collector

  • Jaeger Collector는 추적 정보를 검색하기 위한 구성 요소로, trace 정보를 확인 및 처리 그리고 저장함

Storage

  • Collector를 통해 수집한 추적 정보를 저장하기 위한 저장소 (e.g. ES)

Ingester

  • Jaeger는 Kafka를 통해 trace 정보를 전송할 수도 있음
  • 이 때 Ingester를 통해 Kafka에서 trace 정보를 읽고 별도로 저장할 수 있음

Query

  • Query component를 통해 스토리지에 저장된 trace 정보를 쿼리를 통해 조회할 수 있음

Console

 

 

References

728x90
728x90

디자인 패턴(Design Pattern)

  • 특정 문맥에서 공통적으로 발생하는 문제에 대해 쓰이는 재사용 가능한 해결책
  • 목적별로 일정한 패턴이 제시되어 있음
  • 완전한 정답이 되는 알고리즘과 달리 현재 상황에 맞춰 최적화된 패턴을 결정하여 사용하는 것이 좋음
  • 대표적으로 구체화된 디자인 패턴은 GoF(Gang of Four)에서 제시한 23개의 패턴이 있음

디자인 패턴의 장점

  • 개발자 간의 원활한 협업이 가능
  • 소프트웨어의 구조를 파악하기 용이함
  • 재사용을 통해 개발 시간 단축
  • 설계 변경이 있을 경우 비교적 원활하게 조치가 가능

디자인 패턴의 단점

  • 객체지향적 설계를 고려하여 진행해야 함
  • 초기 투자 비용이 많이 들어감



GoF 디자인 패턴


목적에 따른 분류

  • 생성 패턴, 구조 패턴, 행동 패턴 총 3가지로 구분됨
  • 각 패턴이 어떤 작업을 위해 생성되는 것인지에 따른 구분

생성 패턴 구조 패턴 행동 패턴
Abstract Factory
Builder
Factory Method
Prototype
Singleton
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
Memento
Observer
State
Stratergy
Template Method
Visitor



생성 패턴


  • 생성 패턴은 객체의 생성과 관련된 패턴
  • 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 최소화할 수 있도록 유연성 제공

생성 패턴 의도
추상 팩토리(Abstract Factory) 구체적인 클래스를 지정하지 않고 인터페이스를 통해 연관되는 객체들을 보여줌
빌더(Builder) 객체의 생성과 표현을 분리하여 객체를 생성
팩토리 메서드(Factory Method) 객체 생성을 서브클래스로 분리하여 위임(캡슐화)
프로토타입(Prototype) 원본 객체를 복사하여 객체를 생성(클론)
싱글톤(Singleton) 한 클래스마다 인스턴스를 하나만 생성하여 어디서든 참조



구조 패턴


  • 구조 패턴은 프로그램 내 자료 구조나 인터페이스 구조 등 프로그램 구조를 설계하는데 사용되는 패턴
  • 클래스나 객체를 조합하여 더 큰 구조를 만들 수 있게 해줌

구조 패턴 의도
어댑터(Adapter) 클래스의 인터페이스를 어떤 클래스에서든 이용할 수 있도록 변환
브리지(Bridge) 구현부에서 추상층을 분리하여 각자 독립적으로 변형하고 확장할 수 있도록 함
컴포지트(Composite) 객체들의 관계를 트리 구조로 표현하는 방식으로 복합 객체와 단일 객체를 구분없이 다룸
데코레이터(Decorator) 주어진 상황에 따라 객체에 다른 객체를 덧붙임
파사드(Facade) 서브 시스템에 있는 인터페이스 집합에 대해 통합된 인터페이스 제공
플라이웨이트(Flyweight) 크기가 작은 여러 개의 객체를 매번 생성하지 않고 최대한 공유하여 사용하도록 메모리 절약
프록시(Proxy) 실제 기능을 수행하는 객체 대신 가상의 객체를 사용해 로직의 흐름을 제어



행동(행위 패턴)


  • 행동 패턴은 반복적으로 사용되는 객체들의 커뮤니케이션을 패턴화
  • 객체 사이에 알고리즘 또는 책임을 분배하는 방법에 대해 정의됨
  • 결합도를 최소화하는 것이 주 목적

행동 패턴 의도
책임 연쇄(Chain of Responsibility) 요청을 받는 객체를 연쇄적으로 묶어 요청을 처리하는 객체를 만날 때까지 객체 Chain을 따라 요청을 전달
커맨드(Command) 요청을 객체의 형태로 캡슐화하여 재사용하거나 취소
인터프리터(Interpreter) 특정 언어의 문법 표현을 정의
반복자(Iterator) 컬렉션 구현 방법을 노출하지 않으면서 모든 항목에 접근할 수 있는 방법을 제공
중재자(Mediator) 한 집합에 속해있는 객체들의 상호작용을 캡슐화하여 새로운 객체로 정의
메멘토(Memento) 객체가 특정 상태로 다시 되돌아올 수 있도록 내부 상태를 실제화
옵저버(Observer) 객체 상태가 변할 때 관련 객체들이 그 변화를 전달받아 자동으로 캐싱
상태(State) 객체의 상태에 따라 동일한 동작을 다르게 처리
전략(Stratergy) 동일 계열의 알고리즘군을 정의하고 캡슐화하여 상호 교환이 가능하게 함
템플릿 메서드(Template Method) 상위 클래스는 알고리즘의 골격만을 작성하고 구체적인 처리를 서브 클래스로 위임
방문자(Visitor) 객체의 원소에 대해 수행할 연산을 분리하여 별도의 클래스로 구성
728x90
728x90

인메모리 데이터 저장소


  • 인메모리란 컴퓨터의 메인 메모리 RAM에 데이터를 올려서 속도를 높여서 사용하는 방법이다.
  • SSD,HDD 같은 저장공간에서 데이터를 가져오는 것보다 RAM에 올려진 데이터를 가져오는데 걸리는 속도가 수 백배(HDD 기준) 이상 빠르다. 따라서 인메모리 데이터 저장소를 사용할 때 속도면에서 효과를 볼 수 있다.
  • 하지만 용량적으로 크기가 작기 때문에 메인 데이터베이스로 활용하기에는 무리가 있다.
  • 대표적으로 Redis, Memcached 등이 있다.

Redis vs Memcached


  • Redis와 Memcached는 많이 사용되는 오픈 소스이자 인메모리 데이터 저장소이다.
  • 두 가지 모두 사용하기 쉽고 고성능을 자랑한다.
  • 하지만 엔진으로 사용할 때 차이를 반드시 고려해야한다.
  • Memcached는 명료하고 단순함을 위하여 개발된 반면, Redis는 다양한 용도에 효과적으로 사용할 수 있도록 많은 특징을 가지고 개발되었다.

공통점

  1. 1ms 이하의 응답 대기시간

    • 1ms 이하의 응답시간을 제공
    • 데이터를 메모리에 저장하기 때문에 디스크 기반의 데이터베이스보다 빠르게 데이터를 읽을 수 있음
  2. 개발의 용이성

    • 문법적으로 사용하기 쉽고 개발코드의 양 또한 적음
  3. 데이터 파티셔닝

    • 데이터를 여러 노드에 분산하여 저장시킬 수 있음
    • 수요가 증가할 때 더 많은 데이터를 효과적으로 처리하기 위하여 스케일 아웃이 가능
  4. 다양한 프로그래밍 언어 지원

    • Java, Python, C, C++, C#, JavaScript 등 다양한 언어를 지원

Redis


장점

  • 다양한 데이터 구조를 지원

  • 스냅샷 지원

    • 특정시점에 데이터를 디스크에 저장하여 파일 보관이 가능
    • 장애 상황 발생 시 복구에 사용할 수 있음
  • 복제

    • Master-Slave 구조로 여러 개의 복제본을 생성할 수 있음
    • 데이터베이스 읽기를 확장할 수 있기 때문에 고가용성 클러스터를 제공
  • 트랜잭션

  • Pub/Sub Messaging

    • Publish(발행)과 Subscribe(구독)방식의 메시지를 검색 가능
    • 높은 성능을 요구하는 채팅, 실시간 스트리밍, SNS 피드 그리고 서버 상호통신에 사용할 수 있음

단점

  • 싱글 스레드 작동으로 인한 속도 차이

    • 싱글 스레드로 작동하기 때문에 한 번에 1개의 명령어만 실행할 수 있음
    • keys *와 같이 모든 키를 조회하거나, flushall와 같이 모든 데이터를 삭제하는 명령어를 사용할 때 Memcached는 1ms 정도 소요되지만, Redis의 경우 100만건의 데이터 기준 1초로 엄청난 속도 차이가 발생
    • 해결책
      • keys *scan으로 대체
      • Hash나 Sorted Set 등의 자료구조를 활용
  • 메모리를 2배로 사용

    • Redis는 싱글 스레드로 작동하기 때문에 스냅샷을 뜰 때 자식 프로세스를 하나 만들낸 후 새로 변경된 메모리 페이지를 복사해서 사용함
    • Redis는 copy-on-write 방식을 사용하고 있지만 보통 사용할 때 데이터 변경이 빈번하기 때문에 실제 메모리 양만큼의 메모리를 자식 프로세스가 복사하게 됨
    • 그래서 실제로 필요한 메모리 양보다 더 많은 메모리를 사용하게 됨
  • Redis는 메모리를 직접 처리할 수 없기 때문에 메모리 파편화가 발생하기 쉬움

    • 해결책
      • 다양한 크기의 데이터 사용을 줄이고 유사한 크기의 데이터를 사용하여 파편화를 줄일 수 있음



Memcached


장점

  • 멀티스레드를 지원하기 때문에 멀티 프로세스를 사용할 수 있음

    • 따라서 스케일 업을 통해 더욱 많은 작업을 처리할 수 있음
  • Redis는 트래픽이 몰리면, 응답속도가 불안정하다고 한다. 반면 트래픽이 몰려도 Memcached의 응답 속도는 안정적인 편

  • 메모리 파편화가 Redis에 비해 덜함

  • Redis에 비하면 메타 데이터를 적게 사용하기 때문에 메모리 사용량이 상대적으로 적음


단점

  • 지원하는 타입이 다양하지 않음



Reference

728x90
728x90

AOP(Aspect Oriented Programming)

  • OOP를 보완하는 수단으로, 흩어진 Aspect를 모듈화할 수 있는 프로그래밍 기법

image

  • 동일한 기능이 흩어져 있으면 유지보수하는데 어려움이 존재
  • 각 클래스 내에 흩어진 관심사를 묶어서 모듈화
  • 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용

 

AOP 주요 용어

  • Aspect : 관심사를 모듈화한 것
  • Target : 적용이 되는 대상
  • Advice : 해야할 일(실제 수행되는 코드)
  • Join point : 메서드 실행 시점(Advice를 실제로 실행하고자 하는 위치)
  • Pointcut : 대상 내에 어디에 적용이 되어야 하는지에 대한 정보(Join point를 선정하는 방법)
  • Weaving : Aspect가 target에 적용되는 전체적인 과정, PointCut으로 지정된 JoinPoint에 Advice가 적용되어 Target을 호출 시 AOP Proxy가 만들어지는 과정

image

AOP 구현체

 

AOP 적용 방법

  • 컴파일
  • 로드 타임
  • 런타임

 

Spring AOP가 사용하는 방법 → 런타임 (Dynamic Proxy 기법으로 구현)

  • A라는 Class 타입의 Bean을 생성할 때, A 타입의 Proxy Bean을 생성
  • AOP가 적용된 Target 메서드를 호출 할 때 실제 메서드가 호출되는 것이 아니라 Advice가 요청을 대신 랩핑(Wrraping) 클래스로써 받고 그 랩핑 클래스가 Target을 호출



Spring AOP : Proxy 기반 AOP

 

Spring AOP의 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있음
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라 스프링 IoC와 연동

 

프록시 패턴

image

  • 프록시 패턴을 적용하는 이유 : 기존 코드 변경없이 접근 제어 또는 부가 기능 추가

 

만약 성능(시간)을 측정하는 기능을 추가해야 한다면?

@Service
public class EventServiceImpl implements EventService {

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");

        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();

        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");

        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
        System.out.println("Deleted an event");
    }
}
  • 성능(시간)을 측정하는 부가적인 기능을 추가할 경우 기존 코드를 수정해야 하는 상황이 발생
  • 프록시 패턴을 이용하여 기존 클래스를 두고 프록시 클래스를 작성하여 기능을 위임하고 부가적인 기능을 작성할 수 있음
@Primary
@Service
@RequiredArgsConstructor
public class ProxyEventServiceImpl implements EventService {

    private final EventServiceImpl eventServiceImpl;

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        eventServiceImpl.createEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        eventServiceImpl.publishEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
        eventServiceImpl.deleteEvent();
    }
}
  • 프록시 클래스를 활용하여 기능을 위임
  • 하지만 이 경우에 모든 클래스에 대해 프록시 클래스를 작성해야 하는 비용이 발생
  • 추가로 프록시 클래스 내에도 동일한 기능에 대한 중복 코드가 발생하고 다른 클래스에서 재사용이 어려움

 

Spring AOP 등장 배경

 

Proxy Bean 생성 과정

  1. 원본 클래스의 Bean이 등록
  2. AbstractAutoProxyCreator을 통해 원본 클래스를 감싸는 Proxy Bean을 생성
  3. Proxy Bean을 원본 클래스의 Bean 대신에 등록

 

@AOP

  • 애노테이션 기반의 스프링 AOP

 

의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

 

Aspect 정의 : @Aspect

  • Bean으로 등록해야 하므로 (컴포넌트 스캔을 사용한다면) @Componet도 추가

 

PointCut 정의

 

execution

  • ex. @Around("execution(* com.example..*.EventService.*(..))")
  • execution은 기존 코드를 완전히 건드리지 않고 aspect 내에 작성된 표현식으로 기능을 수행할 수 있음
  • 하지만 pointcut 조합이 어려움
    • ex. &&, ||, !

 

@annotation

  • ex. @Around("@annotation(PerfLogging)")
  • 애노테이션 내 @Retention
    • RetentionPolicy.CLASS : 애노테이션 정보가 바이트 코드까지 남아 있음 (default)
    • RetentionPolicy.SOURCE : 컴파일 후에 사라짐
    • RetentionPolicy.RUNTIME : 런타임까지 유지 (굳이 할 필요 없음)
  • 애노테이션을 정의하고, 해당 애노테이션을 원하는 메소드에 추가

 

Example

// aspect
@Aspect
@Component
public class PerfAspect {

    // @Around("execution(* com.example..*.EventService.*(..))") // execution
    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

// interface
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}

// service
@Service
public class EventServiceImpl implements EventService {

    @PerfLogging
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    ...
}



cf. pointcut 관련

 

참고

728x90

'Spring' 카테고리의 다른 글

[Spring] @Cacheable, @CachePut, @CacheEvict  (0) 2024.08.17
728x90
본 문서는 Discord Migrates Trillions of Messages from Cassandra to ScyllaDB 아티클을 정리한 글입니다.

디스코드에서 트래픽과 메세지가 증가하면서 기존 메세지 스토리지인 Cassandra에서 ScyllaDB로 마이그레이션한 내용을 정리합니다.

 

Discord 시작: MongoDB

MongoDB → Cassandra: What we were doing

 

2015년 개발 당시 Discord는 MongoDB 를 사용

  • a single MongoDB replica set

2015년 11월, 사용이 증가함에 따라 저장된 메세지가 1억 건에 달했고 이 때 성능 문제가 발생

  • 데이터/인덱스가 메모리(RAM) 크기를 넘어섬
  • latency가 예측 불가능해짐

메세지의 수는 지속적으로 증가

  • 2016년 7월: 일 4천만 개
  • 2016년 12월: 일 1억 개

 

MongoDB 를 사용했을 때 발생했던 문제점과 새로운 DB 선택 요구사항

MongoDB → Cassandra: Choosing the Right Database

 

MongoDB 를 사용했을 때 발생했던 문제점

매우 랜덤하게 읽기 작업이 발생

  • 읽기/쓰기 비율이 5:5

음성 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 1000개 메세지 발생

  • 소수 메세지 조회 → 디스크에 많은 랜덤 검색 발생 → 캐시 효율 감소

비공개 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 10~100만개의 메세지 발생

  • 요청은 최근 메세지만 조회, 회원 수는 100명 미만 → 요청 비율이 낮고, 캐시 적중률(히트)이 낮음

대규모 공개 Discord 서버(디스코드 내 organization/workspace): 연간 수백만 개의 메세지 발생

  • 최근 한 시간 내에 발생하는 메세지(데이터)를 조회하는 경우가 많음 → 캐시 히트가 높음

또한 앞으로 랜덤 검색을 유발하는 기능들이 추가될 예정


DB 선택 요구사항

  • 선형 수평 확장: 솔루션 재검토하거나 데이터를 수동으로 재배치(re-shard)하고 싶지 않음
  • 자동 장애 조치: 스스로 복구될 수 있는 시스템
  • 낮은 유지보수 비용: 데이터가 증가하면 노드만 추가
  • 검증된 기술: 새 기술을 좋아하지만 너무 새롭지 않은 기술
  • 예측 가능한 성능: p95의 응답 시간이 80ms 이하, Redis 또는 Memcached에 메세지를 캐시하고 싶지 않음
  • blob 저장소 아닌 저장소: 초당 수천 개의 메세지가 작성되기 때문에 blob 직렬화/역직렬화는 비효율적임
  • 오픈 소스: 타사에 의존하고 싶지 않음

 

Cassandra 선택

Cassandra → ScyllaDB: Our Cassandra Troubles


2017년: 12개 노드로 시작, 수 십억 개의 메세지를 저장

 

적용 후 좋았으나 GC가 10초동안 발생하는 문제가 발생하기도 함

  • 툼스톤(데이터 삭제 작업)때문에 발생한 문제
    • 툼스톤 기간: 10일에서 2일로 축소
    • 빈 버킷을 조회하지 않도록 함

 

2022년: 177개 노드, 메세지는 수 조개에 도달

 

많은 동시 읽기 → 핫 파티션 → 성능 문제 발생: 대기 시간 예측 불가

  • 카산드라: 읽기 비용 > 쓰기 비용
  • 읽기는 Memtable(메모리)에 데이터가 없으면 SSTable(파일)을 조회

유지보수 비용 증가

  • SSTable 압축에 따른 성능 문제 → gossip dance 운영 작업
  • gossip dance: 클러스터 내 노드 중 한 대를 가져와서 트래픽을 받지 않고, 파일을 압축하고, 다시 클러스터에 돌려보내는 작업을 반복

 

아키텍처 변경

Cassandra → ScyllaDB: Changing Our Architecture


Cassandra → ScyllaDB

  • GC로 인해 발생하는 지연 시간 문제 감소

데이터 서비스 API(레이어) 추가

  • 동일 데이터에 대한 여러 요청을 한 번에 DB로 보냄 → DB로 갈 쿼리 수를 줄여서 DB 부하를 감소
  • 일관성 해시(consistence hashing) 사용으로 동일 데이터에 대한 요청은 동일 데이터 서비스로 보냄

 

결과

2022년 5월 기준

  • Cassandra 177개 노드 → ScyllaDB 72개 노드
    • 노드 별 평균 4TB 디스크 → 9TB 디스크
  • 메세지 히스토리 읽기 p99: 40-125ms → 15ms
  • 메시지 작성 p99: 5-70ms → 5ms
  • 온콜 대응이 줄어듦

 

 

Reference

728x90
728x90

Web Server: HTTP를 통해 웹 브라우저에서 요청하는 HTML 문서나 오브젝트(이미지 파일 등)을 전송해주는 서비스 프로그램을 말한다.


Overview

  • Apache와 Nginx는 웹 서버를 운영할 수 있는 오픈 소스 서버 기술을 제공

  • Apache는 오랫동안 이용되어 왔고, 근래에는 Nginx가 점유가 높아짐

    • 최근 점유율

      possesion

  • 웹 서버의 선택은 인터넷 트래픽에 영향을 주기 때문에 상황에 알맞는 선택이 필요

  • 두 기술은 작동 방식이 다르기 때문에 각각의 작동 방식 및 장단점을 알고 사용하자


Apache Nginx

Apache

  • Apache HTTP는 1995년부터 많이 사용되어진 웹 서버로 HTTP 표준을 준수하도록 구축된 오픈 소스이며, 고성능 웹 서버

Nginx

  • Apache의 C10K 문제점 해결을 위해 만들어진 Event-Driven 구조의 웹 서버
    • C10K : 일만개의 클라이언트 문제, 한 시스템에 동시 접속자수가 1만명이 넘어갈 때 효율적 방안

Compairson

설계 구조

  • 클라이언트의 요청을 처리하고 응답하는 방식의 차이

Apache

  • 프로세스 기반 접근 방식으로 하나의 스레드가 하나의 요청을 처리하는 구조
  • 매 요청마다 스레드를 생성 및 할당해야 하기 때문에 리소스를 많이 잡아먹음

thread programming


Nginx

  • 이벤트 중심 접근 방식으로 하나의 스레드 내에서 여러 요청을 처리하는 구조
    • 비동기 Event-Driven 구조 : Event Handler에서 비동기 방식으로 먼저 처리되는 요청을 진행
  • 코어 모듈이 Apache보다 적은 리소스로도 많은 트래픽을 효율적으로 처리 가능

event driven programming

cf.

  • 적은 스레드가 사용되면 CPU 소모가 적고, context switching 비용이 감소

  • Spring에서 Sevlet 기반 MVC(멀티 스레드)와 WebFlux(이벤트 루프) 비교할 때와 유사

  • 대용량 트래픽을 처리하기 위해 가벼움과 높은 성능을 목표로 한다면 Nginx가 적합


성능 차이

정적 컨텐츠

  • Apache는 서버 컴퓨터의 디스크에 저장하는 파일 기반 방법을 사용하여 정적 콘텐츠를 처리
  • Nginx는 설계 아키텍처 구조상 Nginx가 적은 비용으로 효율적으로 제공
  • Nginx는 최대 1,000개의 동시 연결을 실행하는 벤치마크 테스트에 따르면 Apache보다 2.5배 더 빠른 성능을 발휘

동적 컨텐츠

  • 두 웹 서버의 성능이 비슷함
  • Apache는 외부 구성 요소에 의존할 필요 없이 웹 서버 자체 내에서 동적 컨텐츠를 처리할 수 있음
  • Nginx는 동적 컨텐츠를 웹 서버 내에서 처리하지 않지만 SCGI 핸들러와 FastCGI 모듈을 사용해서 동적 컨텐츠 제공할 수 있음

OS 지원 여부

Apache

  • Linux 및 BSD를 포함한 모든 Unix 계열 OS 지원
  • Windows 모두 지원

Nginx

  • 거의 모든 Unix 계열 OS 지원
  • Windows는 부분적으로 지원

중앙 집중/분산 구조

Apache

  • .htaccess 파일을 통해 디렉토리 별로 추가 구성을 허용
  • 이로 인해 권한이 없는 사용자가 웹 사이트의 특정 측면을 제어할 수 있음

Nginx

  • 추가 구성을 허용하지 않음
  • 권한이 없는 사용자가 웹 사이트의 특정 측면을 제어할 수 없지만 추가 구성을 제공하지 않음으로써 성능 향상
  • 디렉토리 구성을 허용하지 않음으로 .htaccess 파일을 검색하고 사용자가 만든 요구 사항을 해석할 필요 없기 때문에 Apache보다 빠르게 요청을 처리할 수 있음

요청 처리 및 해석하는 방법의 차이

Apache

  • 요청을 해석하기 위해 파일 시스템 위치 전달
  • URI 위치를 사용하지만 일반적으로 더 추상적인 디렉토리 구조를 사용

Nginx

  • 요청을 해석하기 위해 URI를 전달
  • URI로 전달함으로써 웹 서버뿐만 아니라 프록시 서버, 로드 밸런서 및 HTTP 캐시로 쉽게 동작 가능
  • 서버에서 클라이언트로 데이터가 전송되는 속도가 Apache보다 더 빠름

기능 모듈의 차이

Apache

  • 동적으로 로드 가능한 다양한 60개의 공식 모듈을 제공
    모든 모듈을 가지고 서버가 실행되지만 실제 사용되는 모듈은 소수임 = 무거움

Nginx

  • 타사 플러그인 과정으로 선택되고 컴파일되기 때문에 동적으로 모듈을 로드할 수 없음
  • 사용하려는 기능만 선택해서 서버를 실행 = 가벼움

유연성

Apache

  • 동적 모듈과 로딩을 지원함

Nginx

  • 아직까지는 동적 모듈과 로딩을 지원하지 않음

보안

  • 두 웹 서버 모두 C언어 기반으로 확장된 보안을 제공
  • 하지만 Nginx가 코드가 더 작기 때문에 미래 지향적인 보안 관점에서 장점을 가짐(비슷하지만 Nginx가 조금 더 안전한 것으로 간주)

compairson

Summary

항목 Apache Nginx
작동 방식 프로세스 기반 접근 방식
(멀티 스레드 or 멀티 프로세스)
이벤트 중심 접근 방식
(단일 스레드)
성능 차이
(정적 컨텐츠)
Nginx가 효율적
성능 차이
(동적 컨텐츠)
비슷함
(다만 Nginx는 내부에서 처리하지 못 하고 핸들러를 통해 처리)
OS 지원 범위 거의 비슷하지만 조금 다름
(Apache가 좀 더 넓게 지원)
요청 해석 파일 디렉토리 구조 접근 URI 전달
무게 사용하지 않는 모듈도 포함하기 때문에 무거움 가벼움
  • Apache는 오랫동안 이용되어 왔기 때문에 신뢰할 수 있고 문제가 발생했을 때 참고할 자료가 Nginx에 비해 많다. 추가로 다양한 동적 모듈을 로드하는데 이점이 있다.

  • 하지만 다량의 트래픽이 발생하는 경우에는 Nginx가 안정성과 속도면에서 확실히 효율적이다.

  • 추가로 두 웹 서버를 함께 사용해도 된다. Apache 앞단에 Nginx를 프록시 서버로 활용할 수 있다.

    both


Reference

728x90
728x90

Overview

캐시는 서버의 부담을 줄이고, 성능을 높이기 위해 사용되는 기술이다

  • 예를 들어 어떤 요청을 처리하는데 계산이 복잡하거나 혹은 DB에서 조회하는게 오래 걸리는 등에 적용하여 결과를 저장해두고 가져옴으로써 빠르게 처리할 수 있다.
    • 캐시는 값을 저장해두고 불러오기 때문에 반복적으로 동일한 결과를 반환하는 경우에 용이하다.
  • 만약 매번 다른 결과를 돌려줘야 하는 상황에 캐시를 적용한다면 오히려 성능이 떨어지게 된다.
    • 오히려 캐시에 저장하거나 캐시를 확인하는 작업 때문에 부하가 생기기 때문이다.
  • 그러므로 캐시는 동일한 결과를 반환하는 반복적인 작업과 시간이 오래 걸려서 서버(애플리케이션)에 부담이 되는 경우에 적용하면 좋다.

스프링은 AOP 방식으로 편리하게 메소드에 캐시를 적용하는 기능을 제공하고 있다.

 

 

Spring에서 캐시 적용하기

스프링에서는 `@Cacheable`, `@CachePut`, `@CacheEvict`어노테이션을를 활용해서 캐시를 적용할 수 있다.

 

@EnableCaching

@Cacheable과 같은 어노테이션 기반의 캐시 기능을 사용하기 위해서 `@EnableCaching`를 추가한다.

@EnableCaching
@Configuration
public class CacheConfig {
    ...
}

 

캐시를 관리해줄 CacheManager를 Bean으로 등록한다.

 

스프링 공식 문서 - 캐시 추상화 (Cache Abstarction)에서 현재 지원되는 다양한 매니저의 종류를 확인할 수 있다.

 

 

@Cacheable

캐시를 저장/조회를 설정한다.

  • 보통 메소드에 적용한다.
    • 클래스나 인터페이스에 지정할 수도 있지만 그런 경우는 극히 적다.
  • 캐시에 데이터가 없을 경우에는 기존의 로직을 실행한 후에 캐시에 데이터를 추가한다.
  • 캐시에 데이터가 있으면 캐시의 데이터를 반환한다.

메소드의 파라미터가 1개인 경우

@Cacheable("todayWebtoon")
public Webtoon getTodayWebtoon(String webtoonNo) {
    // logic
}

 

파라미터로 넘어온 `webtoonId`를 기준으로 `todayWebtoon` 캐시에서 값을 조회한다.

  • 값이 없으면 logic을 실행하고, 반환 값을 저장한다.
  • 값이 있으면 저장된 값을 반환한다.

만약 파라미터가 없는 경우 디폴트 값을 key로 활용하면 된다.

 

메소드의 파라미터가 여러 개인 경우

@Cacheable(value = "todayWebtoon", key = "webtoonNo")
public Webtoon getTodayWebtoon(String webtoonNo, Date date) {
    // logic
}

 

파라미터가 여러 개인 경우 key를 지정해준다.


메소드의 파라미터가 객체인 경우

@Cacheable(value = "todayWebtoon", key = "#webtoon.webtoonNo")
public Webtoon getTodayWebtoon(Webtoon webtoon, Date date) {
    // logic
}

 

Key값의 지정에는 SpEL이 사용된다. 그렇기 때문에 만약 파라미터가 객체라면 다음과 같이 하위 속성에 접근하면 된다.

 

 

@CachePut

캐시에 값을 저장하는 용도로만 사용된다.

@CachePut(value = "todayWebtoon", key = "webtoonNo")
public Webtoon updateWebtoon(String webtoonNo) {
    // logic
}

 

실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시의 내용을 사용하지는 않고 항상 메소드의 로직을 실행한다.

 

@CacheEvict

캐시를 제거하는 용도로 사용된다.

 

만약 값이 달라지거나 현재 저장된 값이 무의미한 값이라면 제거되어야 한다.

 

캐시를 제거하기 위해서는 크게 두 가지 방법이 존재한다.

  • 일정한 주기로 캐시를 제거
  • 값이 변할 때 캐시를 제거

예를 들어 하루에 한 번씩 바뀌는 정보라면 batch 또는 scheduled 기능을 이용하여 일정 시간 주기로 캐시를 삭제한다.


특정 캐시 내의 값을 모두 제거하는 경우

@CacheEvict(value = "todayWebtoon", allEntries = true)
public void clearTodayWebtoon() {
    // logic    
}

 


캐시 내 특정 값만 제거하는 경우

@CacheEvict(value = "todayWebtoon", key = "webtoon.webtoonNo")
public void clearWebtoon(Webtoon webtoon) {
    // logic    
}

 

728x90

'Spring' 카테고리의 다른 글

[Spring] AOP (Aspect Oriented Programming)  (0) 2024.08.28

+ Recent posts