Clean Code - 동시성(Concurrency)

No Image

동시성(Concurrency)

“Objects are abstractions of processing. Threads are abstractions of schedule.” — James O. Coplien

Thread를 하나만 실행하는 코드는 짜기가 쉽다. 겉으로 보기에는 멀쩡하지만 깊숙한 곳에 문제가 있는 다중 Thread 코드도 짜기 쉽다. 시스템이 커지고 부하가 커지면 문제는 발생한다. 이를 해결하기 위해 지키는 규칙에 대해 소개한다.

왜 Concurrency가 필요한가?

Concurrency는 단일 스레드에서 엮여 있던 “무엇(What)을 할 것인가”와 “언제(When) 끝날 것인가”간의 Coupoing을 해소시켜 준다. 이는 처리량과 구조 개선에 도움을 줄 수 있다. 거대한 하나의 Loop가 아니라 작은 협력 프로그램 여럿으로 나눌 수 있으며 이해하기 쉽고 문제를 분리하기 쉽다.

처리량 또한 향상될 수 있다. 한 유저의 요청을 처리하는 데에 1초가 필요한 시스템을 생각해 보자. 이 시스템은 적은 유저가 사용할 경우 그럭저럭 괜찮은 퍼포먼스를 보여줄 것이다. 하지만 유저가 늘어남에 따라 모든 유저는 자신보다 먼저 도착한 요청이 끝날 때까지 기다려야만 한다. 이러한 경우 concurrency가 여러 유저를 동시에 처리함으로써 처리량을 향상시킬 수 있다.

미신과 오해

Concurrency는 항상 퍼포먼스를 향상시킨다.

항상 그런 것만은 아니다. Concurrency는 여러 스레드 혹은 여러 프로세서가 대기 시간을 공유할 수 있는 경우에만 퍼포먼스를 향상시킨다. 하지만 이러한 경우는 드물다. 또한 Context Switching이 일어나기 때문에 퍼포먼스, 코드 작성 양쪽 모두에 약간의 오버헤드를 일으킨다.

Concurrency는 시스템의 디자인을 변경시키지 않는다.

“무엇”과 “언제”를 분리하는 작업은 보통 시스템의 구조에 큰 영향을 미친다. Concurrency 문제에는 보통 근본적인 디자인 개편이 필요하다.

Web나 EJB와 같은 컨테이너를 사용한다면 Concurrency 문제들은 신경쓸 필요가 없다.

컨테이너가 어떤 일을 하는가에 대해 알아야 하며 Concurrency update, Deadlock을 해결하는 방법을 알아야 한다.

Concurrency 관련 버그는 재현하기 어렵기 때문에 종종 one-off 2로 취급된다.

왜 Concurrency는 구현하기 어려운가?

public class ClassWithThreadingProblem {
    private int lastIdUsed;

    public ClassWithThreadingProblem(int lastIdUsed) {
        this.lastIdUsed = lastIdUsed;
    }

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

