[CleanCode 13장] 동시성

soyeon·2022년 7월 26일
0

CleanCode

목록 보기
13/18
post-thumbnail

이 장에서는 스레드를 동시에 돌리는 이유와 어려움을 말한다.
또한 동시성을 테스트하는 방법과 문제점도 얘기한다.🎉

동시성이 필요한 이유?

동시성 : 결합을 없애는 전략이다. 무엇과 언제를 분리한다.

무엇과 언제를 분리하면 애플리케이션의 구조와 효율이 극적으로 나아진다. 작은 협력 프로그램 여럿으로 나뉘기 때문에 시스템을 이해하기 쉽고 문제를 분리하기 쉽다.

동시성은 구조적 개선 만이 아니라 응답 시간과 작업 처리량 개선을 위해서도 필요하다.

미신과 오해

반드시 동시성이 필요한 상황이 존재한다. 하지만 동시성은 어렵기 때문에 각별한 주의가 필요하다. 아래는 동시성에 대한 미신과 오해이다.

  1. 동시성은 항상 성능을 높여준다.
    : 동시성은 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능을 높여준다.

  2. 동시성을 구현해도 설계는 변하지 않는다.

  3. 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
    : 컨테이너의 동작 방법, 동시 수정과 데드락 같은 문제를 해결하는 방법을 알고 있어야 한다.

다음은 동시성과 관련된 타당한 생각이다.

  1. 동시성은 다소 부하를 유발한다.
  2. 동시성은 복잡하다.
  3. 일반적으로 동시성 버그는 재현하기 어렵다.
  4. 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

동시성을 구현하기 어려운 이유는 무엇일까?

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

=> 인스턴스 X를 생성하고, lastIdUsed의 값을 42로 설정한 후에 두 Thread가 getNextId()를 호출한다.

: 실행되는 순서가 일정하지 않기 때문에 여러 값이 실행 결과로 출력될 수 있다.

동시성 방어 원칙

다음은 동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술이다.

1. 단일 책임 원칙

: 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다.

동시성 코드는 복잡하기 때문에 다른 코드와 분리해야 한다. 동시성을 구현할 때는 아래 내용을 고려해야 한다.

  • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
  • 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
  • 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.

2. 따름 정리

자료 범위를 제한하라

객체 하나를 공유하고, 동일 필드를 동시에 thread가 수정하려고 하면 예상치 못한 결과가 발생한다.
=> 이 문제를 해결하기 위해서는 코드 내에 임계영역(critical section)을 synchronized 키워드로 보호한다.

하지만 공유 자료를 수정하는 위치가 많을수록 다음의 가능성도 높아진다.

  • 보호할 임계영역을 빼먹는다.
  • 모든 임계영역을 올바로 보호했는지 확인하는데 시간이 오래 걸린다.
  • 버그를 찾기 더 어려워진다.

=> 자료를 캡슐화 하고, 공유 자류를 최대한 줄인다.

자료 사본을 사용하라

공유 자료를 최대한 줄이기 위해 각 스레드가 객체를 복사해 사용하고, 한 스레드가 해당 사본에서 결과를 가져오는 방법을 사용할 수 있다.

스레드는 가능한 독립적으로 구현하라

다른 스레드와 자료를 공유하지 않게 구현한다.

독자적인 스레드, 가능하면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할한다.

라이브러리를 이해하라

Java 5부터는 동시성 측면에서 이전 버전보다 좋아졌다.

스레드 환경에 안전한 컬렉션

java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 사용해도 안전하고, 성능도 좋다.

이 외에도 여러 클래스가 추가 되었다.
-> ReentrantLock, Semaphore, CountDownLatch

실행 모델을 이해하라

  • 기본 용어 익히기
용어설명
한정된 자원다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다.
상호 배제한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
기아한 스레드나 여러 스레드가 오랫동안 자원을 기다린다.
데드락여러 스레드가 서로가 끝나기를 기다린다.
라이브락락을 거는 단계에서 각 스레드가 서로를 방해한다.

다중 스레드 프로그래밍에서 사용하는 실행 모델

생산자-소비자

생산자 스레드는 정보를 생성해서 버퍼나 대기열에 넣는다. 소비자 스레드는 대기열에서 정보를 가져와서 사용한다. 대기열이 한정된 자원이다.

생산자 스레드는 대기열에 빈 공간이 생겨야 채울 수 있고, 소비자 스레드는 대기열에 정보가 있어야 가져올 수 있다.

