스레드 Pool

병렬 작업 처리가 많아지면 스레드 갯수가 증가하고 그에 따른 스레드 생성과 스케줄링으로 인해 메모리 사용량이 늘어납니다.
갑작스런 병렬 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀을 사용해야 합니다.

스레드풀은 작업 처리에 사용되는 스레드를 제한된 갯수만큼 정해 놓고 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리합니다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리합니다. 그렇기 때문에 작업 처리 요청이 폭증 되어도 스레드의 전체 개수가 늘어나지 않으므로 애플리케이션의 성능이 급격히 저하되지 않습니다

자바는 스레드풀을 생성하고 사용할 수 있도록 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있습니다


스레드풀 생성 및 종료

스레드풀 생성

스레드풀을 생성하는 방법은 크게 2가지가 있습니다.

  1. newCachedThreadPool()
    • 초기스레드0, 코어스레드0, 최대스레드 Integer.MAX_VALUE
  2. newFixThreadPool(int nThread)
    • 초기스레드0, 코어스레드0, 최대스레드 nThreads
  • 초기 스레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수
  • 코어 스레드 수 : 스레드가 늘어난 후 사용되지 않은 스레드를 스레드 풀에서 제거할 때 최소한으로 유지해야할 수
  • 최대 스레드 수 : 스레드풀에서 관리하는 최대 스레드 수
newCachedThreadPool()

스레드 개수보다 작업 개수가 많아지면 새로운 스레드를 생성하여 작업을 처리합니다
만약 스레드가 60초동안 아무일을 하지않으면 스레드를 종료시키고 스레드풀에서 제거합니다

ExecutorService executorService = Executors.newCachedThreadPool();
newFixedThreadPool(int nThreads)

스레드 개수보다 작업 개수가 많으면 스레드를 새로 생성하여 작업을 처리합니다
newCachedThreadPool과 다른 점은 일을 하지 않아도 스레드를 제거하지 않습니다

ExecutorService executorService = Executors. newFixThreadPool();

newCachedThreadPool(),newFixedThreadPool() 메서드를 사용하지 않고
직접 스레드 개수들을 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성하면 됩니다.

ExecutorService threadPool = new ThreadPoolExecutor(
    3,                                     // 코어 스레드 개수
    100,                                // 최대 스레드 개수
    120L,                                // 놀고 있는 시간
    TimeUnit.SECONDS,                     // 놀고 있는 시간 단위
    new SynchronousQueue<Runnable>()     // 작업큐
);

 


스레드풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있습니다. 중요한 부분인데 main() 메서드가 실행이 끝나도 애플리케이션 프로세는 종료되지 않습니다. 따라서 애플리케이션 (ex, Spring Batch)을 종료하려면 스레드풀을 종료시켜 스레드들이 종료상태가 되도록 처리해야 합니다

executorService.shutDown();  
// 또는
executorService.shutDownNow();

shutdown()
작업큐에 남아있는 작업까지 모두 마무리 후 종료
shoutdownNow()
작업큐 작업 잔량 상관없이 강제 종료


스레드 작업 생성과 처리 요청

작업 생성

스레드 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현합니다.
Runnable과 Callable의 차이는 작업 처리 완료후 리턴값이 있느냐 없느냐 입니다.

Ruunable
Runnable task = new Runnable() {
    @Override
    public void run() {
        // 스레드가 처리할 작업 내용
    }
}
Callable
Callalbe<T> task = new Callable<T>() {
    @Override
    public T call throws Exception() {
        // 스레드가 처리할 작업 내용
        return T;
    }
}

작업 처리 요청

public class ExecuteExample {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for(int i = 0; i < 10; i++){
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
                    int poolSize = threadPoolExecutor.getPoolSize();
                    String threadName = Thread.currentThread().getName();

                    System.out.println("[총 스레드 개수:" + poolSize + "] 작업 스레드 이름: "+threadName);
                    int value = Integer.parseInt("윤호");
                }
            };

            //스레드풀에게 작업 처리 요청
            executorService.execute(runnable);
            //executorService.submit(runnable);

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }        
        }     
        executorService.shutdown();
    }
}
execute()로 실행했을 경우


execute로 실행했을 때에는 작업 처리 도중 예외가 발생하면 해당 스레드는 제거 되고 새 스레드가 계속 생성됩니다.

submit()으로 실행 했을 경우


submit의 경우 예외가 발생하더라도 스레드가 종료되지 않고 계속 재사용이 되어 다른 작업을 처리하는 것을 볼 수 있습니다

