본문 바로가기

IT Book Summary/Clean Code

Chapter 13, 14

13 동시성 Concurrency

객체는 처리의 추상화다. 스레드는 일정의 추상화다.

- 제임스 O.코플리엔 

 

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

 

동시성이 필요한 이유?

동시성은 결합 coupling 을 없애는 전략 : 무엇 what 과 언제 when 을 분리하는 전략.

 

서블릿 servlet 모델. 서블릿은 웹 호은 EJB 컨테이너 에서 돌아가는데 동시성을 부분적으로 관리함.

웹요청이 들어올때마다 웹서버는 비동기식으로 서블릿을 실행.

 

동시성이 필요한 상황이 존재.

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

 

동시성 방어 원칙

단일 책임 원칙 SRP

주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙.

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

 

자료 범위를 제한하라

공유객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호하라고 권장.

임계영역의 수를 줄이는 기술이 중요.

자료를 캡슐화 하라. 공유자료를 최대한 줄여라.

 

자료 사본을 사용하라

각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능.

 

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

다른 스레드와 자료를 공유가지 않는다.

스레드는 클라이언트 요청 하나를 처리.

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

 

라이브러리를 이해하라

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

java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 안전.

ConcurrentHashMap 은 HashMap 보다 빠르고 다중 스레드 상에서 안전한 메서드로 제공.

ex) ReentrantLock, Semaphore,CountDownLatch .. 

 

실행 모델을 이해하라

한정된 자원 다중 스레드 환경에서 사용하는 자원
상호 배제 한번에 한 스레드만 공유자료나 공유자원을 사용할 수 있는 경우
기아 Starvation 스레드가 오랫동안 자원을 기다림.
데드락 Deadlock 여러 스레드가 서로 끝나기를 기다림. 모든 스레드가 각자 필요한 자원을 다른 스레드가 점유함.
라이브락 Livelock 락을 거는 단계에서 각 스레드가 서로를 방해.

 

생산자-소비자

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

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

대기열을 올바로 사용하고자 생산자와 소비자 스레드는 서로에게 시그널을 보내는데,

잘못하면 동시에 서로에게 시그널을 기다릴 가능성이 존재.

 

읽기 -쓰기

처리율 throughput 이 문제의 핵심.

대개 쓰기 스레드가 버퍼를 오랫동안 점유하면 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어짐.

양쪽 균형을 잡으면서 동시 갱신 문제를 피하는 해법이 필요.

 

식사하는 철학자들

애플리케이션은 여러 프로세스가 자원을 얻으려 경쟁.

 

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

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

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

 

  • 클라이언트에서 잠금
    클라이언트에서 첫번째 메서드를 호출하기전 서버를 잠금. 마지막 메서드를 호출할 때까지 잠금을 유지.
  • 서버에서 잠금
    서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하느 메서드를 구현.
  • 연결 서버 잠금을 수행하는 중간단계를 생성.

 

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

synchronized 문을 남발하는 코드는 바람직하지 않음.

임계영역 수를 최대한 줄여야 함.

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

 

올바른 종료 코드는 구현하기 어렵다

종료 코드를 개발 초기붙 고민하고 동작하게 초기부터 구현하라.

어려우므로 이미 나온 알고리즘을 검토.

 

스레드 코드 테스트하기

스레드가 하나인 프로그램은 충분한 테스트로 위험을 낮출수 있으나 
같은 코드와 같은 자원을 사용하는 스레드가 둘 이상으로 늘어나면 복잡해짐.

 

문제를 노출하는 테스트 케이스를 작성하고, 프로그램 설정과 시스템 설정과 부하를 바꾸면 자주 돌려야 함.

 

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

 

결론

 

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

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

 

동시성 오류를 일으끼는 잠정적 원인을 철저히 이해.

사용 라이브러리와 기본 알고리즘을 이해.

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

공유하는 정보와 공유하지 않는 정보를 제대로 이해.

 

일회성 문제는 시스템 부하나 예상치 않게 발생하므로

많은 플랫폼에서 많은 설정으로 반복해서 계속 테스트 해야함.

스레드 코드는 출시하기 전 최대한 오랫동안 돌려볼 것.

 


14 점진적인 개선

- 명령행 인수 구문분석기 사례 연구

 

점진적 개선을 보여주는 사례.

확장성이 부족했던 모듈을 소개하고 그것을 정리하는 단계를 살펴봄.

 

Args 생성자에 인수 문자열과 형식문자열을 넘겨 Args 인스턴스 생성 후 

인스턴스에 인수값을 질의

public static void main(String[] args) { 
  try {
    Args arg = new Args("l,p#,d*", args); 
    boolean logging = arg.getBoolean('l');
    int port = arg.getInt('p');
    String directory = arg.getString('d'); 
    executeApplication(logging, port, directory);
  } catch (ArgsException e) {
    System.out.printf("Argument error: %s\n", e.errorMessage());
  } 
}

 

형식문자열이나 명령행 인수에 문제가 있으면 ArgsException 발생.

 

Args 구현

 

ArgumentMarshaler 정의 인터페이스와 파생클래스를 주의깊게 볼 것.

 

깨끗한 코드를 짜려면 지저분한 코드를 짠위 저리해야 함.

일단 돌아가는 프로그램을 목표로 잡는건 초보 프로그래머 대다수의 실수.

초안을 만들고 1차 초안, 2차 초안을 거쳐 점진적으로 개선해야 깔끔한 코드를 짤수 있음.

 

Args: 1차 초안

 

추갛라 인수 유형이 늘어나면서 코드는 나빠지기 시작하고 

이때 리팩터링을 시작.

 

개선하려고 구조를 계속 뒤집는건 프로그램을 망치게 된다.

TDD 방식으로 코드를 고치더라도 시스템이 항상 돌아가게 해야함.

 

String 인수

 

모든 논리를 ArgumentMarchaler로 옮긴후 파생클래스로 기능을 분산.

계속해서 Boolean, String, Integer 인수 유형에 동일하게

set, get 을 옮긴 후 사용하지 않은 함수를 제거하고 변수를 옮김.

테스트는 계속 진행함.

목표는 맵을 사용하는 코드를 제거하는 것.

구조에 새로운 인수 유형을 추가하기 쉽게 만드는 것.

오류처리 동작도 만들어야 함.

 

리팩터링은 루빅 큐브 맞추기와 비슷하다.

큰 목표 하나를 이루기 위해 자잘한 단계를 수없이 거치고 

각 단계를 거쳐야 다음 단계가 가능하다.

 

소프트웨어 설계느 분할만 잘해도 품질이 크게 높아진다.

적절한 장소 만들어 관심사, 코드 분리.

 

결론

 

그저 돌아가는 코드는 그 무게가 늘어나 나중에 발목을 잡는다.

나중에 고치려면 엄청난 비용이 든다.

아직 마음에 짐으로 남아있는 나쁜코드는 방치하지 말고 빠른 시일안에 정리하자.

 

 

 

 

 

'IT Book Summary > Clean Code' 카테고리의 다른 글

Chapter 17  (0) 2021.07.04
Chapter 15, 16  (0) 2021.06.27
Chapter 11, 12  (0) 2021.06.13
Chapter 09, 10  (0) 2021.06.07