[클린코드] 13장 동시성

이준기·2022년 2월 20일
0

다중 스레드는 겉으로 보기에는 멀쩡하나 깊숙한 곳에 문제가 있을 수 있다.

동시성과 깔끔한 코드는 양립하기 아주 어렵다.

동시성이 필요한 이유?

  • 동시성은 결합을 없애는, 즉 무엇언제를 분리하는 전략이다.
  • 분리하면 구조와 효율이 극적으로 나아지며 프로그램은 거대한 루프 하나가 아니라 작은 협력 프로그램 여럿으로 보인다.
  • 구조적 개선뿐만 아니라 응답 시간과 작업 처리량 개선이라는 요구사항으로 인해 동시성 구현이 필요하다.

동시성의 진실

  • 동시성은 항상이 아닌 특정 상황에서 성능을 높여준다.
  • 동시성을 구현하면 시스템 구조가 크게 달라진다.
  • 동시성은 성능 측면에서 부하가 걸리며, 코드도 더 짜야한다.
  • 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다.

난관

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

인스턴스 X를 생성하고, lastIdUsed 필드를 42로 설정한 다음, 두 스레드가 해당 인스턴스를 공유한다.

이제 두 스레드가 getNextId();를 호출한다고 가정했을때, 결과는 어떻게 될까?

한 스레드는 43을 받는다. 다른 스레드는 43을 받는다. lastIdUsed는 43이 된다.

Why?

두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많은데, 그 중에 일부 경로가 잘못된 결과를 내놓기 때문이다.

동시성 방어 원칙

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

단일 책임 원칙

  • 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다.
  • 즉, 동시성 관련 코드는 다른 코드와 분리해야 한다.

자료 범위를 제한하라

  • 공유 객체를 사용하는 코드 내 임계영역을 syncronized 키워드로 보호하라고 권장한다.
  • 공유 자료들이 많아질수록 문제가 많아지므로, 애초에 임계영역의 수를 줄이는게 중요하다.

자료 사본을 사용하라

  • 공유 자료를 줄이려면 처음부터 공유하지 않는 것이 제일 좋다.
  • 객체를 복사해 사용하자.

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

  • 자신만의 세상에 존재하는, 즉 다른 스레드와 자료를 공유하지 않는 스레드를 구현하라.
  • 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

라이브러리를 이해하라

  • 스레드 환경에 안전한 컬렉션을 사용하자!
  • 가능하다면 스레드가 차단(blocking)되지 않는 방법을 사용한다.

실행 모델을 이해하라

다중 스레드 애플리케이션을 분류하는 방식은 여러 가지다.

  • 한정된 자원(Bound Resource) : 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다.
  • 상호 배제(Mutual Exclusion) : 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
  • 기아(Starvation) : 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다.
  • 데드락(Deadlock) : 여러 스레드가 서로가 끝나기를 기다린다.
  • 라이브락(Livelock) : 락을 거는 단계에서 각 스레드가 서로를 방해한다.

세세하게 공부 필요!!!!

생산자-소비자

잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.

읽기-쓰기

읽기 쓰레드(공유 자원을 사용)의 요구와 쓰기 쓰레드(공유 자원을 갱신)의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.

식사하는 철학자들

둥근 식탁에 한 무리가 둘러 앉았고, 각 사람 왼쪽에는 포크가 있다. 음식을 먹기 위해선 양손에 포크를 쥐어야 한다. 이런 상황에서, 왼쪽이나 오른쪽 사람이 음식을 먹는다면 포크를 내려놓을때 까지 기다려야한다.

사람을 스레드로, 포크를 자원으로 바꿔서 생각해보자. 여러 프로세스가 자원을 얻으려 경쟁한다.


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

  • 공유 객체 하나에는 메소드 하나만 사용하자.

만약 공유 객체 하나에 여러 메소드가 필요한 상황일땐, 다음 세 가지 방법을 고려한다. ( 어렵다,,,,, )

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

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

  • syncronized 키워드를 사용해 설정한 락 코드 영역은 한 번에 한 스레드만 실행 가능하다.
  • 락은 스레드를 부하시키므로 임계영역 수를 최대한 줄이자.

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

  • 데드락에 빠지면 영원히 기다리게 된다. 확실히 고민하고 초기부터 구현해라.

스레드 코드 테스트하기

  • 문제를 노출하는 테스트 케이스를 작성하라.
  • 스레드가 둘 이상으로 늘어나면 상황은 급격하게 복잡해진다.
  • 다시 돌렸더니 통과하더라는 이유로 넘어가면 절대 안된다. 원인을 추적하라.

말이 안 되는 실패를 잠정적인 스레드 문제로 취급하라

  • '일회성' 문제로 치부하고 무시하지 말자.

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

  • 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 말자.

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

  • 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라

다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라

  • 적절한 스레드 개수를 파악하려면 상당한 시행착오가 필요하다.

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

  • 시스템이 스레드를 스와핑(swapping)할 때도 문제가 발생한다.
  • 많은 스레드를 돌려, 스와핑을 일으켜 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉽게 하자.

다른 플랫폼에서 돌려보라

  • 플랫폼에 따라 다르게 돌아간다.

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

  • 보조 코드를 이용하여 오류를 일으켜 버그를 발견해보자.
  • 보조 코드 방법으로 자동화를 쓰자. 자동화는 도구를 써서 편하게 오류를 내준다.

결론

간단했던 코드가 여러 스레드와 공유 자료를 추가하면서 악몽으로 변한다.

다중 스레드 코드를 작성한다면, 위의 규칙들을 차근차근 생각하며 각별히 깨끗하게 코드를 짜보자.

Reference

클린 코드: 애자일 소프트웨어 장인 정신 - 로버트 마틴 지음

profile
Hongik CE

0개의 댓글