execute와 submit의 차이는 두가지가 있습니다
하나는 execute()는 작첩 처리 결과를 받지 못하고, submit()((은 작업 처리 결과를 받을 수 있도록 Future를 리턴합니다.
두번째로는 위의 예에서 알아본 예외 발생의 경우입니다. execute()는 스레드를 종료하고 스레드풀에서 제거됩니다. submit()의 경우에는 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용됩니다

그렇기 때문에 가급적 스레드 생성의 오버헤드를 줄이기 위해서 submit()을 사용하는 것이 좋습니다


참조

  • ‘이것이 자바다’ - 신용권님 저
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

이전 포스팅에선 다음과 같이 알아 봤는데요

  • Thread 생성
  • Thread 동기화 블록

이번에는 Thread가 제공하는 몇가지 메서드를 확인해보고
Thread 상태를 제어하는 방법에 대해 알아보도록 하겠습니다


Thread 상태

스레드를 생성하고 start() 메서드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만, 사실은 '실행대기' 상태가 됩니다.
실행대기 상태란 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태를 말합니다.
실행대기 상태에 있는 스레드 중에서 스케줄링으로 선택된 스레드가 비로서 CPU를 점유하고 run() 메서드를 실행하게 됩니다.
이때를 실행 상태라고 합니다.

아래의 사진으로 스레드의 대략적인 상태를 살펴 보겠습니다

실행 상태의 스레드는 다시 실행대기 상태로 돌아갈 수 있고
실행, 실행대기 상태를 번갈아가며 자신의 run() 메서드를 수행합니다.
run() 메서드가 종료되면 스레드의 실행은 멈추게 됩니다.
이 상태를 종료 상태라고 합니다

경우에 따라서 ‘실행상태’에서 ‘일시정지’ 상태로 가기도 하는데,
일시정지 상태에서는 스레드가 실행할 수 없는 상태입니다.
일시정지 상태로는 WAITING, TIMED_WAITING, BLOCKED가 있습니다.
스레드가 다시 실행 상태로 가기 위해서는 일시정지 상태에서 실행대기 상태가 되어야 합니다.

상태 Enum 상수 설명
객체 생성 NEW 레드 객체 생성, 아직 start() 메서드 호출되지 전의 상태
실행대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시정지 WAITING 다른 스레드가 통지(notify) 할 때까지 기다리는 상태
일시정지 TIMED_WAITING 주어진 시간 동안 기다리는 상태
일시정지 BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

스레드의 상태 제어

사용자는 미디어 플레이어에서 동영상을 보다가 일시 정지 시킬수도 있고, 종료시킬수도 있습니다
정지는 다시 동영상을 보겠다는 의미로 스레드를 일시정지 상태로 만들어야 합니다
종료는 더 이상 동영상을 보지 않겠다는 의미이므르 미디어 플레이어는 스레드를 종료 상태로 만들어야 합니다
이와 같이 스레드의 상태를 변경하는 것은 스레드 상태 제어라고 합니다

아래는 스레드 상태를 제어하는 메서드입니다. 하나씩 메서드를 알아보도록 하겠습니다

일시정지 (sleep)

sleep() - 주어진 시간동안 일시 정지 상태로 만들고, 주어진 시간이 지나면 자동적으로 실행대기 상태가 됩니다
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
} 

Thread.sleep(3000) 메서드를 통해 3초간 ‘일시정지’ 상태가 되고 3초가 지나면 다시 ‘실행대기’ 상태로 돌아오게 됩니다.

실행 양보 (yield)

yield() - 실행중에 우선순위가 높은 또는 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 됩니다
if (work) {
    System.out.println(getName() + " 스레드 작업 내용");
} else {
    Thread.yield();
}

yield() 메서드 호출시 해당 Thread는 우선 순위가 높거나 동일한 Thread에게 실행을 양보하고 실행 대기 상태로 전환됩니다

다른 스레드의 종료를 기다림 (join)

join() -메서드를 호출한 스레드는 일시 정지 상태가 됩니다. 실행 대기 상태로 가려면 join() 메서드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 합니다. 이부분은 예제를 보도록 하겠습니다
SumThread
public class SumThread extends Thread {
    private long sum;
    public long getSum() {
        return sum;
    }

    public void run() {
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
    }
}
JoinExample
public class JoinExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join();
        } catch (Exception e) {
        }

        System.out.println("합 : " + sumThread.getSum());
    }
}

sumthread.join()을 걸어 두었기 때문에 메인 Thread는 합연산이 끝날때까지 기다리게 됩니다. 따라서, 결과값은 5050이 나오게 됩니다

여기서 의문이 들 수 있습니다. Thread.sleep(3000)으로도 충분히 가능한데 무엇이 차이인가? sleep은 일정 시간동안만 실행대기 상태로 만들어줍니다. join의 경우 thread의 작업을 처리할때까지 묶어 둘 수 있기 때문에 멀티스레드 환경에서 동기화를 맞춰야 할 경우 유용하게 사용할 수 있습니다.

스레드간 협업 (wait, notify, notifyAll)

notify, notifyAll - 동기화 블록 내에서 wat 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만들어 줍니다
wait - 동기화 블록 내에서 스리드를 일시 정지 상태로 만듭니다. 매개값으로 시간이 주어지면 일정 시간이 지난후 자동으로 실행대기 상태가 되고, 시간이 주어지지 않으면 notify, notifyAll 메서드에 의해 실행 대기 상태로 갈 수 있습니다

스레드 종료 (stop, interrupt)

Thread는 자신의 run() 메서드가 모두 실행되면 자동으로 종료됩니다. 경우에 따라서 Thread를 즉시 종료할 필요가 있습니다

stop - 갑자기 종료하면 스레드가 사용중이던 자원들이 불안전한 상태로 남겨지기 때문에 deprecated 되었습니다
interrupt - 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다

참조

  • '이것이 자바다' - 신용권님 저
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

이전 포스팅 에선 n개의 스레드를 생성하여 실행하여 보았습니다.


작업의 동시성을 위해 thread는 매우 유용하게 사용되는데요,
하지만 공유되는 자원에 대해서 동기화 처리가 되지 않는다면 문제를 일으킬 수 있습니다.
이번 포스팅에선 thread를 동기화하여 안전하게 공유 자원을 사용하는 방법에 대해서 알아 보도록 하겠습니다.

 


비동기화

먼저, 동기화 처리가 되지 않는 thread의 결과값을 확인해보겠습니다
'이것이 자바다'의 나오는 예제입니다.

Calculator

public class Calculator {

    private int memory;

