티스토리 뷰

개발/JAVA

[JAVA] Thread Safety

애쿠 2023. 1. 21. 23:37

서버에서 감당할 수 있는 양보다 많은 요청이 들어온다면, 어떻게 될까?

 

프론트엔드에서는 앞선 요청이 모두 처리될 때까지 무한 대기 상태에 있거나, 심한 경우 서버가 죽는 경우가 발생할 것이다. 정상적으로 처리되어 대기상태가 빠르게 해결되면 다행이지만, 프론트엔드 단에서 요청할때 걸려있는 시간 내에 응답이 나가지 않으면, time out으로 에러 처리를 해버릴 수도 있다.

 

그렇게 된다면 필요한 데이터가 소실 될 수도 있고, 서비스 로직 단에서 중단되버려 데이터가 꼬여 다음 요청을 정상적으로 처리하지 못할 수도 있다.

 

그렇다면 서버에서는 프론트엔드에 "지금 과도하게 요청이 몰려 있어"를 알려줄 필요가 있다.

 

빌드업이 길었는데, 요약하면 서버에서 과부하 제어를 하기 위해 요청이 과하게 몰렸을 때 응답코드를 내려 줄 일이 생겼다. 과부하 제어에는 다양한 방법이 있는데, 괜찮은 포스팅이 있어 첨부한다.

 

https://d2.naver.com/helloworld/6070967

 

우선 개발 방식은 WAS 한대에 과도한 요청이 동시에 들어 왔을 때, 현재 서버는 과부하 상태입니다! 라고 알려주는 에러 코드를 내리는 방식으로 결정했다.

 

여기서 고려할 점과 개인적으로 공부하고 싶은 부분들이 나왔는데,

 

어느 부분에서 처리할 것인가?

  1. WAS로 들어오는 모든 request를 카운팅하고 카운팅한 횟수를 기억하고 있어야 하기 때문에, 서비스 로직 진입 이전에서 처리해줘야한다.
  2. filter, Interceptor, AOP 등 Spring과 WAS에서 제공하는 기능들을 이용해야 한다.
  3. 괜히 일 벌리는게 아닌가 싶지만 Spring의 요청처리 과정도 추가 포스팅할 예정이다.

Tomcat은 요청들을 멀티 쓰레드로 처리한다. 그렇다면 요청 카운팅을 어떻게 저장할 것인가?

  1. 일반 서비스 로직에서 사용하는 변수를 Thread에서 사용하게되면, Thread 간의 변수 값이 공유되지 않아 과부하 상태를 정상적으로 체크하지 못할 수 있다.
  2. 때문에 Java에서 제공하는 Thread Safety하게 구현하는 방법이 있다. 이 내용이 본 포스팅의 주제다.

1. 멀티쓰레드 사용시 발생하는 문제

멀티쓰레드는 언제나 동시성(Concurrent) 이슈가 있다. 서로 다른 프로세스 안에서 병렬적(Parallel)으로 동작하는게 아닌 하나의 프로세스 안의 메모리 영역을 모든 쓰레드가 공유한다. 때문에, 별도의 코드를 사용하지 않을 경우 자바에선 멀티 쓰레드 사용 시 데이터의 정합성과 원자성을 보장하지 않는다.

Thread A : 11
Thread B : 11
Thread A : 12
Thread B : 12
Thread A : 13
Thread B : 13

A 다음 B 를 실행시켰지만 위와 같이 꼬이는 경우가 가장 대표적인 케이스이다.

2. 어떻게 해결해야 하나?

자바에서도 이 문제를 명확히 알고 있어서 정말 다양한 기능을 제공한다.

  1. syncronized
  2. volatile
  3. AtomicInteger

한번이라도 사용해 본 적 있는 것들로만 정리했지만, java.util.concurrent 클래스를 뒤져보면 다양한 Lock 기능과 더불어 동시성 제어에 많은 신경을 쓰고 있음을 알 수 있다.

간단히 정리해보면

