Java

Java에서의 동시성 제어

jwKim96 2022. 1. 11. 21:03

동시성 제어가 필요한 이유

서비스에 과도한 트래픽이 몰릴 경우, 서버가 예상한 대로 동작하지 않는 경우가 있습니다.

인프라에서 문제가 발생할 수도 있고, 코드에서 문제가 발생할 수도 있습니다.

인프라의 경우 AWS나 GCP같은 클라우드 서비스를 통하여 유동적으로 대처 가능하지만,

이미 서버에 배포된 코드는 유동적으로 대처하는것이 거의 불가능 하죠.

그래서 개발자는 항상 과도한 트래픽이 몰릴 경우를 미리 대비해야 합니다.

그럼, 아래에서 Java에서 동시성 제어를 위해 추가된 기능들에 대해 알아보겠습니다.

동시성 제어

public class Main {
    public static void main(String[] args) {
        CountInteger count = new CountInteger();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < 100; j++) {
                new Thread(() -> {
                    System.out.println(count.getAndIncrement());
                }).start();
            }
        }
    }
}

class CountInteger {
    private int count = 1;

    public int getAndIncrement() {
        return count++;
    }
}

위 코드는 동시성 제어 오류가 발생할 가능성이 높은 코드입니다.

CountInteger 클래스의 getAndIncrement() 메소드는 count를 반환한 뒤, 1씩 증가하는 로직입니다.

이를 10,000개의 스레드에서 동시에 사용한다고 해봅시다.

결과는 아래와 같이 나오게 됩니다.(실행 환경에 따라 차이가 있을 수 있습니다)

image

count는 1부터 시작하기 때문에 맨 마지막에 찍히는 숫자는 10000이어야 하지만, 다르게 나옵니다.

이 경우에는 10000개의 중 3개의 스레드는 정상적으로 동작하지 않았다는 것을 의미하는데요.

만약 이런 단순한 로직이 아니라, 예를들어 원서접수 시스템이었다면 10000명 중 3명의은 원서접수가 안됐다는 의미입니다.

synchronized

Java에는 이런 동시성 문제를 해결하기 위해서 synchronized라는 키워드를 제공합니다.

위의 CountInteger 클래스를 다음과 같이 개선해볼 수 있습니다.

class CountInteger {
    private int count = 1;

    public synchronized int getAndIncrement() {
        return count++;
    }
}

이렇게 메소드에 synchronized 키워드를 붙이게 되면, 한개의 스레드만 사용하게 됩니다.

image

이렇게 정상적으로 10000까지 출력된 것을 알 수 있습니다.

만약 메서드 전체가 아니라 특정 로직만 적용하고 싶다면, 해당로직을 블록(block)으로 지정하여 동시성 제어를 할 수도 있습니다.

class CountInteger {
    private Integer count = 1;

    public int getAndIncrement() {

        synchronized (count) {
            return count++;
        }
    }
}

여기서 변경된 점은, 먼저 count가 기본형 int에서 참조형 Integer타입으로 변경되었는데요.

synchronized 블록의 인자값으로는 참조형만 들어갈 수 있기 때문에, 현재 로직으로는 기본자료형을 사용할 수 없습니다

하지만 개발자들은 언제나 이런 보일러플레이트(자주 사용하는 로직)의 중복을 줄이려고 노력하죠.

java.util.concurrent.atomic 패키지

그래서 Java 1.5 버전 이상 부터는 java.util.concurrent 라는 동시성 제어 유틸리티 패키지를 제공합니다.

이 패키지의 하위에는 아래와 같은 패키지들이 있는데요.

  • java.util.concurrent : 동시성 제어 유틸 & 동시성 제어가 적용된 Collections 클래스들
  • java.util.concurrent.atomic : Boolean, Integer, Long 및 참조타입의 동시성 제어 클래스들
  • java.util.concurrent.locks : 읽기, 쓰기관련 동시성 제어 유틸

여기서는 java.util.concurrent.atomic의 기능을 사용해보겠습니다.

마찬가지로 아까 만들었던 CountInteger 클래스에 Integer 대신에 AtomicInteger를 사용해 보겠습니다.

class CountInteger {
    private AtomicInteger count = new AtomicInteger(1);

    public int getAndIncrement() {
        return count.getAndIncrement();
    }
}

이렇게 AtomicInteger 클래스를 사용하면, 동시성 제어도 처리된 로직을 간편하게 만들 수 있습니다.

참고하면 좋은 링크