    public void setMemory(int memory) {
        this.memory = memory;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}

User1

public class User1 extends Thread {

    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User1"); // set thread name
        this.calculator = calculator;
    }

    public void run() {
        calculator.setMemory(100);
    }
}

User2

public class User2 extends Thread {

    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User2"); // set thread name
        this.calculator = calculator;
    }

    public void run() {
        calculator.setMemory(50);
    }
}

Main

public class MainThreadExample {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        User1 user1 = new User1();
        user1.setCalculator(calculator);
        user1.start();

        User2 user2 = new User2();
        user2.setCalculator(calculator);
        user2.start();
    }
}

결과

User1: 50
User2: 50

User1은 setMemory에서는 100을, User2 setMemory에서는 50을 설정하였습니다.
그러나 User1의 결과는 100이 아닌 50이 설정되었습니다.

그이유는 즉슨, User1과 User2 Calculator를 공유하여 사용하고 있습니다
다만, memory set후 바로 출력하는것이 아닌 2초간의 sleep time이 있기 때문입니다
따라서 원하는 값을 얻기 위해서는 공유 자원의 대한 제어가 필요합니다


동기화 블록 설정

스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날떄 까지
객체에 잠금을 걸어 다른 스레드가 사용할 수 없도록 해야합니다.


이처럼 멀티스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 합니다.
자바는 임계 영역을 지정하기 위해 동기화 메서드블록을 제공합니다

동기화 메서드

public synchronized void setMemory(int memory) {
    this.memory = memory;

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}

동기화 블록

public void setMemory(int memory) {
    synchronized (this) {
        this.memory = memory;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}

이외에도 'volatile' 이라는 키워드가 있습니다.
'volatile'은 변수에 적용하여 Cache memory가 아닌 main memory에서 동작하게 설정됩니다.
즉, 멀티스레드 환경에서 임계영역이 아닌 main memory에서 동작하기 때문에
여러 스레드가 공유 자원에 대해 동시에 '읽기-쓰기' 작업을 진행한다면 원자성을 보장해주지 않습니다.

따라서, 위와 같은 공유자원에 여러 스레드가 '읽기-쓰기' 작업을 수행한다면,
임계영역을 보장해주는 synchronized 키워드를 사용해야 합니다

참조

이것이 자바다 - 신용권님 저

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

우리는 하나의 프로그램, 프로세스로 두 가지 이상의 작업을 처리할 수 있다는 것을 알고 있습니다.
스레드를 이용하면 두가지 이상의 일을 동시에 할 수 있는데요,
이번 포스팅에선 Java를 이용해서 스레드를 생성하는 방법을 알아보도록 하겠습니다.


프로세스와 스레드

스레드를 다루기 전에 필수적인 용어만 짚고 넘어가도록 하겠습니다

  • 프로그램은 파일 시스템에 존재하는 실행파일 (*.exe)
  • 프로세스는 운영체제에서 실행중인 애플리케이션
  • 스레드는 프로세스 내에서 실행되는 여러 흐름의 단위

간단히 정리하면, 프로그램은 실행파일입니다.
이 실행파일은 메모리에 올린 것은 프로세스라고 하는데요
프로세스는 n개의 thread로 구성되어 있습니다.

운영체제의 대한 추가적인 설명은 생략하도록 하고, 자세히 설명되어 있는 링크를 달아두도록 하겠습니다.
스레드, 프로세스의 대한 개념 정리가 필요하시다면 한번씩 읽어보세요 😄
Process와 Thread 이야기. 프로세스(Process) | by Charlezz | Medium


Java Thread 생성

Java에서 Thread를 생성하는 방법은 두가지가 존재합니다

  1. Thread 클래스를 상속 받아 run 메서드를 오버라이딩 하는것
  2. Runnable 인터페이스를 Implements 하여 run 메서드를 정의하는것

먼저, Thread 상속

public class MyThread extends Thread {
    private int index;
    public MyThread(int index) {
        this.index = index;
    }

    @Override
    public void run() {
        System.out.println(this.index + " thread start");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(this.index + " thread end");
    }
}

다음으로, Runnable 인터페이스 implements

public class MyRunnable implements Runnable {

    private int index;
    public MyRunnable(int index) {
        this.index = index;
    }

    @Override
    public void run() {
        System.out.println(this.index + " thread start");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(this.index + " thread end");
    }
}

실행

public class MyThreadMain {
    public static void main(String[] args) throws  {
          // thread
        for (int i = 0; i < 10; i++) {
            Thread myThread = new MyThread(i);
            myThread.start();
        }

          // runnable
        for (int i = 10; i < 20; i++) {
            Runnable runnable = new MyRunnable(i);
            Thread runnableThread = new Thread(runnable);
            runnableThread.start();
        }
    }
}

이외에도 단순한 Thread의 처리라면 익명클래스나 람다를 이용하는 방법도 있습니다.
Java 8버전 이상을 사용하신다면 익명클래스 보단 람다 사용을 권장합니다!!

new Thread() {
    @Override
    public void run() {
        System.out.println("anonymous class thread");
    }
}.start();

new Thread(() -> System.out.println("lambda thread")).start();

 


start vs run

우리는 run() 메서드를 재정의하였지만, 정작 thread 사용시에는 start() 메서드를 이용하였습니다.
그 이유는 jvm의 메모리 구조때문인데요, 아래의 사진을 잠시 살펴보도록 하겠습니다.

jvm은 ‘data, code, stack, heap’ 영역으로 구성되어 있습니다.
여기서 프로세스 내부의 thread들은 data, code, heap 영역을 공유하게 됩니다.

다만, run() 메서드는 메인쓰레드의 call-stack을 공유하여 작업을 하지만
start() 메서드는 각 쓰레드마다 call-stack을 새로 만들어 작업을 합니다.

run() 메서드는 thread가 순번을 기다리며 대기하고 있기에 하드웨어의 성능을 적절히 사용할 수 없습니다.
따라서 쓰레드의 작업을 적적히 분배하기 위해서는 start() 메서드를 이용해야 합니다


출처

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Spring CORS 설정하기

이번 포스팅에서는 CORS에 대해서 간단한 개념을 짚어보고 Spring을 통해서 CORS 정책 설정 방법을 알아보도록 하겠습니다
자세한 개념은 아래 페이지에 정리가 잘 되어 있으니 프론트/백엔드 상관없이 웹개발자분들은 한번씩 읽어 보시는 것을 추천드립니다 🤗
교차 출처 리소스 공유 (CORS) - HTTP | MDN


CORS란?

먼저, 아래 사진의 에러 내용을 살펴보도록 하겠습니다.

http://localhost:3000' -> ‘http:localhost:8080/todo’를 호출하였더니 위와 같은 메세지가 출력되었습니다. 빨간 네모박스 안에 메세지를 보면, CORS 정책에 의해 접근이 막혔다는 메시지가 나와 있습니다. CORS란 무엇일까요?

 

CORS란 ‘Cross-Origin Resource Sharing’의 약자로 ‘교차 출처 리소스 공유’로 번역됩니다. 웹브라우저 에서 다른 출처의 자원을 공유하는 방법입니다.

 


Origin 이란?

Origin은 출처라는 의미로 번역되는데 일반적으로 '동일/교차' 출처라는 의미로 사용됩니다.
여기서 말하는 Origin은 아래와 같이 표현됩니다
https://yhame.tistory.com

  • 프로토콜 : https
  • 호스트 : yhmane.tistory.com
  • 포트 : 443 (80, 443 포트는 생략이 가능합니다 😀)

즉, 여기서 말하는 Origin은 우리가 흔히 얘기하는 URL 구조에서 '프토토콜 + 호스트 + 포트'를 합친것과 같습니다

Same Origin / Cross Origin

위에서 알아본 동일/교차 출처에 대해서 url과 비교하여 동일출처인지 다른출처인지 비교해보도록 하겠습니다. 기준 url은 아래와 같습니다

# 기준 url
https://yhmane.tistory.com
URL 출처 내용
https://yhmane.tistory.com/category 동일출처  protocol, host, port 동일
https://yhmane.tistory.com/198?category=769262 동일출처 protocol, host, port 동일
http://yhmane.tistory.com/category 다른출처 protocal이 다름
https://yhmane.tistory.com:8081/category 다른출처 port가 다름
https://github.com/yhmane 다른출처 host가 다름

※ 동일출처의 경우 SOP (Same-Origin Policy)라 웹브라우저 상에서 보안상 이슈가 없지만, 교차출처의 경우 보안상 이슈로 인해 웹브라우저상에서 에러메시지가 처리됩니다. 결국, 이러한 문제를 해결하기 위해서는 서버(Spring, Node 등등)에서 CORS를 처리해주어야 합니다.


Spring CORS 설정하기

Spring에서는 크게 2가지 방식으로 CORS를 설정할 수 있습니다.

  1. @Configuration 설정 방법
  2. @CrossOrigin 설정 방법

일반적으로는 1번 @Configuration 설정 방법을 많이 사용합니다

@Configuration 이용 방법

먼저, @Configuration을 이용하여 전역으로 처리하는 방법을 알아보겠습니다.

* Java

@Configuration
public class CorsConfig implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
			.allowedOrigins("http://127.0.0.1:8080")
			.allowedMethods(
				HttpMethod.GET.name,
				HttpMethod.POST.name,
				HttpMethod.PUT.name,
				HttpMethod.DELETE.name
			)
	}
}

