From cf637cd672191c38c495b64c3e98ee0c85c99873 Mon Sep 17 00:00:00 2001 From: "now.water" Date: Thu, 26 May 2022 01:26:26 +0900 Subject: [PATCH] =?UTF-8?q?cleancode=2012,=2013=EC=9E=A5=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/books/cleancode/12.md | 104 ++++++++++ content/books/cleancode/13.md | 358 ++++++++++++++++++++++++++++++++++ 2 files changed, 462 insertions(+) create mode 100644 content/books/cleancode/12.md create mode 100644 content/books/cleancode/13.md diff --git a/content/books/cleancode/12.md b/content/books/cleancode/12.md new file mode 100644 index 0000000..96132b1 --- /dev/null +++ b/content/books/cleancode/12.md @@ -0,0 +1,104 @@ +--- +title: '12장. 창발성' +metaTitle: '만렙 개발자 키우기' +order: 11 +tags: ['Book'] +date: '2022-05-25' +--- + +## 단순한 설계 규칙 1. 모든 테스트를 실행하라 + +> 설계는 의도한 대로 돌아가는 시스템을 내놓아야 한다. + +문서로는 시스템을 완벽하게 설계했지만, 시스템이 의도한 대로 돌아가는지 검증할 간단한 방법이 없다면 문서 작성을 위해 투자한 노력에 대한 가치는 인정받기 힘들다. + +테스트가 불가능한 시스템은 검증도 불가능하다. 이러한 검증 불가 시스템은 절대 출시하면 안된다. + +테스트가 (쉽게) 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다. + +- 크기가 작고 하나의 목적만을 수행하는 클래스 + +- SRP 를 준수하는 클래스 + +테스트가 많을수록 개발자는 테스트가 쉬워지게 하는 코드를 작성하는데, 이렇게 철저한 테스트가 가능한 시스템을 만들면 더 나은 설계가 얻어진다. + +
+ +결합도가 높으면 테스트 케이스를 작성하기 어렵다. + +그러므로 DIP, 의존성 주입, 인터페이스, 추상화 등의 도구를 사용해 결합도를 낮추면 +테스트 케이스를 작성하기 쉽게 만들고, 테스트 케이스도 많이 작성할 수 있다. + +따라서 설계 품질은 더욱 높아진다. + +단순히 "테스트 케이스를 만들고 계속 돌려라" 라는 규칙을 따르면, 시스템은 낮은 결합도와 높은 응집력을 갖게 된다. + +--- + +## 단순한 설계 규칙 2. 리팩터링 + +테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리한다. 구체적으로는 코드를 점진적으로 리팩터링 해나간다. + +### (1) 중복 없애기 + +깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요 + +공통 코드를 새 메서드로 추출하고, 적절한 위치의 클래스로 옮겨줄 수 있다. + +그러면 새 메서드는 가시성이 높아지고, 다른 맥락에서 재사용할 기회를 가질 수 있다. + +이러한 소규모 재사용은 시스템 복잡도를 극적으로 줄여준다. + +> ex. `템플릿 매서드 패턴` - 고차원 중복을 제거할 목적으로 자주 사용하는 기법 +> +> 하위 클래스는 중복되지 않는 정보만 제공해 `accrueVacation` 알고리즘에서 빠진 부분을 채워 넣는다. +> +> ```kotlin +> abstract class VacationPolicy { +> fun accrueVacation() { +> calculateBaseVacationHours() +> alterForLegalMinimums() +> applyToPayroll() +> } +> +> private fun calculateBaseVacationHours() { /* 공통 코드 구현 */ } +> protected abstract fun alterForLegalMinimums() // 상속 클래스마다 코드 구현 +> private fun applyToPayroll() { /* 공통 코드 구현 */ } +> } +> ``` + +### (2) 표현하라 + +코드는 개발자의 의도를 분명히 표현해야 한다. + +명백한 코드일수록 다른 사람이 그 코드를 이해하기 쉬워지고, 그래야 결함이 줄어들고 유지보수 비용이 적게 든다. + +- 좋은 이름 선택하기 + +- 함수와 클래스 크기를 가능한 줄이기 + +- 표준 명칭을 사용하기 (ex. 특정 디자인 패턴이 사용되면, 클래스 이름에 패턴 이름을 넣기) + +- 단위 테스트 케이스를 꼼꼼히 작성 + + - 테스트 케이스는 예제로 보여주는 문서 + + - 잘 만든 테스트 케이스를 읽으면 클래스 기능이 한 눈에 들어온다. + +
+ +표현력을 높이는 가장 중요한 방법은 노력. 나중에 읽을 사람을 고려해 조금이라도 읽기 쉽게 만들려는 충분한 고민이 필요하다. + +### (3) 클래스와 메서드 수 최소로 줄이기 + +함수와 클래스 수를 가능한 줄이는 것을 권장. 그러나 기본적인 개념도 극단으로 치달으면 득보다 실이 많아진다. + +가능한 독단적인 견해는 멀리하고, 실용적인 방식을 택하자. + +--- + +## 결론 + +**우선순위** + +테스트 케이스 작성 > 중복 줄이기 > 의도 표현하기 > 함수, 클래스 수 줄이기 diff --git a/content/books/cleancode/13.md b/content/books/cleancode/13.md new file mode 100644 index 0000000..0daf97c --- /dev/null +++ b/content/books/cleancode/13.md @@ -0,0 +1,358 @@ +--- +title: '13장. 동시성' +metaTitle: '만렙 개발자 키우기' +order: 12 +tags: ['Book'] +date: '2022-05-25' +--- + +## 동시성이 필요한 이유 + +동시성은 결합을 없애는 전략. 즉, `무엇`과 `언제`를 분리하는 전략이다. + +스레드가 하나인 프로그램은 `무엇`과 `언제`가 서로 밀접하다. + +구조적 관점에서 프로그램은 거대한 루프 하나가 아니라, 작은 협력 프로그램 여럿으로 구성되어 있어 `무엇`과 `언제`를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다. + +따라서 시스템을 이해하기 쉽고 문제를 분리하기도 쉬워진다. + +### **장점** + +- 구조적 개선 + +- 응답 시간과 작업 처리량 개선 + +### **단점** + +- 다소 부하를 유발한다. + + - 성능 측면에서 부하. + - 더 많은 코드 작성 + +- 복잡하다. + +- 일반적으로 동시성 버그는 재현하기 어렵다. + +- 근본적인 설계 전략을 재고해야 한다. + +--- + +## 난관 + +여러 스레드가 특정 메서드를 실행하는 잠재적인 경로는 굉장히 많이 존재할 수 있다. + +물론 대다수 경로는 올바른 결과를 내놓지만, 문제는 잘못된 결과를 내놓은 일부 경로이다. + +이것이 동시성을 구현하기 어려운 이유이다. + +--- + +## 동시성 방어 원칙 + +동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술 + +### 단일 책임 원칙 + +> 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙 + +동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하므로, **동시성과 관련된 코드는 다른 코드와 분리해야 한다.** + +### 따름 정리: 자료 범위를 제한하라 + +> 자료를 캡슐화 하고, 공유 자료를 최대한 줄여라. + +공유 객체를 사용하는 코드 내 임계 영역 키워드(synchronized)로 보호하라. + +이런 임계영역의 수를 줄이는 기술이 중요한데, 공유 자료를 수정하는 위치가 많을수록 다음 가능성도 커진다. + +- 보호할 임계영역을 빼먹어서 공유 자료를 수정하는 모든 코드를 망가뜨린다. + +- 모든 임계영역을 올바로 보호했는지(DRY 위반 - Don't Repeat Yourself) 확인하느라 똑같은 노력과 수고를 반복한다. + +- 그렇지 않아도 찾아내기 어려운 버그가 더욱 찾기 어려워진다. + +### 따름 정리: 자료 사본을 사용하라 + +> 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다. + +공유 객체를 피하는 방법이 있다면, 코드가 문제를 일으킬 가능성도 아주 낮아진다. + +사본으로 동기화를 피함으로써 `내부 잠금을 없애 절약한 수행 시간`이 `사본 생성과 가비지 컬렉션에 드는 부하`를 상쇄할 가능성이 크다. + +**방안** + +- 객체를 복사해 읽기 전용으로 사용하기 + +- 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오기 + + +### 따름 정리: 스레드는 가능한 독립적으로 구현하라 + +> 스레드는 가능하면 독립적으로 구현하라. +> +> 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라. + +각 스레드는 클라이언트 요청 하나를 처리. 모든 정보는 비공유 출처에서 가져오고, 로컬 변수에 저장 + +그러면 다른 스레드와 동기화할 필요가 없어진다. + +--- + +## 라이브러리를 이해하라 + +- 스레드 환경에 안전한 컬렉션 사용 + +- 가능하다면 스레드가 차단(block)되지 않는 방법을 사용 + +자바 기준 + +- `ConcurrentHashMap` : 동시 읽기/쓰기 지원 + +- `ReentrantLock` : 한 메서드에서 잠그고, 다른 메서드에서 푸는 락 + +- `Semaphore` : 전형적인 세마포어. 개수가 있는 락 + +- `CountDownLatch` : 지정한 수만큼 이벤트 발생 후 대기 중인 스레드를 모두 해제하는 락 -> 모든 스레드에게 동시에 공평하게 시작할 기회 + +--- + +## 실행 모델을 이해하라 + +기본 용어 중, 라이브락(Livelock) 이라는 것이 있는데 처음 보는 단어이다. + +> **라이브락** +> +> 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명(reasonance)으로 인해 굉장히 오랫동안 혹은 영원히 진행하지 못한다. +> +> 더 쉽게 생각하면, 갑이 A 를 가진 상태에서 B 를 얻으려 하나, B 는 을이 가지고 있어서 데드락을 피하기 위해 A 를 포기. 을도 마찬가지로 B 를 가진 상태에서 A 를 얻으려 하나, A 는 갑이 가지고 있어서 데드락을 피하기 위해 B 를 포기 +> +> 결국 갑과 을 모두 A 와 B 중 하나만 계속해서 획득하고 해제하는 상황을 반복하게 됨 + +### 생산자-소비자 + +하나 이상 생산자 스레드가 정보를 생성해, 버퍼나 대기열에 넣는다. + +하나 이상의 소비자 스레드가 대기열에서 정보를 가져와 사용한다. + +
+ +생산자 스레드는 대기열에 정보를 채운 다음 소비자 스레드에게 "대기열에 정보가 있다"는 시그널을 보낸다. + +소비자 스레드는 대기열에서 정보를 읽어들인 후 "대기열에 빈 공간이 있다"는 시그널을 보낸다. + +따라서 잘못하면 생산자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다. + +### 읽기-쓰기 + +쓰기 스레드가 버퍼 갱신 도중에 읽기 스레드가 버퍼를 읽지 않게, + +읽기 쓰레드가 버퍼를 읽는 동안 쓰기 스레드가 버퍼를 갱신하지 않게, + +복잡한 균형잡기가 필요하다. + +대개 쓰기 스레드가 버퍼를 오랫동안 점유하는 바람에 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어짐 + +양쪽 균형을 잘 잡아서 동시 갱신 문제를 피하는 해법이 필요. + +--- + +## 동기화하는 메서드 사이에 존재하는 의존성을 이해하라 + +동기화하는 메서드 간 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다. + +자바에서는 개별 메서드를 보호하는 `synchronized` 라는 개념을 사용할 수 있다. + +
+ +> 공유 객체 하나에는 메서드 하나만 사용하라 + +공유 객체 하나에 여러 메서드가 필요한 상황도 생길 수 있다. + +그럴 때는 다음 세 가지 방법을 고려 + +**1. 클라이언트에서 잠금** + +- 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠궈서 마지막 메서드를 호출할 때 까지 잠금 유지 + + +**2. 서버에서 잠금** + +- 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드 구현. + +- 클라이언트는 이 메서드를 호출 + + +**3. 연결 서버** + +- 잠금을 수행하는 중간 단계를 생성 + +- "서버에서 잠금" 방식과 유사하지만, 원래 서버는 변경하지 않는다는 차이 존재 + +--- + +## 동기화하는 부분을 작게 만들어라 + +자바에서 `synchronized` 키워드를 사용하면 락을 설정하고, 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행 가능 + +락은 스레드를 지연시키고 부하를 가중시킨다. 따라서 여기저기 남발하는 코드는 바람직하지 않다. + +
+ +**반면 임계영역은 반드시 보호해야 한다. 따라서 코드를 짤 때는 임계영역 수를 최대한 줄여야 한다.** + +필요 이상으로 임계영역 크기를 키우면 스레드 간 경쟁이 늘어나고 프로그램 성능이 떨어진다. + +--- + +## 올바른 종료 코드는 구현하기 어렵다. + +- 종료 코드를 개발 초기부터 고민하고 동작하게 구현하라 + +- 생각보다 어렵고 오래 걸리므로, 이미 나온 알고리즘을 검토하라 + + +가장 흔히 발생하는 문제가 데드락 -> 절대 오지 않을 시그널을 기다림 + +--- + +## 스레드 코드 테스트하기 + +코드가 올바르다고 증명하기에 테스트가 정확성을 보장하지는 않는다. + +그럼에도 충분한 테스트는 위험을 낮춘다. + +그러므로 `문제를 노출하는 테스트 케이스를 작성`하고, `프로그램 설정`과 `시스템 설정`과 `부하`를 바꿔가며 자주 돌려라. + +테스트가 실패하면 원인을 추적한다. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안된다. + +그러한 상황은 고려할 사항이 아주 많다는 뜻이므로, 아래의 구체적인 지침을 따르자 + +### 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라 + +다중 스레드 코드는 때떄로 말이 안되는 오류를 일으킨다. + +대다수 개발자는 스레드가 다른 코드와 교류하는 방식을 직관적으로 이해하지 못한다. 또, 실패를 재현하기가 아주 어렵다. + +그래서 많은 개발자가 단순 일회성 문제로 치부하고 무시하는데, 이러한 문제가 계속 무시된다면 잘못된 코드 위에 코드가 계속 쌓인다. + +따라서 시스템 실패를 일회성이라 치부하지 말자. + +### 다중 스레드를 고려하지 않은 순차코드부터 제대로 돌게 만들자 + +> 스레드 환경 안/밖 에서 생기는 버그를 동시에 디버깅 하지 말라. + +스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인 + +일반적인 방법으로, 스레드가 호출하는 POJO 를 만들면 POJO 는 스레드를 모르기 때문에 스레드 환경 밖에서 테스트가 가능하다. + +POJO 에 넣는 코드는 많을수록 더 좋다. + +### 다중 스레드 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 구현 + +> 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라 + +- 실행 중 스레드 수 바꿔보기 + +- 실제 환경/테스트 환경 에서 돌려보기 + +- 테스트 코드를 다양한 속도로 빨리, 천천히 돌려보기 + +- 반복 테스트 가능하게 테스트 케이스 작성하기 + +### 다중 스레드 코드 부분을 상황에 맞게 조율할 수 있게 작성하라 + +적절한 스레드 개수를 파악하려면 상당한 시행착오 필요 + +처음부터 다양한 설정으로 프로그램의 성능 측정 방법을 강구하기 위해, 스레드 개수를 쉽게 조율할 수 있는 코드를 구현하라 + +### 프로세서 수보다 많은 스레드를 돌려보라 + +시스템이 스레드를 스와핑할 때도 문제가 발생하므로, 스와핑이 발생할 수 있게 프로세서 수보다 많은 스레드를 돌려보라 + +스와핑이 잦을수록 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다. + +### 다른 플랫폼에서 돌려보라 + +다중 스레드 코드는 플랫폼에 따라 다르게 돌아간다. + +따라서 코드가 돌아갈 가능성이 있는 플랫폼 전부에서 테스트를 수행해야 마땅하다. + +### 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라 + +흔히 스레드 코드는 오류를 찾기 쉽지 않다. 간단한 테스트로는 버그가 드러나지 않는다. + +스레드 버그가 재현이 어려운 이유는 코드가 실행되는 수천 가지 경로 중 아주 소수만 실패하기 때문 + +이때 보조 코드를 추가해 코드가 실행되는 순서를 바꿔주면 버그가 드러날 가능성이 높아진다. + +
+ +**보조 코드 추가 방법** + +**1. 직접 구현하기** + +코드에 직접 `wait()`, `sleep()`, `yield()`, `priority()` 함수를 추가. + +특별히 까다로운 코드를 테스트할 때 적합 + +배포 환경이 아니라 테스트 환경에서 보조 코드를 실행할 방법이 필요 + +스레드를 전혀 모르는 POJO 와 스레드를 제어하는 클래스로 프로그램을 분할하면 보조 코드를 추가할 위치를 찾기 쉬워진다. + +**2. 자동화** + +AOF, CGLIB, ASM 등과 같은 도구를 사용 + +
+ +이렇게 코드를 흔드는 이유는 스레드를 매번 다른 순서로 실행하기 위해서다. + +좋은 테스트 케이스와 흔들기 기법(jiggling)은 오류가 드러날 확률을 크게 높여준다. + +--- + +## 결론 + +다중 스레드 코드를 작성한다면 각별히 깨끗하게 코드를 짜야 한다. 주의하지 않으면 희귀하고 오묘한 오류에 직면 + +깔끔한 접근 방식을 취한다면 코드가 올바로 돌아갈 가능성이 극적으로 높아진다. + +### 무엇보다 먼저 SRP 를 준수한다. + +POJO 를 사용해 `스레드를 아는 코드`와 `스레드를 모르는 코드`를 분리한다. + +스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다. + +스레드 코드는 최대한 집약되고 작아야 함 + +### 동시성 오류의 잠정적 원인을 철저히 이해한다. + +여러 스레드가 공유 자료 조작 or 자원 풀 공유 시 동시성 오류 발생 + +루프 반복 끝내거나, 프로그램을 깔끔하게 종료하는 등 경계 조건의 경우 까다로우므로 특히 주의 + +### 사용하는 라이브러리와 기본 알고리즘 이해 + +특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악 + +### 보호할 코드 영역을 찾는 방법과 특정 코드영역을 잠그는 방법 이해 + +잠글 필요가 없는 코드는 잠그지 않는다. + +잠긴 영역에서 다른 잠긴 영역을 호출하지 않는다. + +클라이언트에게 공유 상태를 관리하는 책임을 떠넘기지 않는다. + +**공유하는 객체 수와 범위를 최대한 줄인다.** + +### 많은 플랫폼에서 많은 설정으로 반복 테스트 + +### 시간을 들여 보조 코드 추가 + +오류가 드러날 가능성이 크게 높아진다. + +스레드 코드는 출시 전까지 최대한 오래 돌려봐야 한다. + +직접 구현 or 자동화 기술을 사용하자 + +