    public static void main(String args[]) {
        final ClassWithThreadingProblem classWithThreadingProblem = new ClassWithThreadingProblem(42);

        Runnable runnable = new Runnable() {
            public void run() {
                classWithThreadingProblem.getNextId();
            }
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

결과는 총 3가지 이다.

  • t1이 43을, t2가 44를 가져간다. lastIdUsed는 44이다.(O)
  • t1이 44을, t2가 43를 가져간다. lastIdUsed는 44이다.(O)
  • t1이 43을, t2가 43를 가져간다. lastIdUsed는 43이다.(X)

위의 getNextId() 메서드는 8개의 자바 byte-code로 변환되며, 이를 두 스레드에서 실행하게 되면 총 12,870개의 코드 조합을 낼 수 있다. 그 중 얼마 안 되는 몇몇 조합이 위의 3가지 결과 중 마지막 결과를 낳게 된다.

동시성 방어 원칙

단일 책임 원칙(Single Responsibility Principle, SRP)

Concurrency 디자인은 그 자체로 충분히 복잡하기 때문에 변경이 발생할 수 있다. 따라서 concurrency 관련 코드는 분리되어야 한다.

  • Concurrency 관련 코드는 개발, 변경, 튜닝시 다른 코드와 분리된 생명주기를 가진다.
  • Concurrency 관련 코드는 그 자체가 가지는 어려움(풀기 힘든 문제)이 있다.
  • 잘못 작성된 concurrency 코드는 여러 문제를 발생시킬 수 있으며, 이는 추가적인 코드 없이 해결되기 힘들다.

Concurrency 관련 코드는 다른 코드들과 분리하라.

따름 정리(Corollary): 자료 범위를 제한하라

공유 객체를 두 스레드에서 수정하는 중 간섭이 발생할 수 있으며 이는 예기치 못한 결과를 야기할 수 있다. 이러한 critical section을 보호하는 한 가지 방법은 synchronized 키워드를 사용하는 것이다. Critical section의 수는 가능한한 적게 만들어야 하며 이를 어길 경우 아래와 같은 문제가 발생하기 쉽게 된다.

  • 한두 군데를 보호하는 것을 까먹기 쉬우며 이로 인해 해당 자원을 수정하는 모든 코드를 망가트리게 된다.
  • 모든 곳이 보호되었는지 파악하기 위해 중복적인 노력이 필요하게 된다.
  • 이미 찾기 어려운 문제의 근원을 더 찾기 어렵게 만들게 된다.

데이터 캡슐화를 가슴 깊이 새기며, 공유될 만한 자원에 접근하는 부분(코드)을 극도로 줄여라.

따름 정리: 자료 사본을 사용하라

  • 공유 자원 문제를 해결하는 좋은 방법중 하나는 애초에 공유 자원을 사용하지 않는 것이다.
  • 읽기 전용으로 사용될 경우 자원의 복사본을 사용하게 하는 방법이 있다. 경우에 따라서는 복사본을 여러 스레드에 전달, 작업을 수행하고 결과를 단일 스레드에서 수집해 사용하는 것도 가능하다.
  • 객체의 복사에 드는 비용을 걱정할 수도 있다. 객체의 복사본을 사용함으로써 동기화를 피할 수 있다면, 객체 생성 및 GC에 드는 비용은 공유 자원 동기화에 필요한 비용 보다 일반적으로 적은 비용으로 문제를 해결하게 해 준다.(객체 복사 cost < 공유 자원 동기화 cost)

따름 정리: 스레드는 가능한 독립적으로 구현하라

  • 스레드 코드를 공유 자원을 사용하지 않는 독립된 세계로 만든다면 동기화 문제는 없어지게 된다. HttpServlet을 생각해 보라. HttpServlet을 상속받는 클래스는 doGet, doPost와 같은 메서드에서 필요한 파라미터를 받아 처리한다. 이는 각 Servlet이 각자의 세계에 있는 것처럼 작동하게 도와주며, 지역 변수를 사용하는 한 동기화 문제는 발생하지 않게 된다. 물론 대부분의 Servlet들은 데이터베이스 연결과 같은 공유 자원이 필요하긴 하다.

데이터를 스레드 더 나아가 각각의 프로세서에서 사용될 수 있게 독립적인 단위로 분할하라.

라이브러리 이해하기

  • 자바에서 제공하는 thread-safe 컬랙션을 사용하라. ex) ConCurrentHashMap, Vector
  • 연관이 없는 태스크들을 수행시 executor 프레임워크를 사용하라.
  • 가능하면 nonblocking 방법을 사용하라.
  • 몇몇 라이브러리 클래스들은 thread-safe하지 않다.

Thread Safe 컬렉션

java.util.concurrent 패키지는 멀티 스레드 환경에서 사용할 수 있는 컬랙션들을 제공한다. ConcurrentHashMap의 경우에는 일반 HashMap보다 대부분의 상황에서 더 좋은 퍼포먼스를 제공한다. 만약 배포 환경이 자바 5버전 이상이라면 이 패키지를 활용하자. 아래와 같은 고급 concurrency 디자인 구현을 위한 컴포넌트들도 숙지하자.

Name Description
ReentrantLock 한 메서드에서 잠그고 다른 메서드에서 해제될 수 있는 lock이다.
Semaphore 전통적인 세마포어(갯수를 셀 수 있는 lock)의 구현체이다.
CountDownLatch 기다리는 모든 스레드들을 해제하기 전 특정 횟수의 이벤트가 발생하는 것을 기다리게 할 수 있는 lock이다. 모든 스레드가 거의 동시에 시작될 수 있게 도와줄 수 있다.

당신에게 맞는 클래스를 살펴보라. 자바의 경우 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks를 살펴보라.

실행 모델 이해하기

Name Description
한정된 자원(Bound Resource) 다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.
상호 배제(Mutual Exclusion) 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
기아(Starvation) 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다.
데드락(Deadlock) 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.
라이브락(Livelock) 락을 거든 단계에서 각 스레드가 서로를 방해한다.

생산자 - 소비자(Produce-Consumer)

한 개 이상의 생산자가 생산한 작업물을 버퍼 혹은 큐에 넣는다. 한 개 이상의 소비자가 버퍼 혹은 큐에서 작업물을 습득, 작업을 마친다. 생산자와 소비자 사이에 있는 큐는 bound resource이다. 따라서 생산자는 큐에 남는 공간이 생길 때까지, 소비자는 큐에 작업물이 하나라도 생길 때까지 기다려야 한다. 큐를 통한 생산자와 소비자간의 조율에는 둘 사이의 시그널링이 필요하다. 생산자는 큐에 작업물을 넣고 소비자에게 “큐가 비어있지 않다”는 신호를 보내고 소비자는 큐에서 작업물을 꺼낸 후 “큐가 가득차 있지 않다”는 신호를 보낸다. 그 전까지 둘은 신호를 기다린다.

읽기-쓰기(Readers-Writers)

일반적으로 독자를 위한 정보로 사용되며, 가끔 저자에 의해 업데이트되는 공유 자원의 경우 처리량이 문제가 된다. 처리량을 강조해 독자가 상대적인 우선권을 가지게 되면 저자는 기아 상태에 빠지며 공유 자원은 정체된 정보로 가득차게 된다. 반대로 저자가 우선권을 가지면 처리량이 줄어들게 된다. 저자-독자 문제는 이 둘 사이의 균형을 맞추며 concurrent 업데이트를 방지하는 것을 주안점으로 둔다.

식사하는 철학자들(Dining Philosophers)

원탁을 둘러싼 여러 명의 철학자들이 있다. 각 철학자의 왼쪽에 포크가 놓여 있으며 테이블의 중앙에 큰 스파게티 한 그릇이 놓여 있다. 그들은 배가 고파지기 전까지 각자 생각을 하며 시간을 보낸다. 배가 고파지면 그들은 자신의 양쪽에 놓여 있는 포크 2개를 잡고 스파게티를 먹는다. 철학자는 포크 2개가 있어야만 스파게티를 먹을 수 있다. 그렇지 않다면 옆 사람이 포크를 다 사용하기 전까지 기다려야 한다. 스파게티를 먹은 철학자는 다시 배가 고파질 때까지 포크를 놓고 있게 된다. 위 상황에서 철학자를 스레드로, 포크를 공유 자원으로 바꾸게 되면 이는 자원을 놓고 경쟁하는 프로세스와 비슷한 상황이 된다. 잘 설계되지 않은 시스템은 deadlock, livelock, 처리량 문제, 효율성 저하 문제에 맞닥뜨리기 쉽다. 당신이 맞닥뜨릴 대부분의 concurrent관련 문제들은 이 세 가지 문제의 변형일 가능성이 높다. 이 알고리즘들을 공부하고 스스로 해법을 작성함으로써 이와 같은 문제들을 직면하더라도 의연하게 대처할 수 있도록 하자.

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

동기화된 메서드 간의 의존성은 concurrent 코드에서 사소한 버그를 일으킬 수 있다. 자바는 synchronized라는 “메서드 하나를 보호하는 노테이션”을 제공한다. 하지만 한 클래스에 두 개 이상의 synchronized 메서드가 존재하면 문제를 일으킬 수도 있다.

추천: 공유된 객체의 두 메서드 이상을 사용하는 것을 피하라.

만약 위 추천을 따를 수 없는 상황이라면 아래의 세 방법을 고려해 보라.

클라이언트 기반 잠금(Client-Based Locking)

  • 클라이언트가 첫 메서드를 부르기 이전부터 마지막 메서드를 부른 다음까지 서버를 잠근다.

    Bad: 서버를 사용하는 모든 클라이언트 코드에서 lock이 필요하게 되며 이는 유지보수 및 디버깅에 필요한 비용을 상승시킨다.

서버 기반 잠금(Server-Based Locking)

  • 서버 내에서 서버(자신)을 잠그고 모든 동작을 수행한 후 잠금을 푸는 메서드를 제공한다. 클라이언트에게는 새로운 메서드를 제공한다.

    Good: Critical section에 접근하는 코드를 최소화해 위 4-2에 부합한다.

중계된 서버(Adapted Server)

  • 잠금을 수행하는 중계자를 작성한다. 이는 기본적으로 서버 기반 잠금이지만 기존의 서버를 변경할 수 없는 상황에 사용할 수 있는 방법이다.

    Good: 서버 기반 잠금 방식을 사용할 수 없는 경우에 사용하자.

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

  • Synchronized로 수행되는 잠금은 딜레이와 오버헤드를 만들기 때문에 “비싼 수행”으로 간주되며 가능한 한 작게 만들어야 한다. 반면 critical section은 꼭 보호되어야 한다.

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

“항상 살아 있어야 하는 코드”의 작성은 “잠시 동작하고 조용히 끝나는” 코드의 작성과는 다르다. 조용히 끝나는 코드는 작성하기 어렵다. 이는 보편적으로 “오지 않을 신호”를 기다리는 쓰레드의 데드락을 포함한다. 데드락에 걸린 자식 스레드의 수행이 끝나길 기다리는 부모 스레드의 경우를 생각해 보라. 자식은 데드락에 걸려 멈춰 있고 부모는 이를 끝없이 기다리게 된다. 이와 같은 코드를 작성할 경우 정상적인 종료가 이루어질 때까지 많은 시간이 소요될 것을 상정해야 한다.

추천: 개발 초기에 시스템 종료에 대해 고민하고 구현하라. 이 작업은 생각보다 오래 걸릴 것이다. 기존에 구현한 알고리즘을 리뷰하는 것도 필요하다.

스레드 코드 테스트하기

  • 테스트는 정확성을 보장하지 않으며 “코드가 제대로 작성되었는가”를 증명할 수 없다. 다만 잘 작성된 테스트는 위험을 최소화할 수 있다. 이는 멀티 스레드 상황에서는 훨씬 더 복잡해 진다.

추천: 문제를 발생시킬 만한 테스트를 작성하고 여러 프로그램 설정과 시스템 설정, 부하 하에서 자주 수행하라. 테스트가 한번이라도 실패한다면 원인을 분석하라. 한번 더 테스트를 실행해 성공했다고 해서 이전의 실패를 무시하지 마라.

  • 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라
  • 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
  • 프로세서 수보다 많은 스레드를 돌려보라
  • 다른 플랫폼에서 돌려보라
  • 코드에 보조 코드instrument를 넣어 돌려라. 강제로 실패를 일으키게 해보라

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

멀티 스레드 코드는 일반적으로 발생할 리 없어 보이는 문제를 발생시킨다. (저자를 포함한)대부분의 개발자는 이러한 문제를 직관적으로 파악하지 못한다. 또한 이는 매우 드물게 발생해 개발자들을 좌절하게 만든다. 그래서 개발자들은 이러한 문제들을 우주선(宇宙線), 하드웨어 버그, 혹은 이러한 류의 one-off로 치부한다. 제일 좋은 방향은 one-off는 없다고 판단하는 것이다. 이러한 one-off들이 무시될 수록 더 많은 코드들이 이미 문제가 있는 시스템에 추가되게 될 뿐이다.

추천: 시스템 오작동을 one-off로 판단해 무시하지 말라.

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

당연한 말이지만 거듭 강조할 만큼 중요한 이야기이다. 스레드 밖에서 잘 동작하는 코드를 먼저 작성하라. 이는 스레드에서 사용될 POJO를 뜻한다. POJO는 스레드와 연관이 없어 스레드 밖에서도 테스트할 수 있다. 시스템은 가능한 한 POJO로 작성하는 것이 좋다.

추천: 스레드 관련 버그와 그렇지 않은 버그를 동시에 잡으려 하지 마라. 작성한 코드가 스레드 밖에서 잘 작동하는지 먼저 체크하라.

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

Concurrency 지원 코드를 아래와 같이 여러 설정으로 실행될 수 있게 만들어라. 단일 스레드, 여러 스레드 환경에서 동작하게 구현 실제 사용될 객체 혹은 Test Double과 상호작용할 수 있는 스레드 코드로 구현 수행 속도를 조절할 수 있는 Test Double을 구현 지정된 횟수만큼 반복 수행할 수 있게 구현

추천: 스레드 기반 코드를 여러 환경에서 실행할 수 있게 하라.

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

스레드 관련 코드의 적절한 균형을 맞추는 작업은 보통 시행착오를 필요로 한다. 여러 환경에서 시스템의 퍼포먼스를 테스트할 수 있는 방법을 개발 초기에 강구하라. 실행할 스레드 갯수를 쉽게 변경할 수 있게 작성하라. 이를 시스템이 동작하는 도중에 변경할 수 있게 하는 것을 고려해 보라. 처리량과 시스템 활용도를 기준으로 스스로를 조정할 수 있게 하는 것을 고려해 보라.

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

시스템이 작업을 전환할 때에도 문제는 발생한다. 작업 전환을 빈번히 발생하게 하기 위해 프로세서 수보다 많은 스레드를 실행해 보라. 작업 전환이 잦을수록 빠뜨린 critical section이나 dead lock을 찾을 확률이 높아지게 된다.

다른 플랫폼에서 돌려보라

우리(저자)는 2007년 중순 concurrent 프로그래밍 강좌를 개발했다. 강좌의 개발은 OSX에서 진행되었으며 시연은 VM상의 Windows XP에서 진행되었다. 하지만 실패를 시연하기 위해 작성된 테스트는 OSX에서는 자주 발생했지만 Windows XP에서는 OSX에서만큼 자주 발생하지 않았다. 우리는 이로 인해 서로 다른 운영체제는 상이한 스레딩 정책을 가지며 코드의 실행에 영향을 미친다는 것을 다시 한번 깨닫게 되었다. 멀티 스레드 코드는 실행 환경에 따라 다르게 동작한다. 따라서 당신은 모든 잠재적 배포 환경에 대해 테스트를 수행해야 한다. 추천: 스레드 관련 코드를 이른 시기에, 빈번한 주기로 모든 타겟 플랫폼에서 수행하라.

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

스레드 관련 문제는 수많은 실행 경로중 얼마 안되는 확률로 발생하기 때문에 드물게 발생하며 재현하기 어렵다. 이 실행 경로를 조작해 스레드 문제가 발생할 확률을 높이는 code instrumentation에는 두 가지 방법이 있다.

  • 직접 구현하기
  • 자동화

직접 구현하기

이는 Object.wait(), Object.sleep(), Object.yield(), Object.priority()등의 메서드를 사용해 실행 경로를 변경함으로써 코드의 문제를 발견하는 방법이다.

public synchronized String nextUrlOrNull() {
    if(hasNext()) {
        String url = urlGenerator.next();
        Thread.yield();
        // inserted for testing.
        updateHasNext();
        return url;
    }
    return null;
}

yield() 메서드를 호출함으로써 코드의 실행 경로를 변경할 수 있다. 만약 위 코드에서 문제가 발생한다면 이는 yield()를 추가해 생긴 문제가 아니라 이미 존재하던 문제를 명백히 만든것 뿐이다. 하지만 이 방법에는 몇 가지 문제가 있다.

  • 테스트할 부분을 직접 찾아야 한다.
  • 어디에 어느 메서드를 호출해야 할지 알기 어렵다.
  • 이와 같은 코드를 제품에 포함해 배포하는 것은 불필요하게 퍼포먼스를 저하시킬 뿐이다.
  • Shotgun approach이기 때문에 반드시 문제가 발생한다는 보장을 얻을 수 없다.

우리는 실제 제품에 포함되지 않으며 여러 조합으로 실행해 에러를 찾기 쉽게 만들 방법이 필요하다. 이를 위해서는 시스템을 최대한 POJO 단위로 나눠 instrument code를 삽입할 부분을 찾기 쉽게 하고 여러 정책에 따라 sleep, yield등을 삽입할 수 있게 해야 한다.

자동화

위와 다르게 Aspect-oriented Framework, CGLib, ASM등을 통해 프로그램적으로 코드를 조작할 수도 있다. 아래의 예를 보자.

public class ThreadJigglePoint {
    public static void jiggle() { }
}

public synchronized String nextUrlOrNull() {
    if(hasNext()) {
        ThreadJiglePoint.jiggle();
        String url = urlGenerator.next();
        ThreadJiglePoint.jiggle();
        updateHasNext();
        ThreadJiglePoint.jiggle();
        return url;
    }
    return null;
}

위와 같이 구현한 후 간단한 Aspect를 이용해 ‘아무 것도 안하기’, ‘sleep’, ‘yield’등을 무작위로 선택하게 할 수 있다. 혹은 ThreadJigglePoint가 두 가지 구현을 가지게 할 수도 있다. 첫 번째 구현은 배포용 코드를 위한 ‘아무 것도 안하기’를 수행하며 두 번째 구현은 ‘sleep, yield, 아무 것도 안하기’ 중의 하나를 무작위로 선택하는 것이다. 다소 간단하긴 하지만 좀 더 정교한 툴을 사용하는 대신 이 정도로 구현하는 것도 적절한 선택일 것이다.

혹은 이와 비슷한 작업을 수행해 주는 IBM에서 개발한 ConTest라는 툴도 있다. 이는 수행시마다 다른 순서로 스레드를 실행하게 만들어 줌으로써 문제를 발견할 확률을 극적으로 높여준다.

결론

Concurrent 코드는 제대로 작성하기 어렵다. 이해하기 쉬운 코드는 여러 스레드와 공유 자원이 엮이게 되면 끔찍한 결말을 낳게 된다. 당신이 concurrent code를 작성하게 된다면 엄격한 기준으로 clean하게 작성하라.

그렇지 않으면 찾기 어렵고 빈번하지 않은 오류를 만나게 될 것이다. 최우선적으로 SRP를 숙지하라. 시스템을 최대한 POJO단위로 잘라 스레드 관련 코드와 非 스레드 관련 코드를 나누어라. 스레드 관련 코드를 테스트할 때에는 그 이외의 것들은 제외하고 스레드 관련 문제만 테스트하라. 이는 스레드 관련 문제가 최대한 작은 부분에 집중되게 한다.

한 공유 자원에 대한 멀티 스레드 수행, 공유되는 자원 풀 등 concurrency 문제를 일으킬 수 있는 부분에 대해 인지하라. 깔끔하게 종료되게 하는 문제나 반복문 탈출과 같은 문제는 특히 성가실 수 있다. 라이브러리를 이해하고 기본적인 알고리즘을 이해하라. 라이브러리가 제공하는 기능이 어떻게 문제를 해결하는지 이해하라.

잠가야 할 필요가 있는 부분을 찾는 방법을 배우고 잠가라. 쓸데 없는 구간을 잠그지 마라. 잠긴 구간에서 또 다른 잠긴 구간을 부르는 것을 기피하라. 이는 “무엇이 공유되고 안되고”에 대한 깊은 이해를 요구한다. 공유 객체의 갯수와 공유 영역을 최소한으로 줄여라. 클라이언트가 공유 객체의 상태(잠금 등)를 관리하는 대신 공유 객체의 디자인을 변경하라.

문제는 돌연 발생할 것이다. 그렇지 않은 문제들은 보통 “한번만 발생하는” 문제로 치부된다. 이러한 one-off들은 보통 시스템에 부하가 걸린 경우, 혹은 무작위로 발생한다. 그러므로 스레드 관련 코드는 여러 설정, 환경에서 반복적이고 지속적으로 수행해 보라. 당신의 코드를 시간을 들여 instrument하게 되면 문제점을 찾을 확률은 높아질 것이다. 직접 코드를 작성할 수도 있고 자동화 툴을 사용할 수도 있다. 출시하기 전까지 최대한 오래 테스트 해야할 것이다 Clean한 접근 방식을 사용한다면, 제대로 된 코드를 만들어낼 가능성은 급격히 올라갈 것이다.

Reference

0%