클린코드
Concurrency(동시성)
동시성이 필요한 이유
동시성은 결합을 없애는 전략이다. 즉 무엇과 언제를 분리하는 전략이다.
스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다. 그래서 호출스택을 살펴보면 프로그램 상태가 곧바로 드러난다.
단일 스레드를 디버깅하는 프로그래머는 정지점을 정한후 어느 정지점에 걸렸는지 보고 시스템 상태를 파악한다.
미신과 오해
일반적인 동시성의 미신과 오해
- 동시성은 항상 성능을 높혀준다. - 동시성은 때로 성능을 높여준다.
- 동시성을 구현해도 설계는 변하지 않는다. - 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
- 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다 - 실상 컨테이너가 어떻게 동작하는지 알아야 한다.
동시성과 관련된 타당한 생각
- 동시성은 다소 부하를 유발한다. 성능 측면에서 부하가 걸리며, 코드도 더 짜야 한다.
- 동시성은 복잡하다. 간단한 문제라도 동시성은 복잡하다.
- 일반적으로 동시성 버그는 재현하기 어렵다. 그래서 일회성 문제로 여겨 무쉬하기 쉽다.
- 동시성을 구현하려면 흔히 근본적인 설계 전략은 재고 해야 한다.
난관
바이트 코드를 처리하는 방식과 자바 메모리 모델이 원라로 간주하는 최소 단위를 알아야 한다.
동시성 방어 원칙
단일 책임원칙
주어진 메소드/클래스/컨포넌트를 변경할 이유가 하나여야 한다는 원칙
동시성을 구현할 때는 다음 몇가지를 고려한다.
- 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
- 동시성 코드에는 독자적인 난관이 있다.
- 잘못 구현한 동시성 코드는 별의 별 방식으로 실패한다.
권장사항: 동시성 코드는 다른 코드와 분리하라.
추론: 자료범위를 제한하라.
코드내 임계영역 synchronized 키워드로 보호하라
임계영역의 수가 많으면
- 보호할 임계영역을 빼먹는다.
- DRY 위반을 확인하느라 똑같은 노력과 수고를 반복한다.
- 안 그래도 찾아내기 어려운 버그가 더욱 찾기 어려워 진다.
권장사항 : 자료를 캡슐화 하라. 공유자료를 최대한 줄여라.
추론: 자료 사본을 사용하라
공유 자료를 줄이려면 처음 부터 공유하지 않는 방법이 제일 낫다.
어떤 경우에는 객체를 복사해 읽기 전용으로 사용하는 방법이 가능하다.
추론: 스레드는 가능한 독립적으로 구현하라
자신만의 세상에 존재하는 스레드를 구현한다. 즉 다른 스레드와 자료를 공유하지 않는다.
권장사항 : 독립적인 스레드로, 가능하면 다른 프로세서로 돌려도 괜찮도록 자료를 독집적인 단위로 분활하라.
라이브러리를 이해하라.
자바 5는 동시성 측면에서 이전 버전보다 많이 나아졌다.
- 스레드 환경에 안전합 집합 컬렉션을 사용한다.
- 서로 무관한 작업을 수행할때는 실행자 프레임워크를 사용한다.
- 가능하다면 스레드가 차단되지 않는 방법을 사용한다.
- 일부 클래스 라이브러리는 스레드에 안전하지 못하다.
스레드 환경에 안전한 집합 컬렉션
ConcurrentHashMap는 거의 모든 상황에서 HashMap 보다 성능이 좋다.
권장사항 : 언어가 제공하는 클래스를 검토하라.
실행 모델을 이해하라.
- 한정된 자원(Bound Resource) - 다중 스레드 환경에서 사용하는 자원으로 크기나 숫자가 제한적이다.(데이터 베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등)
- 상호 배재(Mutual Exclusion) - 한 번에 한 스레드만 공유 자료나 공유 자원을 활용할수 있는 경우를 가르킨다.
- 기아(Starvation) - 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다.(항상 짧은 스레드를 우선순위를 높히면 긴스레드가 기아상태에 빠진다. )
- 데드락(Deadlock) - 여러 스레드가 서로가 끝나길 기다린다.
- 라이브락(Livelock) - 락을 거는 단계에서 각 스레드가 서로를 방해한다.
생산자 - 소비자 (Producer-Consumer)
생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다.
잘못하면 생산자 스레드와 소비자 스레드가 둘다 진행 가능함에도 불구하고 동시에 서로에게 신호를 기다를 가능성이 있다.
읽기-쓰기 Readers-Writers
읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 이 공유 자원을 이따금 갱신한다고 하자
이런 경우 처리율 뮨제의 핵심이다.
양쪽이 균형을 잡으면서 동시 갱신을 피하는 해법이 필요하다.
식사하는 철학자들
둥근 식탁에 철학자 무리가 있다 각 철학자 왼쪽에는 포크가 놓였다 양손에 포크를 쥐지 않으면 먹지를 못한다.
각 알고리즘을 공부하고 해법을 구현해보라
동기화하는 메소드 사이에 의존성을 이해 하라
동기화 하는 메소드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.
공유 개체 하나에는 메소드 하나만 사용하자
공유 개체 하나에 여러 메소드가 필요한 상황도 생긴다. 그럴 때는 다음 세 가지 방법을 고려한다.
- 클라이언트에서 잠금 - 클라이언트에서 첫번째 메소드를 호출하기 전에 서버를 잠든다. 마지막 메소드 호출할 때까지 잠금을 유지한다.
- 서버에서 잠금 - 서버에다 서버를 잠그고 모든 메소드를 호출한후 잠금을 해제하는 메소드를 구현한다. 클라이언트는 새메소드를 호출한다.
- 연결 서버 - 잠금을 수행하는 중간 단계를 생성한다. 서버에서 잠금 방식과 유사하지만 원래 서버를 변경하지 않는다.
동기화하는 부분을 작게 만들어라
자바에서 synchronized 키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 한번에 스레드 하나만 실행 가능하다.
락은 스레드를 지연시키고 부하를 일으킨다. 여기저기 synchronized문을 남발하는 코드는 바람직하지 못하다.
동시에 임계영역은 반드시 보호 해야 한다.
임계영역 개수를 최대한 줄여야 한다. 임계영역 개수를 줄인다고 거대한 임계영역 하나로 구현하는 프로그래머가 있다.
필요이상 임계영역 크기를 키우면 스레드간에 경쟁이 늘어나고 프로그램 성능이 떨어진다.
동기화 하는 부분을 최대한 작게 만들자.
올바른 종료 코드는 구현하기 어렵다.
깔끔하게 종료하는 다중 스레드 코드를 짜야 한다면 시간을 들여서 올바로 구현하길 바란다.
권장 사항 : 종료 코드를 개발 초기에 생각하고 동작하게 구현하라. 생각 보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.
스레드 코드 테스트 하기
코드가 올바르다고 증명하기는 현실적으로 불가능하다. 테스트가 정확성을 보장하지 않는다.
권장 사항 : 문제를 노출하는 테스트 케이스를 작성하라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대 안된다.
구체적인 지침
- 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라.
- 다중 스레드를 고혀하지 않은 순차 코드부터 제대로 돌게 만들자.
- 다중 스레드를 쓰는 코드 부분을 쉽게 다양한 환경에 끼워 넣을수 있게 스레드 코드를 구현하라.
- 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조정할수 있게 작성하라.
- 프로세서 수보다 많은 스레드를 돌려보라.
- 다른 플랫폼에서 돌려보라.
- 코드에 보조 코드를 넣어서 돌려라. 강제로 실패를 일으키게 해보라.
말이 안되는 실패는 잠정적인 스레드 문제로 취급하라.
시스템 실패를 일회성이라 치부하지 마라.
다중 스레드를 고혀하지 않은 순차 코드부터 제대로 돌게 만들자.
스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅 하지마라.
먼저 스레드 환경 밖에서 코드를 올바로 돌려라.
다중 스레드를 쓰는 코드 부분을 쉽게 다양한 환경에 끼워 넣을수 있게 스레드 코드를 구현하라.
- 한 스레드로 실행하거나, 여러 스레드로 실행하거나, 실행중 스레드 수를 바꿔본다.
- 스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다.
- 테스트 코드를 빨리, 천천히, 다양한 속력으로 돌려본다.
- 반복해서 테스트가 가능하도록 테스트 케이스를 작성한다.
다중 스레드를 쓰는 코드 부분을 상황에 맞게 조정할수 있게 작성하라.
적절한 스레드 캐수를 파악하려면 상당한 시행착오가 필요하다. 스레드 개수를 변경하기 쉽도록 코드를 구현한다.
프로세서 수보다 많은 스레드를 돌려보라.
시스템이 스레드를 스와핑 할 때도 문제가 발생한다. 스와핑을 일으킬려면 프로세서 수보다 많은 스레드를 돌린다.
다른 플랫폼에서 돌려보라.
OS X이랑 원도우랑 돌렸더니 실패횟수가 틀렸다 잘못된 코드였지만 개발 플랫폼이 과 운영 목표 플랫폼 전부를 테스트 해야 한다.
코드에 보조 코드를 넣어서 돌려라. 강제로 실패를 일으키게 해보라.
간단한 테스트로는 버그가 드러나지 않는다. 스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 수천가지 경로 중에 아주 소수만 실패하기 때문
Object.wait(), Object.sleep(), Object.yield() 같은 메소드를 추가해서 실행 순서를 바꿔준다.
코드에 보조 코드를 추가하는 방법은 두가지다.
- 직접 구현하기
- 자동화
직접 구현하기
코드에다 직접 wait(), sleep(), yield(), priority() 함수를 추가한다.
이렇게 추가하면 실행 경로가 바뀐다. 만약 추가해서 실패하면 추가해서 실패가 아니라 원래 잘못된 코드이다.
이 방법의 문제점
- 보조 코드를 삽입할 적정 위치를 직접 찾아야 한다.
- 어떤 함수를 어디서 호출 해야 적당한지 어떻게 알까?
- 배포 환경에 보조 코드를 그대로 남겨두면 프로그램 성능이 떨어진다.
- 무작위적이다.
자동화
보조 코드를 자동으로 추가하려면 AOP, CGLIB, ASM등과 같은 도구를 사용한다.
이렇게 하면 코드에 수정 없이 원하는 위치에 값을 삽입 가능
흔들기 기법을 사용하여 오류를 찾아 내라.
결론
무엇보다 먼저 SRP를 준수한다. 스레드 코드와 스레드가 아닌 코드롤 분리한다. 스레드 코드를 테스트 할때 전적으로 스레드만 테스트 한다.