[클린코드 완독스터디] TIL (2022.02.18)

yourjin·2022년 2월 27일
0

read.log

목록 보기
28/37
post-thumbnail

TIL (2022.02.18)

DAY 5

🔖 오늘 읽은 범위 : 13장, 동시성


😃 책에서 기억하고 싶은 내용을 써보세요.

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

  • 동시성과 깔끔한 코드는 양립하기 어렵다.
  • 동시성이 필요한 이유?
    • 동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)를 분리하는 전락이다. 스레드가 하나인 프로그램은 무엇언제가 서로 밀접하다.
    • 무엇언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다. 구조적인 관점에서 프로그램은 거대한 루프 하나가 아니라 작은 협력 프로그램 여럿으로 보인다. 따라서 시스템을 이해하기가 쉽고 문제를 분리하기도 쉽다.
    • 서블릿(Servlet) 모델은 웹 혹은 EJB 컨데이너라는 우산 아래서 돌아가는데, 이들 컨테이너는 동시성을 부분적으로 관리한다.
      • 웹 요청이 들어올 때마다 웹 서버는 비동기적으로 실행한다. 서블릿 프로그래머는 들어오는 모든 웹 요청을 관리할 필요가 없다. 원칙적으로 각 서블릿 스레드는 다른 서블릿 스레드와 무관하게 자신만의 세상에서 돌아간다.
      • (하지만) 실제로 컨테이너가 제공하는 결합 분리(decoupling) 전략은 완벽과 거리가 아주 멀다. 서블릿 프로그래머는 동시성을 정확히 구현하도록 각별한 주의와 노력을 기울여야한다.
      • 그럼에도 서블릿 모델이 제공하는 구조적 이점은 아주 크다.

        여기서 말하는 “구조적 이점”이란게 뭘까? 내가 내린 결론은 다음과 같다.
        서블릿 모델은 앞에서 설명한 것처럼 원칙적으로는 서로의 시간대에 영향을 받지 않는다. 즉, 여러 사람이 웹 요청을 보내도 한 사람(단일 스레드)이 순차적으로 처리하는 것이 아니라, 여러 사람(다중 스레드)이 각자 하나씩 맡아서 처리하는 것이다. 각자 한 책임만을 맡고 있기 때문에, 구조적으로는 이득이라고 볼 수 있다. 다른 사람의 책임을 신경 쓸 필요가 없으니까! 하지만 그 결과들이 한 곳에 모여질 때, 동시성을 지키기 위한 마지막 까다로운 작업이 남아 있을 것이다.

    • 어떤 시스템은 응답 시간과 작업 처리량(throughput) 개선이라는 요구사항으로 인해 직접적인 동시성 구현이 불가피하다. → 동시성이 반드시 필요한 상황이 있다!
      • 예시1. 정보 수집기(information aggregator)
        • 단일 스레드 수집기는 웹 소켓에서 입출력을 기다리는 시간이 아주 많다. 한 번에 한 사이트를 방문하는 대신 다중 스레드 알고리즘을 이용하면 수집기 성능을 높일 수 있다.
      • 예시2. 한 번에 한 사용자를 처리하는 시스템 / 예시3. 정보를 대량으로 분석하는 시스템
        • 여러 스레드로 동시에(병렬로) 처리하면 응답 시간을 높일 수 있다.
    • 미신과 오해
      • 동시성은 항상 성능을 높여준다.
        • 동시성은 때로 성능을 높여준다. 하지만 그 경우들은 일상적으로 발생하는 상황들은 아니다.
      • 동시성을 구현해도 설계는 변하지 않는다.
        • 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 일반적으로 무엇언제를 분리하면 시스템 구조가 크게 달라진다.
      • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
        • 실제로는 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.
    • 타당한 생각
      • 동시성은 다소 부하를 유발한다.
      • 동시성은 복잡하다.
      • 일반적으로 동시성 버그는 재현하기 어렵다.
        • 그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다
      • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고 해야 한다.
  • 난관
    • 동시성을 구현하기 어려운 이유는 무엇일까?
      public class X {
      	private int lastIdUsed;  // 42로 설정 
      	
      	public int getNextId() {
      		return ++lastIdUsed;
      	}
      }
      • 이제 두 스레드가 getNextld()를 호출한다고 가정하자. 결과는 셋 중 하나다.
        • 한 스레드는 43을 받고, 다른 스레드는 44를 받는다. lasIdUsed는 44가 된다.
        • 한 스레드는 44를 받고, 다른 스레드는 43을 받는다. lastldUsed는 44가 된다.
        • 한 스레드는 43을 받고, 다른 스레드는 43을 받는다. lastldUsed는 43이 된다. → 문제 발생!
      • 두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많은데, 그중에서 일부 경로가 잘못된 결과를 내놓기 때문이다.
      • 경로가 얼마나 많냐고? 간단하게 답하자면, 바이트 코드만 고려했을 때 두 스레드가 getNextld 메서드를 실행하는 잠재적인 경로 는 최대 12 ,870개에 달한다. lastldUsed 변수를 int 에서 long으로 변경하면 조합 가능한 경로 수는 2,704,156개로 증가한다.
      • 물론 대다수 경로는 올바른 결과를 내놓는다. 문제는 잘못된 결과를 내놓는 일부 경로다.
  • 동시성 방어 원칙
    • 단일 책임 원칙(Single Responsibility Principle, SRP)
      • 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다.
      • 권장사항: 동시성 코드는 다른 코드와 분리하라
    • 따름 정리(corollary): 자료 범위를 제한하라
      • 앞서 봤듯이, 객체 하나를 공유한 후 동일 필드를 수정하던 두 스레드가 서로 간섭하므로 예상치 못한 결과를 내놓는다. 이런 문제를 해결하는 방안으로 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호하라고 권장한다.
      • 이런 임계영역의 수를 줄이는 기술이 중요하다.
      • 권장사항: 자료를 캡슐화(encapsulation)하라. 공유 자료를 최대한 줄여라.
    • 따름 정리: 자료 사본을 사용하라
      • 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
      • 어떤 경우에는 객체를 복사해 읽기 전용으로 사용하는 방법이 가능하다. 어떤 경우에는 각 스레드가 객체를 복사해 사용한 후 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다.
      • 사본으로 동기화를 피할 수 있다면 내부 잠금을 없애 절약한 수행 시간이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.
    • 따름 정리: 스레드는 가능한 독립적으로 구현하라
      • 자신만의 세상에 존재하는 스레드를 구현한다. 즉, 다른 스레드와 자료를 공유하지 않는다. (…) 그러면 각 스레드는 세상에 자신만이 있는듯이 돌아갈 수 있다. 다른 스레드와 동기화할 필요가 없으므로.
      • 권장사항: 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라
  • 라이브러리를 이해해라
    • 스레드 환경에 안전한 컬렉션
      • java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 사용해도 안전하며, 성능도 좋다.
      • 실제로 ConcurrentHashMap은 거의 모든 상황에서 HashMap 보다 빠르다. 동시 읽기/쓰기를 지원하며, (보통 다중 스레드 환경에서 문제가 생기는) 자주 사용하는 복합 연산을 다중 스레드 상에서 안전하게 만든 메서드로 제공한다.
      • 권장사항: 언어가 제공하는 클래스를 검토하라. 자바에서는 java.util.concurrent, java.util.concurrent.atomic, java.util.locks를 익혀라.
  • 실행 모델을 이해하라
    • 기본 용어
      • 한정된 자원(Bounded Resource)
      • 상호 배제(Mutual Exclusion)
      • 기아(Starvation)
      • 데드락(Deadlock)
      • 라이브락(Livelock)
    • 생산자-소비자(Producer-Consumer)
      • 하나 이상 생산자 스레드가 정보를 생성해 버퍼(buffer)나 대기열(queue)에 넣는다. (…) 생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다
      • 잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.
    • 읽기-쓰기(Readers-Writers)
      • 읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 이 공유 자원을 이따금 갱신한다고 하자. 이런 경우 처리율(throughput)이 문제의 핵심이다.
      • 처리율을 강조하면 기아(starvation) 현상이 생기거나 오래된 정보가 쌓인다. 갱신을 허용하면 처리율에 영향을 미친다.
      • (읽기와 쓰기) 양쪽 균형을 잡으며너 동시 갱신 문제를 피하는 해법이 필요하다.
    • 식사하는 철학자들(Dining Philosophers)
      • 기업 애플리케이션은 여러 프로세스가 자원을 얻으려 경쟁한다. 주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.
      • 권장사항: 위에서 설명한 기본 알고리즘과 각 해법을 이해하라

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요

  • 의외로 이번 장에서 소스 코드가 많지 않았던 것은 그만큼 동시성 문제는 눈앞에서 재현하기 힘들다는 것을 보여주는 것 같았다. 이 부분에 나도 크게 동감한다. 동시성을 다루기 시작하면서 내가 쉽게 제어할 수 있는 영역도 그만큼 줄어들음을 느꼈다. 많은 동시성 방어 전략들을 배웠는데, 실무에서 이를 어떻게 적용할 수 있을지 많이 고민해봐야겠다.

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • EJB 컨데이너
  • 결합 분리(decoupling)
  • 처리율(throughput)

소감 3줄 요약

  • 동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)를 분리하는 전락이다.
  • 동시에 진행한다고 무조건적으로 성능을 향상시키는 것이 아니라 많은 부가 문제들이 따른다. 하지만, 동시성이 꼭 필요한 순간들이 있다.
  • 동시성은 무엇을 언제 처리하냐에 따라 무수히 많은 경우의 수를 내놓는다. 그 중 일부는 잘못된 결과를 도출하기 때문에 항상 방어 로직을 세워야 한다.
profile
make it mine, make it yours

0개의 댓글