IT/책

[Clean Code] 13장, 동시성

Terriermon 2021. 11. 14. 01:32

Clean Code

13장, 동시성

 

동시성과 깔끔한 코드는 양립하기 어려움

 

여러 스레드를 동시에 돌리는 이유, 여러 스레드를 동시에 돌리면 왜 어려울까? 그리고 이러한 어려움에 대처하고 깨끗하게 코드는 어떻게 작성할 수 있을까?

 

 

1. 동시성이 필요한 이유

동시성: 결합(coupling)을 없애는 전략, 처리량(throughput) 개선

 

무엇과 언제를 분리

ex. 단일 스레드: breakpoint를 정해서 디버깅하여 시스템 상태 파악

멀티 스레드: 구조적 관점에서 작은 협력 프로그램 여럿으로 보이게 되어 시스템 이해가 쉬워짐

 

ex. 서블릿

- 컨테이너는 동시성을 부분적으로 관리: 웹 요청이 들어올 때, 웹 서버는 비동기식으로 서빌릿 실행, 서블릿 프로그래머는 웹 요청을 ㅗ간리하지 않아도 됨

 

- 정보를 병렬적으로 처리

동시성의 오해
1. 동시성은 항상 성능을 높여준다.
> 때로 성능을 높여준다. 일상적으로 발생하지는 않음

2. 동시성을 구현해도 설계는 변하지 않는다.
> 무엇과 언제를 구분하면 설계가 달라진다.

3. 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
> 동시수정, 데드락 등의 문제는?


동시성의 사실
1. 부하를 유발
2. 복잡
3. 일반적으로 동시성 버그는 재현하기 어려움
4. 근본적인 설계 전략을 재고

 

 

2. 동시성이 어려운 이유

 

- 두 스레드가 같은 변수를 동시에 참조할 때

> 자바의 바이트코드 처리 방식에 의해 어떻게 처리가 되는 지를 알아야 함

> 바이트코드 처리 방식까지 신경쓰기 어려움

 

 

 

3. 동시성 방어 원칙

 

1) 단일 책임 원칙 SRP

** 동시성은 다른 코드와 분리 **

 

- 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 함

 

- 동시성은 복잡성 하나만으로 따로 분리할 이유가 충분 -> 다른 코드와 분리하여 구현

 

 

2) 따름 정리(corollary): 자료 범위 제한

** 자료를 캡슐화하여 공유 자료를 최대한 줄임 **

 

- 객체 하나를 공유하여 수정하면 스레드가 서로 간섭을 하여 예상하지 못하는 결과를 내놓음

 

- 임계영역(critical section)을 synchronized 키워드로 보호

> 임계영역 수를 줄이는 기술도 중요함

 

- 임계영역을 빼먹을 경우 큰 문제 발생 가능성 ↑

 

 

3) 따름 정리: 자료 사본 사용

 

- 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 가장 좋음

> 객체를 복사하여 읽기 전용으로 사용하기

 

- 공유 객체를 비하면 코드가 문제를 일으킬 가능성이 낮아짐

 

 

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

** 독자적인 스레드로, 가능하면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할 **

 

- 다른 스레드와 자료를 공유하지 않고, 각 스레드는 클라이언트 요청 하나를 처리 -> 다른 스레드와 동기화할 필요가 없음

 

- 모든 정보는 비공유 출처에서 가져오고 로컬 변수에 저장

 

 

4. 라이브러리를 이해하라

자바 5에서 스레드 코드 구현 시 고려 사항

 

- 스레드 환경에 안전한 컬렉션: java.util.concurrent

> ConcurrentHashMap: 동시 읽기/쓰기 지원, HashMap보다 빠름

> p.233 참고

 

 

 

5. 실행 모델을 이해하라

한정된 자원 다중 스레드 환경에서 사용하는 자원, 크기나 숫자가 제한적
상호 배제 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우
기아 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다리는 상태
데드락 여러 스레드가 서로 끝나기를 기다림
라이브락 락을 거는 단계에서 스레드가 서로를 방해, 스레드는 계속해서 진행하려 하지만, 공명(resonance)으로 인해 굉장히 오랫동안 진행하지 못함

 

생산자-소비자

- 하나 이상 생산자 스레드가 정보를 생성해 버퍼나 대기열에 넣음

 

- 하나 이상의 소비자 스레드가 대기열에서 정보를 가져와 사용

 

- 생산자와 소비자가 사용하는 대기열은 한정된 자원

> 생산자: 대기열에 빈공간이 있어야함 -> 빈 공간이 있을 때까지 기다림

> 소비자: 대기열에 정보가 있어야함 -> 정보가 채워질때까지 기다림

 

- 대기열에 관한 정보를 생산자와 소비자가 주고받아야 함

 

 

읽기-쓰기

