티스토리 뷰

도서/클린 코드

동시성

woo'^'chang 2022. 7. 22. 22:25

동시적인 문제를 생각하지 않고 스레드를 하나만 실행하는 코드는 작성하기 쉽습니다. 스레드를 동시에 돌리는 코드 작성은 고려해야 할 사항들이 많기에 어려움이 존재합니다. 이번 장에서는 스레드를 동시에 돌리는 이유와 어려움에 대처하고 깨끗한 코드를 작성하는 방법을 제안합니다. 또한 동시성을 테스트하는 방법과 문제점에 관해 얘기합니다.

동시성이 필요한 이유?

동시성은 무엇언제를 분리하는 작업입니다. 이를 분리하게 되면 애플리케이션 구조와 효율이 극적으로 나아집니다. 하나의 작업이 끝나야만 다음 작업으로 넘어가게 된다면 응답 시간이 매우 길어지는 문제가 발생합니다. 동시성을 사용하면 응답 시간도 줄일 수 있는 장점이 존재합니다.

 

동시성은 어렵기에 미신이 존재하기도 합니다.

  • 동시성은 항상 성능을 높여준다.
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
  • 동시성을 구현해도 설계는 변하지 않는다.

동시성은 대기 시간이 길어 여러 스레드가 프로세스를 공유할 수 있을 경우나 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아집니다. 동시성을 알맞게 사용하기 위해서는 동시 수정, 데드락 등과 같은 문제를 이해하고 피할 수 있어야 하고 단일 스레드 시스템과 다중 스레드 시스템은 설계가 다름을 잘 알고 있어야 합니다.

 

동시성에 관한 타당한 생각들도 존재합니다.

  • 동시성은 다소 부하를 유발한다.
  • 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다.
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

동시성 방어 원칙

단일 책임 원칙(Single Responsibility Principle)

동시성이 가지는 복잡성 하나만으로도 분리해야 하는 이유는 충분합니다. 다시 말해서 동시성 관련 코드는 다른 코드와 분리해야 한다는 의미입니다. 하지만 동시에 구현하는 사례가 많기에 다음과 같은 고려 사항을 제안합니다.

  • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
  • 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
  • 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.

자료 범위를 제한하라

공유 객체를 사용해서 발생하는 문제는 모두 잘 알고 계실 것으로 생각합니다. 이를 방지하기 위해서는 공유 객체를 사용하는 코드 내 synchronized 키워드로 보호가 권장됩니다. 이러한 변경 가능성이 존재하는 임계 영역을 줄이는 것이 중요합니다. 그 이유는 다음과 같습니다.

  • 보호할 임계 영역을 빼먹는다. 그래서 공유 자료를 수정하는 모든 코드를 망가뜨린다.
  • 모든 임계 영역을 올바로 보호했는지 확인하느라 똑같은 노력과 수고를 반복한다.
  • 그렇지 않아도 찾아내기 어려운 버그를 더욱 찾기 어려워진다.

자료 사본을 사용하라

공유 자료를 줄이기 위해서는 처음부터 공유하지 않는 방법이 제일 좋습니다. 자료를 복사하는 비용을 우려할 수 있는데 내부 잠금의 비용을 상쇄할 가능성이 큽니다.

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

독자적으로 존재하는 스레드는 지역 변수를 사용하기에 공유로 인해 발생하는 문제를 겪을 수가 없습니다. 필자는 독자적인 스레드로 가능하면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 나누도록 권장합니다.

라이브러리를 이해하라

자바에서 스레드 코드를 구현한다면 다음을 고려해보시기를 바랍니다.

  • 스레드 환경에 안전한 컬렉션을 사용한다. 자바 5부터 제공한다.
  • 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.
  • 가능하다면 스레드가 차단 되지 않는 방법을 사용한다.
  • 일부 클래스 라이브러리는 스레드에 안전하지 못하다.

실행 모델을 이해하라

한정된 자원(Bound Resource)

다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.

상호 배제(Mutual Exclustion)

한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.

기아(Starvation)

한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다. 예를 들어, 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아 상태에 빠진다.

데드락(Deadlock)

여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.

라이브락(Livelock)

락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.

생산자-소비자

생산자 스레드가 정보를 생성해 버퍼나 대기열에 넣습니다. 소비자 스레드가 대기열에서 정보를 가져와 사용합니다. 따라서 대기열은 한정된 자원이라고 할 수 있습니다. 잘못하면 둘 다 진행할 수 있음에도 불구하고 동시에 시그널을 기다릴 가능성이 존재합니다.

읽기-쓰기

읽기와 쓰기는 처리율기아 문제와 관련이 깊습니다. 읽기와 쓰기 타이밍을 적절하게 조절해주는 것이 중요합니다. 읽기 스레드가 존재하는 동안 쓰기 스레드가 기다린다면 기아 문제가 발생하고, 쓰기 스레드에게 우선권을 준다면 처리율에 문제가 발생할 수 있습니다. 상황에 알맞게 양쪽 균형을 잡을 수 있도록 하는 적절한 해결 방안을 제시해야 합니다.

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

공유 객체 하나에는 메서드 하나만 사용하라고 권장하고 있습니다. 여러 메서드가 필요한 상황에는 다음을 고려해야 합니다.

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

스레드 코드 테스트하기

스레드 코드 테스트는 고려해야 할 사항들이 매우 많습니다. 책에서는 구체적인 지침을 제시하고 있습니다.

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

마치며

동시성을 공부하는 것은 매우 어려운 일인것 같습니다. 이를 조금이나마 쉽게 이해하기 위해 다음과 같은 방법을 사용해볼 수 있습니다.

  1. 단일 책임 원칙을 준수합니다.
  2. 동시성 오류를 일으키는 잠정적인 원인을 철저히 이해합니다.
  3. 사용하는 라이브러리와 기본 알고리즘을 이해합니다.
  4. 보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해합니다.
  5. 스레드 코드는 많은 플랫폼에서 많은 설정으로 테스트를 반복합니다.

'도서 > 클린 코드' 카테고리의 다른 글

JUnit 들여다보기  (0) 2022.07.26
점진적인 개선  (0) 2022.07.24
창발성  (0) 2022.07.21
시스템  (0) 2022.07.21
클래스  (0) 2022.07.19
댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Total
Today
Yesterday