본문 바로가기

TIL

동시성 문제: 여러 개의 스레드를 돌리면 반드시 발생하는 일

동시성(Concurrency)는 백엔드를 조금 더 깊게 구현하기 시작하면 직면하게 되는 문제이다. 이전 글에서 스레드에 대한 개념을 설명을 한 적이 있었다. 스레드는.. 일을 하는 일종의 단위이다. 이렇게 말하면 너무 추상적인데, 만약 부엌에서 음식을 만든다고 할 때, 스레드는 요리사로 생각하면 이해가 쉽겠다. 

기초적인 백엔드 서버 구현할 때에는 보통 스레드 하나(요리사 한 명)을 배치하여 작업을 하는 것이 일반적이다. 여러분도 입문하여 처음 프로젝트를 할 때, 대부분 스레드 1개짜리용 서버를 구현을 하였을 것이다.

하지만, 오늘날 서버는 동시에 막대한 양의 요청을 처리해야 한다. 특히 대규모 서비스의 경우 더더욱 그러하다. 요리사 한 명이 아무리 빠르게 음식을 만든다고 해도, 작업에는 한계가 있을 터.. 이럴 경우에는 요리사를 한 명 더 추가(스레드)를 하면 된다.

 

 

동시성(concurrency)와 병렬성(parallelism)은 사실 엄밀히 다르다

위의 내용을 통에서 "아 스레드를 여러 개로 작업하는 것은 병렬로 일을 동시에 하는 거구나"라고 이해를 하실 수 있다. 그러나, 엄밀히 말해 동시성과 병렬성은 다르긴.. 하다. 왜냐하면, 스레드를 여러 개를 만들어 놓는다고 해도, 실제 컴퓨터에서 물리적으론 여러 개가 동시에 작업하지 않을 수 있기 때문.

그러니까.. 우리의 cpu에는 코어가 있다. 아마 들어보셨을 것이다. 6코어라면 코어가 6개인 코어이고, 실제 물리적으로 6개의 작업을 각 코어에 할당하여 동시에 병렬적으로 처리할 수 있다. 만약 12개의 작업을 처리하기 위해 스레드 12개를 만들어 처리하도록 요청한다면? 그렇다면 각 코어가 2개의 스레드를 빠르게 바꿔가면서 처리를 하게 된다.

그렇다. 병렬성은 실제 물리적으로 여러 개가 처리된다는 것이며, 동시성은 논리적인 단위로 그 만큼 여러개가 동시에 처리된다고 설정해 놓은 것이다.

 

 

동시에 다루고 있는 대상이 만약 한 개라면?

동시성 문제는 바로 여기서 출발한다. 만약 두 개의 스레드가 index라는 하나의 변수를 다루는 작업을 각각 맡겼다고 가정해보자. 현재 index는 0이다. 각각 스레드가 idex의 현재 인덱스의 값을 참조하여 1씩 증가시킨다. 이렇게 될 경우, index의 값은 몇일까?

만약 동시성 문제를 정확히 처리하지 못하게 되면, index의 값은 정확이 무엇이 될 지 모르게 된다. 각 스레드 모두 0인 값을 참조한 다음 1로 증가시켜 버릴 경우, index는 1이 된다. 각 스레드가 미세한 시간 차로 참조시간이 다를 경우, 두 번째 스레드가 1인 값을 참조하여 2로 증가시켜 index의 값은 2가 될 수 있다. 왜냐하면 위에서 명시하였듯, 동시성은 엄밀히 말해 병렬적으로 같은 시각 같은 시간에 다발적으로 일어나지 않기 때문이다.

 

 

해결책

해결책은 당연히 있다. 그리고 여러가지이다. 이해 대해 짧게 요약하여 설명하고자 한다.

1. synchronized 이 키워드를 사용하면, 하나의 스레드만 실행하도록 강제한다. 아래 코드에서 increment()라는 메서드는 어느 한 스레드에서 참고하고 있을 경우, 다른 스레드에서 접근하지 못하게 되고, '참고하고 있는 스레드가 끝날 때 까지' wait, 즉 대기상태가 된다.

    private int index = 0;

    public synchronized void increment() {
        index = index + 1;
    }

 

2. Lock 으로 직접 잠그기, 혹은 아래의 코드와 같이 아얘 Lock을 사용하여 잠궈버리는 방법도 있다. 어느 한 스레드가 increment 메서드를 사용하면 다른 스레드가 접근하지 못하게 lock을 걸어버린다. 또한 그 한 스레드가 메서드를 완전히 실행한 수 lock을 unlock해야 잠금이 풀려 접근이 가능.

private Lock lock = new ReentrantLock();

public void increment() {
    lock.lock();         
    try {
        index = index + 1;  
    } finally {
        lock.unlock();   
    }
}

 

3. Atomic(원자적 변수) 사용, 아래의 예시로는 AtomicInteger가 되겠다. 굳이 Integer 타입 뿐만 아니라, Boolean, Long 등의 타입도 있다. 먼저 원자적이라는 개념에 대해 설명을 하자면, 우리가 index를 더할 때 실제로는 3단계의 작업으로 이루어진다는 것을 생각하면 된다.

index 읽기(읽어보니 지금은 2이군..) => index 더하기(좋아 2를 기준으로 1을 더해 3을 만들자) => index 저장하기(3을 저장하자) 

이러한 연속된 작업을 중에 다른 스레드가 끼어들지 못하도록 하여, 하나의 원자처럼 다루도록 하는 것이 원자성이다. 코드는 아래 예시가 있겠다..

    private AtomicInteger index = new AtomicInteger(0);

    public void increment() {
        index.incrementAndGet();  
    }