13장. 동시성

동시성이 필요한 이유

  • 구조적 개선 동시성을 사용하면 what과 when을 분리하여 구조적으로 decoupling이 가능하다.

  • throughput 개선 웹 크롤러: 단일 스레드 모델 대신 다중 스레드를 사용하면 더 빠르게 작업을 끝낼 수 있다. 대량 처리 시스템: 처리해야 하는 대량의 정보를 나누어 병렬로 처리

  • 응답시간 개선 사용자와 상호작용 해야하는 시스템이라면 단일 스레드 모델의 경우 이전 사용자의 작업이 끝날 때 까지 다음 사용자는 작업이 불가능하다.

주의점

  • 동시성이 항상 성능을 높여주지는 않는다. 여러 프로세서(CPU 코어) 가 동시에 처리할 수 있는 독립적인 작업이 충분히 많아야 한다.

  • 동시성을 구현하려면 설계가 변해야 한다.

  • 웹 프레임워크를 사용해도 동시성을 이해해야 한다. 동시성 원리를 모르고서는 리소스 동시 수정 제어(Lock), 데드락 등의 문제를 해결하기 힘들다.

  • 동시성은 부하를 유발한다. 동시성을 위한 코드도 필요하고 생성한 스레드간의 컨텍스트 스위칭도 고려해야 한다.

  • 동시성 버그는 재현하기 어렵다.

난관

public class ThreadUnsafeClass {
    private int lastUsedId = 42;

    public int getNextId() {
        return ++lastUsedId;
    }
}

스레드 두 개가 위의 getNextId()를 부르면 어떻게 될까? 결과는 셋 중 하나다.

스레드 1 → getNextId()
스레드 2 → getNextId()
lastUsedId
결과

케이스 1

= 43

= 44

= 44

정상

케이스 2

= 44

= 43

= 44

정상

케이스 3

= 43

= 43

= 43

오류

getNextId() 메서드가 한 줄짜리 메서드 처럼 보이지만 실제로 컴퓨터가 처리할 때는 여러 개의 바이트 코드 명령어로 나뉘어진다. 바이트 코드 명령을 실행하는 중간에 다른 스레드로 컨텍스트 스위치가 일어나는 경우 lastUsedId 값의 증가(++)가 아직 반영되지 않아서 세 번째 케이스와 같은 결과가 발생할 수 있다.

동시성 방어 원칙

단일 책임 원칙

SRP는 주어진 클래스를 변경할 이유가 하나여야 한다는 원칙

동시성은 복잡성 하나만으로 기존 클래스에서 분리해야 한다. 즉 동시성 코드는 다른 코드와 분리해야 한다.

자료 범위 제한

임계영역은 동기화 영역으로 보호해야 한다. 보호해야 할 임계영역이 많으면 실수의 가능성이 커지기 때문에 임계영역의 수를 줄이는것이 중요하다.

데이터 사본 활용

공유할 자료를 처음부터 만들지 않는것이 가장 좋다. 필요한 객체를 복사본 DTO 등으로 감싸서 읽기 전용으로 사용하는 방법도 있다.

독립적인 스레드 구현

스레드간에 자료 공유를 하지 않도록 구현한다. 필요한 정보는 공유되지 않는 출처에서 가져오고 로컬 변수에 저장한다. 예를 들어 자바 서블릿 코드는 마치 독자적인 하나의 스레드에서 실행되는 것처럼 구현된다.

Thread Safe 라이브러리 사용

java.util.concurrent 패키지를 활용한다. 예를 들어 다중 스레드 환경에서 Map을 사용해야 한다면 ConcurrentHashMap 구현체를 사용한다.

동기화 메서드간의 의존성을 이해하라

동기화 메서드간 의존성이 높으면 찾기 어려운 버그가 생길 가능성이 높다. 공유 클래스 하나에는 동기화 메서드 하나만 사용하는것을 권장한다.

동기화하는 부분을 작게 만들어라

동기화 영역은 한 번에 스레드 하나만 실행된다. 따라서 동기화 영역이 넓으면 다중 스레드의 장점을 살리기 어려워진다. 필요 이상으로 동기화 영역을 잡지 않았는지 확인한다.

올바른 종료 코드는 구현하기 어렵다

동시성 코드를 올바르게 종료하도록 만들기 쉽지 않다. 자식 스레드가 모두 종료되고 나서 부모 스레드가 종료되도록 하는 코드를 생각해 볼 수 있다. 예를 들어 자식 스레드가 Producer, Consumer 관계에 있는 경우 둘 중 한 스레드는 상대의 시그널을 기다리며 블락 되어 있을 수 있다. 이때 부모 스레드가 모든 자식 스레드에게 종료 명령을 내리면 한 스레드는 정상적으로 종료되었지만 다른 스레드는 블락되어있어 종료하지 못하는 데드락에 빠질 수 있다.

이렇듯 동시성 코드의 깔끔한 종료는 생각할 거리가 많으며 개발 초기부터 고민해야 한다.

스레드 코드 테스트하기

  • 문제를 노출하는 TC를 작성하라

  • 프로그램 설정과 부하를 바꾸어가며 자주 돌려라

  • 말이 안되는 실패는 잠정적인 스레드 문제로 간주하라

    • 멀티 스레드 코드는 수백만 번에 한 번 발생하는 아주 드문 버그를 만들 수 있다. 때때로 실패하는 TC는 사실 멀티 스레드 버그라고 간주하는것이 안전하다.

  • 멀티 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자

    • 스레드를 모르는 POJO부터 올바로 만들고 나서 멀티 스레드 코드를 테스트해라

  • 멀티 스레드 코드를 다양한 환경에 쉽게 끼워 넣을 수 있게 구현해라

    • 스레드 수를 바꾸어서 실행한다.

    • 스레드 코드를 개발계, 테스트계에서 돌려본다. 운영 환경과 동일한 곳에서도 돌려본다.

    • TC를 다양한 속도로 돌려본다.

    • 반복 테스트가 가능하도록 TC를 작성한다.

  • 상황에 맞게 스레드의 개수를 조절할 수 있도록 구현해라

  • 프로세서 수 보다 많은 스레드를 돌려보라

    • 스레드간의 스와핑이 잦으면 멀티 스레드 코드에 숨은 버그 (놓친 임계영역 또는 데드락)를 찾기 용이하다.

  • 다른 플랫폼에서 돌려보라

    • OS에 따라서 스레드 처리 정책이 다르기 때문에 원하는 결과가 나오지 않을 수 있다.

  • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라

    • 스레드 코드의 오류를 찾기 어려운 이유는 코드가 실행되는 수많은 경로 중 소수의 경우에 실패하기 때문이다.

    • 드물게 발생하는 오류를 좀 더 자주 일으키게 하기 위해서 보조 코드를 추가해 스레드 코드의 실행 순서를 바꾸는 방법이 있다.

    • Object.wait(), Object.sleep(), Object.yield(), Object.priority() 등과 같은 메서드를 코드 중간 중간 삽입해 코드 실행 흐름을 흔들어 다양한 순서로 실행될 수 있도록 한다.

    c.f. 코드에 보조 코드를 추가하기 위해서 직접 구현하는 방식과 AOF, CGLIB 등의 라이브러리를 사용해 자동화하는 방식이 있다.

Last updated