백앤드 개발일지/자바

클린 코드 - 13장 동시성

giron 2022. 2. 15. 23:09
728x90

동시성 내용이 어려워서 개인적으로 따로 한 번 정리를 해보았다.

13장 동시성📌

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

  1. 여러 스레드를 동시에 돌리는 이유 & 어려움
  2. 이런 어려움에 대처하여 깨끗한 코드를 작성하는 방법
  3. 동시성을 테스트하는 방법과 문제점

동시성이 필요한 이유?

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

서블릿은 웹 혹은 EJB라는 컨테이너 아래서 돌아간다. 이들 컨테이너는 동시성부분적으로 관리한다.
웹 요청이 들어올 때마다 -> 웹 서버는 비동기식으로 서블릿을 실행한다.
프로그래머는 모든 웹 요청을 관리할 필요가 없다. 원칙적으로 각 서블릿 스레드는 다른 서블릿 스레드와 무관하게 자신만의 세상에서 돌아가기 때문이다.

동시성에 대한 오해

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

동시성에 대한 옳은 생각

  1. 동시성은 다소 부하를 유발한다. : 성능 측면에서 부하가 걸리며, 코드도 더 짜야 한다.
  2. 동시성은 복잡하다. : 간단한 문제라도 복잡하다.
  3. 동시성 버그는 재현하기 어렵다.
  4. 동시성을 구현하려면 근본적인 설계 전략을 재고해야 한다.

동시성 방어 원칙

단일 책임 원칙 (SRP)

동시성과 관련된 코드는 다른 코드와 분리해야 한다.

고려할 점

  1. 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
  2. 동시성 코드에는 독자적인 난관이 있다.
  3. 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.
  4. 동시성 코드는 다른 코드와 분리하라

따름 정리(corolloary)

  1. 자료 범위를 제한하라 : critical region(임계영역)을 줄이기 위해 synchronized 키워드로 보호하는것을 권장한다. 이런 임계영억의 수를 줄이는 기술이 중요하다.
  2. 자료 사본을 사용하라 : 이러한 공유 자원을 줄이기 위해서는 처음부터 공유하지 않는 방법이 제일 좋다. 그렇지 않다면 스레드가 객체를 복사해 사용한 후 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다.
  3. 스레드는 가능한 독립적으로 구현하라 : 다른 스레드와 자료를 공유하지 않는다.

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

  • 자바에서는 synchronized 키워드를 사용하면 락을 설정할수 있다. 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다.
  • 락은 스레드를 지연시키고 부하를 가중시킨다. 따라서 synchronized 문을 남발해서는 안된다. 반드시 critical region에만 사용하고 critical region수를 줄여야 한다.
  • 그런다고 critical region을 크게 하나로 구현하면 스레드 간에 경쟁이 늘어나고 성능이 떨어진다.

스레드 코드 테스트하기

  • 문제를 노출하는 테스트 케이스를 작성하라.
  • 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라.
  • 테스트가 실패하면 원인을 추적하라
  • 다시 돌렸더니 통과하더라는 이유로는 그냥 넘어가면 절대로 안 된다.

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

public synchronized String nextUrlOrNull() {
  if(hasNext()) {
    String url = urlGenerator.next();
    Thread.yield(); // 테스트를 위해 추가되었다.
    updateHasNext();
    return url;
  }
  return null;
}

위 코드는 여러가지 문제점이 있다.

  1. 보조 코드를 삽입할 적정 위치를 직접 찾아야 한다.
  2. 어떤 함수를 어디서 호출해야 적당한지 어떻게 알것인가
  3. 배포 환경에 보조 코드를 그대로 남겨둘 수도 있다.그러면 성능이 떨어질 것이다.
  4. 무작위적이다. 오류가 드러날지도 모르고 드러나지 않을지도 모른다.

시스템을 최대한 POJO 단위로 나눠 instrument code를 삽입할 부분을 찾기 쉽게 하고 여러 정책에 따라 sleep, yield등을 삽입할 수 있게 해야 한다.
자동화 적용

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'등을 무작위로 선택하게 할 수 있다.

결론

다중 스레드 코드는 올바로 구현하기 어렵다.
무엇보다 먼저, SRP를 준수한다. POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다.

스레드 코드를 테스트할때는 전적으로 스레드만 테스트한다. 즉, 스레드는 최대한 집약되고 작아야 한다.

라이브러리를 이해하고 기본적인 알고리즘을 이해하라. 라이브러리가 제공하는 기능이 어떻게 문제를 해결하는지 이해하라.

https://github.com/broooom/clean-code

728x90