- 읽기 스레드: 공유 자원을 사용, 쓰기 스레드: 공유 자원을 갱신

 

- 처리율을 강조하면 기아 현상이 생기거나 오래된 정보가 쌓임

> 균형을 잡기가 히듦

 

 

식사하는 철학자들

- 둥근 식탁의 철학자, 각 철학자 왼쪽에는 포크가 있을 때, 배가 고프면 양손에 포크를 집어서 먹야아 함

-> 누군가 포크를 사용중이면 다른 철학자는 먹을 수 없음

-> 자원 처리 문제

 

 

6. 동기하는 메서드 사이 존재하는 의존성 이해

** 공유 객체 하나에는 메서드 하나만 사용 **

 

- 동기화하는 메서드 사이 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생김

 

- 자바 언어는 개별 메서드를 보호하는 synchronized 개념 지원

> 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 확인해야 함

 

- 공유 객체 하나에 여러 메서드가 필요한 상황 시 고려사항 세가지

1) 클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드 호출하기 전에 서버를 잠금, 마지막 메서드를 호출할 때까지 잠금을 유지

2) 서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다.

3) 연결 서버 - 잠금을 수행하는 중간 단계를 수행하는 중간 단계를 생성한다. '서버에서 잠금'방식과 유사하지만 원래 서버는 변경하지 않는다.

 

 

 

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

 

- 자바에서 synchronized 키워드를 사용하면 락을 설정함

> 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행 가능

 

- 락은 스레드를 지연시키고 부하를 가중시킴

 

- 임계영역은 반드시 보호해야 함

 

 

 

8. 올바른 종료 코드는 구현하기 어려움

** 종료 코드를 개발 초기부터 고민하고, 동작하게 초기부터 구현해야 함 **

 

- 영구적으로 돌아가는 시스템 구현과 잠시 돈 후 종료하는 시스템 구현 방법은 다름

 

 

 

9. 스레드 코드 테스트

** 문제를 노출하는 테스트 케이스 작성, 프로그램 설정과 시스템 설정 부하를 바꿔가며 자주 돌려야 함, 실패 시 원인을 추척하라 **

 

- 테스트가 정확성을 보장하지는 않지만, 충분한 테스트는 위험을 낮춤

 

- 고려할 사항이 많음

1) 말이 안되는 실패 = 잠정적인 스레드 문제

> 시스템 실패를 일회성으로 치부하지 말아라

 

2) 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌아야 함

> 스레드가 호출하는 POJO를 만만들어서 스레드 환경 밖에서 테스트

> 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅X

 

3) 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워넣을 수 있어야 함, 상황에 맞춰 조정하게 작성

> 다양한 설정에서 실행할 목적으로 다른 환경에 넣을 수 있는 코드 구현

 

4) 프로세서 수보다 많은 스레드, 다른 플랫폼에서 돌려보기

 

5) 코드에 보조코드를 넣어 돌려서 강제로 실패 해보기

> 코드가 실행되는 수천 가지 경로 중 아주 소수만 실패 -> 버그를 찾기가 어려움

> 보조 코드를 추가해 실행되는 순서 바꿔보기 ex. Object.wait(), Object.slepp() ...

 

- 스레드 순서에 따라 성공 영향을 미침

 

- 방법: 직접 구현, 자동화

 

보조코드

▶️ 직접 구현하기

- wait(), sleep(), yiel(), priority() 함수 추가

 

- 배포 환경이 아니라 테스트 환경에서 보조 코드를 실행할 방법이 필요

 

- 스레드를 전혀 모르는 POJO와 스레드를 제어하는 클래스로 프로그램을 분활하면 보조 코드를 추가할 위치를 찾기 쉬워짐

 

 

▶️ 자동화

- AOP(Aspect-Oriented Framwork), CGLIB, ASM

 

- 다양한 위치에 ThreadJigglePoint.jiggle() 호출

> 무작위로 sleep이나 yield, nop 호출

 

- 코드를 흔드는(jiggle) 것이 중요

 

 

 

결론

1. 다중 스레드는 올바로 구현하기 어려움

 

2. SRP 준수 -> POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리

 

3. 동시성 오류를 일으키는 잠정적 원인 이해

 

4. 사용하는 라이브러리와 기본 알고리즘 이해

 

5. 보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법

 

6. 테스트 용이성 TDD 규칙 따르기

'IT > ' 카테고리의 다른 글

[Clean Code] 15장, 16장, 코드 리팩토링 해보기  (0) 2021.11.19
[Clean Code] 14장, 점진적인 개선  (0) 2021.11.14
[Clean Code] 12장, 창발성  (0) 2021.11.13
[Clean Code] 11장, 시스템  (0) 2021.11.07
[Clean Code] 10장, 클래스  (0) 2021.11.07