2-1. syncronized

    synchronized void sum(int n)
    {

        // Creating a thread instance
        Thread t = Thread.currentThread();
        for (int i = 1; i <= 5; i++) {
            System.out.println(
                    t.getName() + " : " + (n + i));
        }
    }

가장 일반적으로 알고 있을만한 멀티쓰레드 제어 방식이지만, 이 방식은 Pessimistic locking(비관적 락)으로, 선점한 쓰레드가 있다면 다른 무조건 차단이된다. 때문에 비용이 가장 비싼 방식으로 그닥 추천하고 싶지 않은 방식이다.

2-2. volatile

자바의 volatile 키워드는 변수를 '메인 메모리에 저장' 할 것을 명시하기 위해 쓰인다. volatile 키워드로 지정된 변수는 컴퓨터의 메인 메모리로부터 읽히고, volatile 변수에 대한 쓰기 작업은 메인 메모리로 직접 이루어진다.

 

JVM 멀티쓰레드는 메모리가 공유되는 상황에서 데이터를 "캐시 메모리"에서 가져오는 경우 동시성 이슈가 발생하는데, 변수를 "메인 메모리"에 저장하게 함으로써 차단한다.

 

그런데, volatile 키워드는 Thread1에서 write하고, Thread2에서 read 할때만 동시성을 보장한다. 두 개 이상의 쓰레드가 write 작업을 하는 경우 문제가 될 수 있다고 한다. (개인개발 할 때는 해당 이슈가 발생하지 않아서 몰랐는데.. 이번에 알게 되었다)

 

때문에 아래의 코드는 동시성을 보장하지 않는다.

public class Worker {

    private volatile int count = 0;
    private int limit = 100000;

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.doWork();
    }

    public void doWork() {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++) {

                    count = count + 1;

                }
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++) {

                    count = count + 1;

                }
            }
        });
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException ignored) {}
            System.out.println("Count is: " + count);
    }
}

2-3. AtomicInteger

volatile의 문제는 AtomicInteger를 사용함으로써 해결할 수 있다. CAS(Compare-And-Swap)을 사용해서 원자성을 보장하며, 비교적 저렴한 자원으로 동시성 이슈를 해결할 수 있다고 한다.

public class Worker {

    final AtomicInteger myCoolInt = new AtomicInteger(0);
    private int limit = 100000;

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.doWork();
    }

    public void doWork() {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++) {
                    myCoolInt.incrementAndGet();
                }
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++) {
                    myCoolInt.incrementAndGet();
                }
            }
        });
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException ignored) {}
            System.out.println("Count is: " + myCoolInt.get());
    }
}

동시성 이슈 해결을 위한 키워드들의 비교는


https://stackoverflow.com/questions/9749746/what-is-the-difference-between-atomic-volatile-synchronized

 

이 글에 잘 설명되어 있다.

3. 마치며

용두사미가 되버렸는데, 이렇게 길게 쓰려고 작성한 포스팅이 아니긴했다. 미들웨어의 멀티쓰레드 데이터의 원자성을 위해 AtomicInteger를 사용하게 된 계기부터 시작했는데, JVM의 메모리 구조까지 보게 될 줄은 몰랐다.(공부해야될게 또 늘었다) 정말 참고한 사이트가 많은데 몇 개만 남겨두겠다.

 

네이버 메인페이지의 트래픽 처리

geeksforgeeks의 thread saftey 정리 글

 

멀티프로세스와 멀티쓰레드

Java volatile 키워드

멀티-스레드의-동시성-이슈

Java [병렬프로그래밍] 멀티 스레드와 동기화

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

[JAVA] 함수형 인터페이스  (1) 2023.01.26
[JAVA] TemporalAdjusters  (0) 2023.01.26
[Java] ObjectMapper  (0) 2023.01.22
[JAVA] 난수 생성기 Random, SecureRandom  (0) 2022.12.27
[JAVA] HttpURLConnection, HttpClient, okHttp  (0) 2022.12.18
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함