생상자 스레드는 정보를 채운 후에 정보를 채웠다는 시그널을 보내고, 소비자 스레드는 정보를 읽은 후에 대기열이 비었다는 시그널을 보낸다.
=> 동시에 서로에게 시그널을 기다릴 가능성이 존재한다.

읽기-쓰기

읽기 스레드와 쓰기 스레드 간의 균형을 잡는 것은 어렵다. 대개는 쓰기 스레드가 버퍼에 데이터를 쓰는 동안 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어지게 된다.

=> 읽기 스레드가 없을 때까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법을 사용한다.

하지만 이 방법은 읽기 스레드가 계속해서 이어지는 경우에 쓰기 스레드가 기아 상태에 빠지게 된다.

식사하는 철학자들

식사하는 철학자들

철학자 한 무리가 둥근 식탁에 둘러앉아 있고 식탁 가운데에 스파게티가 놓여있다. 각 철학자 왼쪽에는 포크가 놓여있다.
철학자는 배가 고프지 않으면 생각하며 시간을 보내고, 배가 고프면 양손으로 포크를 집어 스파게티를 먹는다. 스파게티는 양손으로 포크를 쥐어야만 먹을 수 있다.
왼쪽 철학자나 오른쪽 철학자가 스파게티를 먹고 있으면 기다려야 한다.

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

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

공유 클래스 하나에 동기화 된 메서드가 여러개가 나오면 안된다.

공유 객체에 여러 메서드가 필요한 경우에는 아래 세가지를 고려한다.

  • 클라이언트에서 잠금
    : 클라이언트에서 첫번째 메서드를 호출하기 전에 서버를 잠근다.
  • 서버에서 잠금
    : 서버에 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다.
  • 연결 서버
    : 잠금을 수행하는 중간 단계를 생성한다.

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

Java에서는 synchronized 키워드를 사용하면 락을 설정할 수 있다.

같은 락으로 감싼 코드 영역은 한 번에 한 스레드만 실행할 수 있다. 락은 스레드를 지연시키기 때문에 너무 남발해서는 안된다.

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

깔끔하게 종료하는 코드는 올바르게 구현하게 어렵다. 데드락이 가장 흔하게 발생한다.

만약, 부모 스레드가 자식 스레드를 여러개 만들고 모두가 끝나기를 기다렸다가 자원을 해제하고 종료하는 프로그램이 있다고 하고 자식 스레드가 데드락이 걸리면 부모 스레드는 영원히 기다리고 시스템은 종료되지 않는다.

위와 같은 상황은 흔하게 발생하기 때문에 시간을 투자해 올바르게 구현해야 한다.

스레드 코드 테스트하기

코드가 올바르다고 증명하는 것은 어렵지만 충분한 테스트는 위험을 낮춘다.

구체적인 지침

  • 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
    다중 스레드 코드는 말이 안되는 오류가 발생하는 경우가 있다. 스레드 코드에서는 실패를 재현하기가 어렵다. 따라서 많은 개발자들이 일회성 문제로 무시한다. 무시하면 잘못된 코드가 계속해서 쌓이게 된다.

  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
    스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인해야 한다.

  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라

  • 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
    적절한 스레드 개수를 파악하는 것은 어렵기 때문에 조율할 수 있게 코드를 작성해야 한다.

  • 프로세서 수보다 많은 스레드를 돌려보라
    스레드를 스와핑 할 때 문제가 발생할 수 있다. 프로세서 수보다 많은 스레드를 돌려 스와핑이 발생하게 테스트 해봐야 한다.

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

  • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
    스레드 코드는 오류를 찾기 어렵다. 오류를 자주 일으키기 위해서는 보조 코드를 추가해 코드가 실행되는 순서를 바꿔준다.

코드에 보조 코드 추가하는 방법

  • 직접 구현하기
    : 코드에 직접 wait(), sleep(), yield(), priority() 함수를 추가한다.
    단점
    => 보조 코드를 삽입할 적정 위치를 찾아야 한다.
    => 무작위적이다.
    => 배포 환경에 보조 코드를 그대로 남겨두면 성능이 떨어진다.
  • 자동화
    : AOF, CGLIB, ASM과 같은 도구를 사용한다.
    코드 흔들기 - 무작위로 sleep이나 yield를 호출한다.

0개의 댓글