CleanCode 13장 동시성

1.  동시성이 필요한 이유

동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)을 분리하는 전략이다.

이 2개를 분리하는 이유는 아래와 같다.

  • 애플리케이션 구조와 효율이 극단적으로 좋아진다.
    - 구조적인 관점에서 거대한 루프 하나가 아닌 작은 협력 프로그램 여럿으로 보인다.
    - 문제를 분리하여 시스템을 이해하기가 쉬워진다.
  • 응답 시간과 작업 처리량 개선에 도움이 된다.
    - 하나씩 처리하면 처리 대기열이 길어지면 그만큼 시간이 늘어나기 때문

2.   동시성에 대한 오해와 타당한 생각

< 오해들 >

  • 동시성은 항상 성능을 높여준다.
    ( 참일수도 거짓일수도 있다. )
    -> 대기시간이 아주 길어 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 참이다!
  • 동시성을 구현해도 설계는 변하지 않는다.
    ( 일반적으로 분리하면 시스템 구조가 크게 달라진다. )
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
    ( 어떻게 동작하는지, 동시수정, 데드락 같은 문제를 피할 수 있는 방법을 찾아야 한다. )

< 올바른 생각 >

  • 동시성은 다소 부하를 유발한다.
    ( 코드도 더 짜야한다. )
  • 동시성은 복잡하다.
    ( 새로 모든 것을 짜야하고 구조도 복잡하기에 맞다. )
  • 일반적으로 동시성 버그는 재현하기 어렵다.
    ( 구조가 복잡한 만큼 버그를 찾기도, 구현하기도 어렵다. )
    -> 일회성 문제로 여기면 안됨!
  • 동시성을 구현하려면 근본적인 설계 전략을 재고해야한다.
    (완전히 다른 코드가 되기 때문)

3.  동시성 구현이 어려운 이유

동시성이 어려운 이유는 아래와 같다.

public class X {
	private int lastIdUsed = 42;
    
	public int getNextId() {
		return ++lastIdUsed;
	}
}

2개의 스레드 A, B가 위 클래스의 인스턴스를 공유한다고 가정하고 getNextId를 호출하면 경우의 수는 다음과 같다.

  • A = 43, B = 44, lastIdUsed = 44
  • A = 44, B = 43, lastIdUsed = 44
  • A = 43, B = 43, lastIdUsed = 43

-> 바이트 코드만 고려했을 때 두 스레드가 getNextId 메서드를 실행하는 잠재적인 경로는 12,870개에 달한다!

3.   동시성 방어원칙

  1. 단일책임원칙(SRP)
  • 동시성 코드는 다른 코드와 분리해라
  1. 자료 범위를 제한하라
  • 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호해라
  • 임계영역을 줄여(캡슐화)해서 공유 자료를 최대한 줄여라.
  1. 자료 사본을 사용해라
  • 공유 자료를 줄이려면 공유하지 않는 방법이 제일 좋다.
  • 객체를 복사해서 읽기 전용으로 사용할 수 있다면 그렇게 해라.
  1. 스레드는 가능한 독립적으로 구현해라
  • 모든 정보를 비공유 출처에서 가져와 로컬 변수에 저장해라.

4.   라이브러리를 이해하라

java5 이후로는 스레드 환경에 안전한 컬렉션을 제공

  • java.util.concurrent: 다중 스레드 환경에서 안전한 클래스를 제공한다.
  • ConcurrentHashMap : 거의 모든 상황에서 HashMap보다 좋음(스레드 환경에서도 안전)
  • ReentrantLock : 한 메서드에서 잠그고 다른 메서드에서 푸는 락
  • Semaphore : 세마포
  • CountDownLatch : 지정한 수만큼 이벤트가 발생하고 나서야 대기 중인 스레드를 모두 해제 시키는 락, 모든 스레드에게 공평한 출발선을 제공한다.

5.   다중 스레드 실행모델을 이해해라

  • 생산자 - 소비자 문제
    - 하나 이상의 Producer 스레드가 정보를 버퍼나 대기열에 넣는다.
    • Producer 스레드는 대기열에 빈 공간이 있을 때만 정보를 채운다.(아니면 기다림)
      - 정보를 채우고 Consumer 스레드에게 시그널을 보낸다.
      - 하나 이상의 Consumer 스레드가 대기열에서 정보를 가져와 사용한다.
      - 대기열에 빈 공간이 있을 경우 Producer 스레드에 시그널을 보낸다.

잘못하면 Producer/Consumer 스레드가 둘 다 진행 가능한 상태임에도 서로에게서 시그널을 기다릴 위험이 있다.

  • 읽기 - 쓰기
    읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 쓰레드가 이 공유 자원을 이따금 갱신하는 상황을 의미한다.
    - 읽는 스레드의 작업 동안 쓰는 스레드의 작업이 기다리면 정보 업데이트가 되지 않는 문제가 있다
    - 쓰는 스레드의 작업 동안 읽는 스레드의 작업이 기다리면 이것도 기아 현상의 위험이 있음.

보통은 쓰기 스레드가 읽기 스레드가 끝날 때까지 기다린다.

  • 식사하는 철학자들
    둥근 식탁에 철학자들이 앉아있고, 철학자 왼쪽에는 포크, 가운데에는 스파게티가 놓여있다.
    배가 고프면 양손에 포크를 들고 스파게티를 먹는다 ( 양손으로 포크를 쥐어야만 식사가 가능 )
    스파게티를 먹으면 포크를 내려놓고 다시 배가 고플 때까지 생각한다.

철학자 = 스레드, 포크 = 자원

여러 프로세스가 자원을 얻으려 경쟁하기에 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.

6.   동기화하는 메서드 사이에 존재하는 의존성을 이해하라

동기화하는 메서드 사이에 의존성이 존재하면, 동시성 코드에 찾아내기 힘든 버그가 생긴다.
java는 개별 메서드를 보호하는 synchronized를 지원하지만 공유 클래스 하나에 동기화된 메서드가 여럿이라면 다시 확인해라

-> 공유 객체 하나에는 메서드 하나만 사용해라.

하지만 공유 객체 하나에 여러 메서드가 필요한 상황이 생긴다면 3가지를 고려해라

  • 동기화하는 부분을 작게 만들어라
    - java에서 synchronized를 사용하면 락을 설정한다.
    ( 락으로 감싼 영역은 한 번에 한 스레드만 실행이 가능하다. )
    - 하지만 락은 스레드를 지연시키고 부하를 가중시킨다.
    -> 동기화하는 부분은 최대한 작게 만들어라

  • 올바른 종료코드는 구현하기 어렵다
    - 깔끔하게 종료하는 코드는 구현하기 어렵다.
    ( 부모 스레드가 자식 스레드를 여러개 만든 후 모두가 끝나기를 기다렸다가 자원을 해제하고 종료하는 시스템이 있다면, 스레드 중 하나가 데드락에 걸리면 부모는 영원히 기다리고 종료가 되지 않는다! )
    -> 종료 코드를 개발 초기부터 고민하고 동작하게 구현해라. 생각보다 어렵기 때문에 이미 나온 알고리즘들을 검토해라

  • 스레드 코드 테스트하기
    - 말이 안되는 실패는 잠정적인 스레드 문제로 취급해라
    - 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
    -> 처음부터 그리고 자주 모든 목표를 플랫폼에서 코드를 돌려라

좋은 웹페이지 즐겨찾기