해당 포스트는 Clean Code의 내용을 참고하여 작성되었습니다.
동시성이 필요한 이유
동시성은 결합을 없애는 전략이다. 즉, 무엇과 언제를 분리하는 전략이다. 무엇과 언제를 분리하면 어플리케이션 구조와 효율이 극적으로 나아진다. 구조적인 관점에서 프로그램은 거대한 루프가 아닌 작은 협력 프로그램 여럿으로 보인다.
서블릿은 웹 혹은 EJB 컨테이너에서 돌아가는데 이 컨테이너들은 동시성을 부분적으로 관리한다. 매 요청바다 웹 서버는 비동기식으로 서블릿을 실행하고 서블릿 프로그래머는 들어오는 모든 웹 요청을 관리할 필요가 없다.
이런 구조적 개선만을 위한 동시성 뿐만 아니라 시스템에 따라서 응답 시간과 작업 처리량 개선이라는 요구사항으로 인해 동시성을 구현해야 하는 경우도 있다. 이런 경우에는 병렬로 처리를 하는것이 성능의 향상을 가져온다.
동시성 바로잡기
다음은 동시성과 관련된 타당한 생각 몇가지이다.
- 동시성은 다소 부하를 유발한다
- 동시성은 복잡하다
- 일반적으로 동시성 버그는 재현하기 어렵다
- 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다
다음은 동시성과 관련된 일반적인 오해이다.
동시성은 항상 성능을 높여준다
동시성은 때로 성능을 높여준다. 대기 시간이아주 길어 여러 스레드가 프로세서를 공유할 수 있거나 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.
동시성을 구현해도 설계는 변하지 않는다
아니다. 싱글 스레드 시스템과 멀티 스레드 시스템은 설계가 판이하게 다르다.
웹 또는 EJB 컨테이너를 사용하면 동시성을 이애할 필요가 없다
실제로 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락과 같은 문제를 회피하는지 알아야만 한다.
동시성이 어려운 이유
두 스레드가 같은 변수를 동시에 참조하면 경우에 따라 잘못된 결과를 도출할 수 있다. 두 스레드가 코드 한줄을 거쳐가는 경우는 수없이 많은데 그 중 일부 경로가 잘못된 결과를 내놓기 때문이다. JIT 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소 단위를 고려했을 때 잠재적인 경로는 최대 12870개 이상이다. 이 중 잘못된 결과를 내놓는 것은 일부이다.
동시성 방어 원칙
SRP
동시성 관련 코드는 SRP에 의거해 다른 코드와 분리해야 한다. 동시성을 구현할 때는 다음 몇가지를 고려한다.
- 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
- 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 매우 어렵다.
- 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로 충분히 어렵다.
동시성 코드는 다른 코드와 분리하다
따름 정리 : 자료 범위를 제한하라
멀티 스레드의 문제를 예방하는 방법으로 critical section을 synchronized 키워드로 보호하라고 권장한다. 이런 critical section의 수를 줄이는 기술이 중요하다. 공유 자료를 수정하는 위치가 많을수록 다음 가능성도 커진다.
- 보호할 critical section을 빼먹는다.
- 모든 임계영역을 올바로 보호했는지(DRY 위반) 확인하느라 같은 노력과 수고를 반복한다.
- 안그래도 찾기 힘든 버그가 더 찾기 힘들어진다.
자료를 캡슐화하라. 공유 자료를 최대한 줄여라.
따름 정리 : 자료 사본을 사용하라
공유 자료를 줄이는 방법중에는 특정 경우에 객체 사본을복사해 읽기 전용으로 만드는 방법도 있다. 어떤 경우에는 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 존재한다. 공유 객체를 피하는 방법이 있다면 코드가 문제를 일으킬 가능성도 아주 낮아진다.
단 이 경우는 객체를 복사하는 비용과 부하와 동기화를 위한 내부 잠금에 따르는 비용과 부하를 검증하는 과정을 거쳐야 한다.
따름 정리 : 스레드는 가능한 독립적으로 구현하라
다른 스레드와 자료를 공유하지 않는 스레드를 구현하라. 각 스레드는 클라이언트 요청 하나를 처리한다. 모든 정보는 비공유 자료에서 가져오며 로컬 변수에 저장한다. 그러면 각 스레드는 세상에 자신만 있는 듯이 돌아갈 수 있다. 다른 스레드와 동기화할 필요가 없기 때문이다.
독자적인 스레드로 가능하면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.
라이브러리를 이해하라
자바로 스레드 코드를 구현한다면 다음을 고려해야 한다.
- 스레드 환경에 안전한 컬렉션을 사용한다.
- 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.
- 가능하다면 스레드가 blocking 되지 않는 방법을 사용한다.
- 일부 클래스 라이브러리는 스레드에 안전하지 못하다.
자바가 제공하는 동시성 설계를 위한 클래스는 다음과 같은 것이 있다.
- ReentrantLock : 한 메소드에서 잠그고 다른 메소드에서 푸는 lock
- Semaphore
- CountDownLatch : 지정한 수만큼 이벤트가 발생하고 난 뒤에야 대기중인 스레드를 모두 해제한다. 모든 스레드에게 동시에 공평하게 시작할 기회를 준다.
실행 모델을 이해하라
producer-consumer
producer-consumer 패턴에서는 서로에게 시그널을 보내게 된다. 이 때 잘못된 코드를 작성하게 되면 서로에게서 시그널을 기다리는 상황이 발생할 수 있다.
readers-writers
읽기와 쓰기를 효율적으로 처리하기 위해 적절한 균형이 필요하다. shared-lock과 exclusive-lock의 균형 유지를 생각해보면 된다.
Dining Philosophers
너무 유명한 문제이지만 너무 많은 것을 배울 수 있는 문제이다. 주의해서 설계하지 않는다면 데드락, 라이브락, through put 저하, 효율성 저하 등의 문제를 겪는다.