* Kotlin

@Configuration
class CorsConfig : WebMvcConfigurer {

	override
	fun addCorsMappings(registry: CorsRegistry) {
		registry.addMapping("/**")
			.allowedOrigins("http://localhost:3000")
			.allowedMethods(
				HttpMethod.GET.name,
				HttpMethod.POST.name,
				HttpMethod.PUT.name,
				HttpMethod.DELETE.name
			) 
	} 
}
  • addMapping
    CORS 적용할 Url 패턴을 정의합니다. 모든 url에 대한 접근을 허용할 경우 위와 같이 ‘/**’로 설정합니다.
  • allowedOrigins
    위에서 Origin은 (Protocol + host + port)의 조합이라고 들었습니다. 따라서, 허용하고자 하는 Origin을 적어주시면 됩니다. 마찬가지로 chaining도 가능합니다
  • allowedMethods
    허용하고자 하는 method를 적어주시면 됩니다.

@CrossOrigin 이용 방법

어노테이션 이용방법은 조금 더 간단합니다.
해당 클래스나 메서드위에 @CrossOrigin 어노테이션을 붙여주면 됩니다.

* Java

@CrossOrigin("http://localhost:3000")
@RestController
@RequestMapping("/todo")
public class TestController {
	
	//@CrossOrigin("http://localhost:3000")
	@GetMapping public String test() {
		// test ..
	}
}

* Kotlin

@CrossOrigin("http://localhost:3000")
@RestController
@RequestMapping("/todo")
class TestController {

	//@CrossOrigin("http://localhost:3000")
	@GetMapping fun test() {
		// test ..
	}
}

정리

  • 웹브라우저를 통해 동일출처가 아닌 교차출처를 이용해야 할 경우 CORS 설정을 해야 합니다
  • Origin은 (프로토콜 + 도메인 + 포트)로 구성됩니다
  • Spring에서 CORS 설정은 전역, @CrossOrigin을 이용해서 설정합니다
  • 이외에도 필터를 이용해 CORS 설정을 해줄수도 있습니다

출처

교차 출처 리소스 공유 (CORS) - HTTP | MDN
Cross Origin Resource Sharing - CORS - 뒤태지존의 끄적거림
baeldung

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

요즘 코드를 짜는 시간보다 업무를 분석하는 시간이 많아졌다
정책은 뭐 이리 많은지 ㅠㅠ ... 히스토리가 끊긴 프로젝트를 이어 받아서 하는중인데
나도 모르게 한숨이 많아 졌나 보다. 주변에서 괜찮냐고 물어보는데 허허
알게 모르게 스트레스를 만땅으로 받고 있는거 같다

남는 시간에 공부를 했었는데, 최근엔 스트레스 해소를 위해 새로운 취미를 시작해볼까 한다
개발 공부도 중요하지만 업무로 인한 스트레스를 잘 날려버릴 수 있도록
대체제를 잘 찾아봐야 겠다


비공개로 글을 썼었는데, 한달만에 새로운 취미를 찾아서 기쁜 마음에 다시 글을 이어서 쓴다!!
내가 찾은 새로운 취미는 볼링이다!!!
이제 100정도 치는거 같다 ㅋ.ㅋ
그래서 그런지 출퇴근길 볼링 유튜브 보는게 요즘 낙인거 같다 ㅎㅎ

올해안에 에버레이지 150 만드는게 목표다!!!

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

인덱스 생성

ElasticSearch는 REST를 지원하기 때문에 여러가지 방법을 이용하여 인덱스를 다룰 수 있습니다.
크게 다루는 방법은 3가지입니다

  • CURL
  • kibana
  • 언어의 라이브러르 이용

여기서는 kibana 쿼리를 이용하여 인덱스를 생성해 보도록 하겠습니다

PUT test_index
{
  "mappings": {
   "_doc": {
      "properties": {
        "testId": {
          "type": "long"
        },
        "testName": {
          "type": "keyword"
        }
      }
    }
  },
  "settings": {
    "index": {
      "refresh_interval": "1s",
      "number_of_shards": "1",
      "number_of_replicas": "1"
    }
  }
}

인덱스의 정보를 위와 같이 정의하였습니다. 사용될 문서의 정보와 샤드 레플리카 정보들로 간단히 설정하였습니다.


Document 생성

마찬가지로 인덱스와 같이 여러 방식을 이용할 수 있습니다.
여기서는 kibana 쿼리를 이용하였습니다

PUT test_index/_doc/1
{
  "testId" : 1,
  "testName" : "yunho"
}

참조

https://esbook.kimjmin.net/04-data/4.2-crud

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

엘라스틱서치를 클러스터로 운영하게 되면 다음과 같이 몇가지 문제를 맞이 할 수 있습니다

  1. 디스크 문제
  2. 메모리 문제
  3. 샤드 할당 문제
  4. 기타 등등

클러스터의 경우 마스터노드와 데이터노드 코디네이터 노드를 여러대 운영하기 때문에 노드의 추가와 제거가 가능합니다
다만, 간단한 작업의 경우 서버 재시작보다는 rolling restart 방법을 고려해 보는게 좋습니다.


문제상황 (예시)

  • 여기선, 단순히 heap 메모리 증설의 경우를 들어 설명하겠습니다.
  • 데이터 노드의 heap 메모리 부족
  • 먼저, 물리 메모리가 확보 됐는지 확인하고, 가능하다면 heap 메모리를 늘려주자

해결방안

메모리 증설은 해당 노드만 종료후 재실행시켜주면 되기에
엘라스틱서치 전체 재시작보다는 rolling-restart 방식을 취하는 것이 좋습니다

쿼리는 kibana 기준으로 작성 되어 있습니다

  • Step1 heap 메모리 재할당
    # path /elasticsearch/data/config/jvm.options
    Xms4g
    Xmx4g

샤드는 프라이머리, 레플리카로 구성됩니다.
노드가 종료되면 샤드 재배치라는 고비용 작업이 이루어지게
rolling-restart 작업을 할 시에는 샤드 재할당 기능을 종료시켜 놓습니다

  • Step2 샤드 할당 비활성

    # kibana 쿼리 / ES 6.4.2 # 노드를 중단했을때 샤드들이 재배치 되지 않도록 설정 
    PUT _cluster/settings { 
      "persistent": {     
          "cluster.routing.allocation.enable": "none" 
      } 
    }
  • Step3 세그먼트 동기화

    # kibana 쿼리 / ES 6.4.2 # primary - replica 간의 세그먼트 저장 상태를 동기화 
    POST _flush/synced
  • Step4 ES 재실행
    재실행시 elasticsearch의 권한을 잘 확인해보는 것이 좋습니다!!

  • Step5 샤드 할당 활성

    # kibana 쿼리 / ES 6.4.2 # unassigned 된 샤드 재배치 
    PUT _cluster/settings 
    { 
      "persistent": { 
          "cluster.routing.allocation.enable": null 
      } 
    }
  • step6 헬스 체크

    GET _cat/health

참조

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

14장. 일관성 있는 협력


객체지향 패러다임의 장점은 설계를 재사용할 수 있다는 것이다.
하지만 재사용은 꽁짜로 얻어지지 않는다.
재사용을 위해서는 객체들의 협력 방식을 일관성 있게 만들어야 한다

아래 예제를 통해 일관성 있는 협력 패턴이, 이해하기 쉽고 직관적이라는 것을 알아보자


01. 핸드폰 과금 시스템 변경하기

기본 정책 확장

11장에서 구현한 핸드폰 과금 시스템의 요금 정책을 아래와 같이 수정해보자

  • 고정요금 방식
    • ex) 10초당 18원
  • 시간대별 방식
    • ex) 00 ~ 19시 10초당 18원
    • ex) 19 ~ 24시 10초당 15원
  • 요일별 방식
    • ex) 평일 10초당 38원
    • ex) 공휴일 10초당 19원
  • 구간별 방식
    • ex) 초기 1분 10초당 50원
    • ex) 1분 이후 10초당 20원

조합 가능한 요금 계산 순서


위의 사진처럼 무수히 많은 조합이 나오기에 설계가 중요해졌다.

클래스 구조

고정요금 방식 구현하기

가장 간단한 고정요금이다. 일반요금제와 동일하다.

public class FixedFeePolicy extends BasicRatePolicy {
    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

시간대별 방식 구현하기

시간대별 방식에 따라 요금을 계산하기 위해서는 통화 기간을 정해진 시간대별로 나눈후 시간대별로 서로 다른 계산 규칙을 적용해야 한다.
시간대별 방식의 통화 요금일 계산하기 위해서는 통화의 시작, 종료시간, 시작일자, 종료일자도 함께 고려되어야 한다.

시간대별 통화 시간을 관리하는 클래스 DateTimeInterval

public class DateTimeInterval {
    public static DateTimeInterval of(LocalDateTime from, LocalDateTime to) {
        return new DateTimeInterval(from, to);
    }

    public static DateTimeInterval toMidnight(LocalDateTime from) {
        return new DateTimeInterval(from, LocalDateTime.of(from.toLocalDate(), LocalTime.of(23, 59, 59, 999_999_999)));
    }

    public static DateTimeInterval fromMidnight(LocalDateTime to) {
        return new DateTimeInterval(LocalDateTime.of(to.toLocalDate(), LocalTime.of(0, 0)), to);
    }

    public static DateTimeInterval during(LocalDate date) {
        return new DateTimeInterval(
                LocalDateTime.of(date, LocalTime.of(0, 0)),
                LocalDateTime.of(date, LocalTime.of(23, 59, 59, 999_999_999)));
    }

    private DateTimeInterval(LocalDateTime from, LocalDateTime to) {
        this.from = from;
        this.to = to;
    }

    public Duration duration() {
        return Duration.between(from, to);
    }

    public LocalDateTime getFrom() {
        return from;
    }

    public LocalDateTime getTo() {
        return to;
    }

    public List<DateTimeInterval> splitByDay() {
        if (days() > 0) {
            return split(days());
        }
        return Arrays.asList(this);
    }

    private long days() {
        return Duration.between(from.toLocalDate().atStartOfDay(), to.toLocalDate().atStartOfDay()).toDays();
    }

    private List<DateTimeInterval> split(long days) {
        List<DateTimeInterval> result = new ArrayList<>();
        addFirstDay(result);
        addMiddleDays(result, days);
        addLastDay(result);
        return result;
    }

    private void addFirstDay(List<DateTimeInterval> result) {
        result.add(DateTimeInterval.toMidnight(from));
    }

    private void addMiddleDays(List<DateTimeInterval> result, long days) {
        for(int loop=1; loop < days; loop++) {
result.add(DateTimeInterval.during(from.toLocalDate().plusDays(loop)));
        }
    }

    private void addLastDay(List<DateTimeInterval> result) {
        result.add(DateTimeInterval.fromMidnight(to));
    }

    public String toString() {
        return "[ " + from + " - " + to + " ]";
    }
}

요일별 방식 구현하기

요일별 방식은 요일별로 요금 규칙을 다르세 설정할 수 있다.
각 규칙은 요일의 목록, 단위 시간, 단위 요금이라는 3가지 요소로 구성된다.

public class DayOfWeekDiscountRule {
    public Money calculate(DateTimeInterval interval) {
        if (dayOfWeeks.contains(interval.getFrom().getDayOfWeek())) {
            return amount.times(interval.duration().getSeconds() / duration.getSeconds());
        }
        return Money.ZERO;
    }
}

요일별 방식 역시 통화 기간이 여러 날에 걸쳐 있을 수 있다.
시간대별 방식과 동일하게 통화 기간을 날짜 경계로 분리하고 각 통화 기간을 요일별로 설정된 요금 정책에 따라 적절하게 계산해야 한다.

public class DayOfWeekDiscountPolicy extends BasicRatePolicy {
    @Override
    protected Money calculateCallFee(Call call) {
        Money result = Money.ZERO;
        for(DateTimeInterval interval : call.getInterval().splitByDay()) {
            for(DayOfWeekDiscountRule rule: rules) { result.plus(rule.calculate(interval));
            }
        }
        return result;
    }
}

잠시 지금까지 구현한 고정요금, 시간대별, 요일별 방식의 클래스를 다시 살펴보자.
겉으로 보기에는 문제가 없이 잘 구현된 것 같아 보인다. FixedFeePolicy, TimeOfDayDiscountPolicy, DayOfWeekDiscountPolicy
세 클래스는 통화 요금을 정확하게 계산하고 있고 응집도와 결합도 측면에서도 특별히 문제는 없어 보인다.
그러나 이 클래스들을 함께 모아놓고 보면 문제점이 보인다

문제는 이 클래스들이 유사한 문제를 해결하고 있음에도 설계의 일관성이 없다는 것이다


02. 설계에 일관성 부여하기

설계에 일관성을 부여하기 위해서는 다음과 같은 연습이 필요하다

  • 다양한 설계 경험 익히기
  • 디자인 패턴을 학습하고 변경이라는 문맥안에 적용해보기
  • 협력을 일관성 있게 만들기 위해 다음과 같은 기본 지침을 따르기
    • 변하는 개념을 변하지 않는 개념으로부터 분리
    • 변하는 개념을 캡슐화

조건 로직 대 객체 탐색

먼저, 조건 로직에 대해서 코드를 통해서 살펴보자.
4장에서 나왔던 ReservationAgency 예제이다.

public class ReservationAgency {
    public Reservation reserve(...) {
        for (DiscountCondition condition : movie,getDiscountConditions()) {
            if (condition.getType() == DiscountCondtionType.PERIDOD) {
                // 기간조건
            } else {
                // 회차조건
            }
        }

        if (discountable) {
            switch(movie.getMovieType)) {
                case AMOUNT_DISCOUNT: // 금액 할인 정책
                case PERCENT_DISCOUNT: // 비율 할인 정책
                case NONE_DISCOUNT: // 할인 정책 없음
            }
        } else {
            // 할인 정책이 불가능한 경우
        }
    }
}

위와 같이 조건 로직으로 구현한다면 변경에 취약하고 유지보수의 어려움성이 생긴다.
조건로직이란 위와 같이 'if ~ else' 를 통해 비즈니스 로직에 덕지덕지 붙여 놓은 것이다.

우리는 추상화와 다형성이라는 것을 배웠기에 인터페이스를 이용해 객체 탐색으로 변경할 수 있다.

public class Movie {
    private DiscountPolicy discountPolicy;
    public Money calculteMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

이처럼 다형성은 조건 로직을 객체 사이의 이동으로 바꾸어 준다.
따라서, 할인 조건에 맞는 메시지만 주고 받으면 되기 때문에 변경이 용이해진다


03. 일관성 있는 기본 정책 구현하기

변경 분리하기

일관성 있는 협력을 만들기 위한 첫 단계는 변하는 개념과 변치 않는 개념을 분리하는 것이다
앞에서 본 핸드폰 과금 시스템의 기본 정책에서 변하는 부분과 변하지 않는 부분을 다시 살펴보자


단위요금이 시간당 요금을 계산하는 반면, 적용조건은 형식이 다르다른 것을 알 수 있다.
이것으로 적용조건은 변하는 부분이고, 단위요금은 변치 않는 다는 것으로 분리할 수 있다는 것을 유추할 수 있다.

변경 캡슐화하기

협력을 일관성 있게 만들기 위해서는 변경을 캡슐화해서 파급효과를 줄여야 한다.
여기서 변하지 않는 것은 ‘규칙’이다. 변하는 것은 ‘적용조건’이다.
따라서 규칙으로부터 적용조건을 분리하여 추상화한 후 시간대별, 요일별, 구간별 방식을 이 추상화의 서브타입으로 만들어야 한다.
그 후에 규칙이 적용조건을 표현하는 추상화를 합성관계로 연결하는 것이 객체의 캡슐화이다.

추상화 수준에서 협력 패턴 구현하기

먼저, ‘적용조건’을 표현하는 추상화인 FeeCondtion

public interface FeeCondition {
    List<DateTimeInterval> findTimeIntervals(Call call);
}

다음으로 FeeFule, 단위요금과 적용조건을 인스턴스변수로 사용한다

public class FeeRule {
    private FeeCondition feeCondition;
    private FeePerDuration feePerDuration;

    public Money calculateFee(Call call) {
        return feeCondition.findTimeIntervals(call)
                .stream()
                .map(each -> feePerDuration.calculate(each))
                .reduce(Money.ZERO, (first, second) -> first.plus(second));
    }
}

FeePerDuartion클래스는 단위 시간당 요금이라는 개념을 표현한다.
또한, 이정보를 이용해 일정기간의 요금을 계산하는 calculate 메서드를 구현한다

public class FeePerDuration {
    private Money fee;
    private Duration duration;

    public Money calculate(DateTimeInterval interval) {
        return fee.times(Math.ceil((double)interval.duration().toNanos() / duration.toNanos()));
    }
}

구체적인 협력 구현하기

이제 맨 처음 앞서 구현한 구체적인 협력 클래스를 인터페이스를 이용해 구현해보자
TimeOfDayFeeCondition 시간대별 정책

public class TimeOfDayFeeCondition implements FeeCondition {
    private LocalTime from;
    private LocalTime to;

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        return call.getInterval().splitByDay()
                .stream()
                .filter(each -> from(each).isBefore(to(each)))
                .map(each -> DateTimeInterval.of(LocalDateTime.of(each.getFrom().toLocalDate(), from(each)),
              LocalDateTime.of(each.getTo().toLocalDate(), to(each))))
                .collect(Collectors.toList());
    }

    private LocalTime from(DateTimeInterval interval) {
        return interval.getFrom().toLocalTime().isBefore(from) ?
                from : interval.getFrom().toLocalTime();
    }

    private LocalTime to(DateTimeInterval interval) {
        return interval.getTo().toLocalTime().isAfter(to) ?
                to : interval.getTo().toLocalTime();
    }
}

DayOfWeekFeeCondition, 요일별정책

public class DayOfWeekFeeCondition implements FeeCondition {
    private List<DayOfWeek> dayOfWeeks = new ArrayList<>();

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        return call.getInterval()
                .splitByDay()
                .stream()
                .filter(each ->
 dayOfWeeks.contains(each.getFrom().getDayOfWeek()))
                .collect(Collectors.toList());
    }
}

DurationFeeCondition 구간별정책

public class DurationFeeCondition implements FeeCondition {
    private Duration from;
    private Duration to;

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        if (call.getInterval().duration().compareTo(from) < 0) {
            return Collections.emptyList();
        }

        return Arrays.asList(DateTimeInterval.of(
                call.getInterval().getFrom().plus(from),
                call.getInterval().duration().compareTo(to) > 0 ?
                       call.getInterval().getFrom().plus(to) :
                        call.getInterval().getTo()));
    }
}

맨처음 짯던 정책들과 비교를 해보자. 각 클래스마다 일관성이 생기게 되었다.
우리는 변하는 개념과 변치 않는 개념을 분리해서 변경을 캡슐화 하였다.
이처럼 변경을 캡슐화해 협력을 일관성 있게 만들면 어떤 장점을 얻을 수 있는지 명확하게 보여준다


마찬가지로, 추가적인 정책이 필요하다면 FeeCondition으로부터 구현을 해주면 된다.

일관성 있는 협력의 핵심은 변경을 분리하고 캡슐화하는 것이다.
변경을 캡슐화 하는 방법이 협력에 참여하는 객체들의 역할과 책임을 결정하고 이렇게 결정된 협력이 코드의 구조를 결정한다.
따라서, 변경의 방향을 파악할 수 있는 감각을 기르는 것이 중요하다.


참조

  • 조영호님의 오브젝트 '일관성 있는 협력'

'Books > Object' 카테고리의 다른 글

[Object] 일관성 있는 협력  (0) 2021.08.16
[Object] 다형성  (0) 2021.08.12
[OBJECT] 합성과 유연한 설계  (2) 2021.08.08
[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Controlling Step Flow


들어가며

이전에 우리는 Job이 n개의 Step으로 구성되어 있다는 것을 확인하였습니다.
Step은 일반적으로 순차적인 흐름으로 진행되지만, 조건에 따라 흐름을 제어할 수도 있습니다.
이번 포스팅에선 Step의 흐름 제어에 대해 알아보도록 하겠습니다.


Sequential Flow

일반적인 Step의 Flow입니다

@Bean
public Job job() {
    return this.jobBuilderFactory.get("job")
                .start(stepA())
                .next(stepB())
                .next(stepC())
                .build();
}
  • start() 메서드를 이용해 첫번째 Step을 호출합니다
  • next() 메서드를 이용해 다음 Step을 호출합니다
  • 우리는 Step이 A -> B -> C 순서로 진행되는 것을 쉽게 알 수 있습니다.

Conditional Flow

위 예제에서는 두가지 시나리오만 가능합니다

  1. Step이 성공하고 다음 Step이 실행됩니다
  2. Step이 실패하고, job이 실패됩니다

일반적으론 위의 flow로도 충분합니다.
하지만, Step이 실패할경우 job을 실패로 끝내기 보다
다른 Step을 호출한다면 어떻게 될까요???
아래의 Step flow를 확인 해보겠습니다

@Bean
public Job job() {
    return this.jobBuilderFactory.get("job")
                .start(stepA())
                    .on("*")
                    .to(stepB())
                .from(stepA())
                    .on("FAILED")
                    .to(stepC())
                .end()
                .build();
}
  • Step A가 성공할 경우 StepB가 실행됩니다.
  • Step A가 실패할 경우 StepC가 실행됩니다.

메서드가 직관적이기 때문에 무엇을 의미하는지 알 수 있습니다.
좀더 정확한 의미를 알기 위해 위의 코드를 잠시 살펴보겠습니다.

  • start()
    • Job의 Step 구성으로부터 시작할 Step을 정의
  • on()
    • ExitStatus를 Catch
    • '*'는 모든 ExitStatus를 정의
    • Status는 BatchStatus가 아닌 ExitStatus가 온다!!!
  • to()
    • 다음으로 이동할 Step을 정의
  • from()
    • Step의 상태값을 보고 일치할경우 캐치
  • end()
    • FlowBuilder를 종료
      이렇듯, 약간의 코드 추가로 Step의 실패가 Job의 실패로 유도되지 않도록 설계할 수 있습니다.

ExitStatus 설정!!!

Job Flow를 설정하면서 가장 많은 실수를 하는 부분이 이 부분이 아닐까 합니다!!

return stepBuilderFactory.get("stepA")
    .tasklet((contribution, chunkContext) -> {
        //////////////////////////////////////////////
        contribution.setExitStatus(ExitStatus.FAILED);
        //////////////////////////////////////////////
        return RepeatStatus.FINISHED;
    })

on() 메서드가 캐치하는 상태값은 Batch Status가 아닌 Exit Status입니다.
따라서 Step의 Flow를 컨트롤할 경우 setExitStatus() 메서드를 통해 상태값을 정의해야 합니다!!

public void setExitStatus(ExitStatus status) {
    this.exitStatus = status;
}

참조

jojoldu님 블로그 - 4. Spring Batch 가이드 - Spring Batch Job Flow
spring.batch.io - Configuring a Step

#Spring/Batch/Step-flow

블로그 이미지

사용자 yhmane

댓글을 달아 주세요