본문 바로가기

Clean code

Clean Code - 13장. 동시성

개요

객체는 처리의 추상화다. 스레드는 일정의 추상화다. -제임스 O.코플리엔

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

다중 스레드 테스트해보기도 어렵다.

이 2가지 어려움을 해당 단원을 읽으면서 헤쳐나갈 수 있었다!

 

동시성이 필요한 이유

동시성은 Coupling을 없애는 전략이다.

동시성은 무엇(what)과 언제(when)을 분리하는 전략이다. 스레드가 하나인 프로그램은 무엇과 언제가 서로밀접하다.
  • 무엇,언제를 분리하면 애플리케이션 구조와 효율이 극적으로 좋아진다.
  • 응답 시간과 작업 처리량 개선에도 도움이 된다.
    • 데이터를 대량으로 분석하는 시스템에서 병렬로 처리

구조 개선의 좋은 예는 Servlet 모델일 것이다. 이론적으로, Servlet 개발자는 요청을 개별적으로 처리하는 데에만 신경을 쓰며 요청 큐를 직접 관리하는 부담을 덜 수 있다. 물론, Servlet이 제공하는 의존성의 해소는 완벽하지 않지만 Servlet이 제공하는 구조적인 이점은 그 자체로 가치가 있다.

 

그리고 동시성과 관련된 타당한 생각들을 살펴보자.

  1. 동시성은 성능, 코드 작성 양쪽 모두에 오버헤드가 발생한다
  2. 동시성 관련 버그는 재현 어렵기 때문에 종종 일회성 문제로 여겨 무시하곤 한다
  3. 동시성 문제는 근본적인 디자인 개편이 필요하다

동시성에 대한 오해

  1. 동시성은 항상 성능을 높여준다 ( 아닌 경우도 있긴 하지만 잘 없다.. )
  2. 동시성을 구현해도 기존 설계는 변하지 않는다
    • 일반적으로 무엇과 언제를 구분하는 것이기 때문에 설계가 많이 변한다
  3. Web 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다
    • 어떻게 동작하는지 이해를 하고 있어야 하며, 동시 수정 문제, 데드락 문제를 피할 수 있는 방법을 알아야 한다

 

동시성 구현 난관 예제

/* Code 1-1 */
    
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();
}
  • t1이 43을, t2가 44를 가져간다. lastIdUsed는 44이다.(O)
  • t1이 44을, t2가 43를 가져간다. lastIdUsed는 44이다.(O)
  • t1이 43을, t2가 43를 가져간다. lastIdUsed는 43이다.(X)

동시성 방어 원칙

단일 책임 원칙!

동시성과 관련된 코드는 다른 코드들과 분리하라! 그렇게 해야 하는 이유와 방법은 아래와 같다

  • Executor, Scheduler와 같은 곳으로 분리하라 ( 자세한 건 밑에 나올 예정 )
  • 동시성 관련 코드는 그 자체가 하나의 역할이기 때문에 분리해야 한다
  • 동시성 코드는 개발,변경,튜닝시 다른 코드와 다른 생명주기를 가진다.

자바 동시성 라이브러리 사용하기

자바5 스레드 환경에 안전한 컬렉션 제공

  • java.util.concurrent : 다중 스레드 환경에서 안전한 클래스 제공
  • ConcurrentHashMap : 거의 모든 상황에서 HashMap보다 빠르다. 동시 읽기/쓰기 지원
  • ReentrantLock : 한 메서드에서 잠그고 다른 메서드에서 푸는 락이다
  • Semaphore : 전형적인 세마포다. 개수가 있는 락
  • CountDownLatch : 지정한 수만큼 이벤트가 발생하고 나서야 대기 중인 스레드를 모두 해제하는 락. 모든 스레에게 동시에 공평하게 시작할 기회를 줌

 

동시성 코드 테스트하기 

동시성 코드 테스트는 통과 되는 경우도 있고 아닌 경우가 있기 마련이다. 이와 관련된 테스트를 진행할 때 어떤 것을 고려해야 할까?

  • 말이 안되는 실패 = 잠정적인 스레드 문제로 취급
    • 일회성 문제라고 치부하지 말자! 무시하고 개발을 진행하면 나중에는 더 큰 문제가 발생한다
  • 다중 스레드를 고려하지 않은 순차코드부터 제대로 돌게 만들자
    • 즉, 스레드가 호출하는 POJO를 만들자
    • 스레드 환경에서 생기는 버그 / 스레드 환경 밖에서 생기는 버그 동시에 디버깅을 하지말자
  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있께 쓰레드 코드를 구현하자
    • 다양한 설정+목적(ex : 스레드 수 변경)으로 테스트 할 수 있게 코드를 구현하라
    • 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려보자
    • 반복 테스트가 가능하도록 테스트케이스를 작성하자
  • 프로세서 수보다 많은 스레드를 돌려보자
    • 시스템이 스레드를 swapping(스와핑) 할 때도 문제가 발생한다
    • 이 때, 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다
  • 코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라
    • 이와 관련해서 자세한 내용은 밑에 예제를 살펴보자

 

동시성 코드 테스트하기 어려운 이유는 재현이 어렵다는 것이다. 이렇게 드물게 발생하는 오류를 자주 일으킬 방법은 없을까?

동시성 코드 테스트하기(1) - 직접 구현하기

테스트를 위해 비즈니스 로직에 sleep, yield 메서드를 사용해 실행 경로를 변경하게 한다. 아래 코드가 예제이다.

/* Code 3-1 */

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

하지만 위의 코드는 딱봐도 문제를 가지고 있다.

  • 운영 환경에서 굳이 이 코드를 가지고 있을 필요가 있는가? 성능 떨어지게...
  • 테스트 할 코드를 직접 찾아야 한다

동시성 코드 테스트하기(2) - 자동화

jiggle(흔들기) 기법을 이용해서 동시성 코드 테스트를 수천번 해보자. 아래 코드는 Aspect를 이용해 '아무것도 안하기', 'sleep','yield' 등을 무작위로 선택해서 테스트 할 수 있게 해주는 것이다.

/* Code 4-1 */

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' 3가지 중 렌덤으로 선택해서 동작되게 하면 된다.