Group Study (2021-2022)/Clean Code

[클린코드 북리뷰 스터디] 6주차 - 창발성과 동시성

angieveloper 2022. 7. 1. 21:50

⛳️ 9. 창발성

목차

  1. 모튼 테스트를 실행한다
  2. 중복을 없앤다
  3. 프로그래머의 의도를 표현한다
  4. 클래스와 메서드 수를 최소로 줄인다.

 

창발적 설계란?

  • 창발성 (Emergence)
    • 하위 계층에는 없는 특성이나 행동이 상위 계층(전체 구조)에서 자발적으로 돌연히 출연하는 현상
    • 작은 요소들의 상호작용의 반복이 전체 구조에 영향을 미친다
    • ex. 개미의 집짓기
  • 창발적 설계
    • 단순한 4가지 단계를 반복하다보면 전체적으로 깨끗한 코드가 만들어진다.

 

1. 모든 테스트를 실행하라

  • 설계를 의도한 대로 돌아가는 시스템을 내놓아야 한다
  • 테스트를 철저히 거쳐 모든 테스트 케이스를 항상 통과하는 시스템 == 테스트가 가능한 시스템
    • 테스트가 불가능한 시스템 👉 검증 불가능 👉 절대 출시 ❌
    • 설계 품질이 더불어 높아짐
  • 테스트가 가능한 시스템을 만드려고 할수록 설계 품질 UP
    • 크기가 작고 목적 하나만 수행하는 클래스
    • 결합도 낮춤
    • DIP와 같은 원칙 적용
    • 의존성 주입
    • 인터페이스, 추상화 등 도구 사용
  • 객체 지향 방법론이 지향하는 목표 달성 가능 👉 낮은 결합도 & 높은 응집력

 

2. 리팩터링

  • 기존의 코드 최대한 재활용
  • 테스트 케이스를 모두 작성했으면 코드 클래스 정리 가능
  • 테스트 케이스가 있으면 리팩터링 과정에서 시스템이 깨질 걱정은 안 해도 된다.
  • 중복을 없애라
    • 중복 : 추가 작업, 추가 위험, 불필요한 복잡도
    • 구현 중복도 중복이다
    • 소규모 재사용은 시스템 복잡도를 줄여줌
    • TEMPLATE METHOD : 알고리즘의 구조를 상위 클래스의 메서드에서 정의하고, 하위 클래스에서 알맞게 세부적으로 정의
      • 알고리즘에 일정한 단계가 있고 세부적인 단계마다 조금씩 구현이 다를 때 사용
      • 고차원 중복 제거 목적으로 사용
int size;
boolean isEmpty() {}

// 👇

boolean isEmpty() {
	return this.size()==0;
}

 

3. 표현하라

개발자의 의도를 분명히 표현하라
  1. 좋은 이름을 선택한다.
  2. 함수와 클래스 크기를 가능한 줄인다 - 작은 클래스와 함수는 이름도 짓기 쉽다
  3. 표준 명칭을 사용한다.
  4. 디자인 패턴을 사용하면 그 이름을 클래스에 넣어주자
  5. 단위 테스트 케이스를 꼼꼼히 작성한다.
  6. 더 읽기 쉽게 만들기 위해 노력하자.

결함이 줄어들고 유지보수 비용이 적게 든다

 

4. 클래스와 메서드 수를 최소로 줄여라

  • 함수와 클래스 수를 가능한 줄이자
  • 무의미하고 독단적인 정책은 지양하고, 실용적인 방식을 택하자
  • 클래스 크기를 작게 유지하고, 시스템 크기를 작게 유지하자는 목표 - 가장 낮은 우선순위
    • 여러가지 규칙에 극단적으로 심취해 클래스와 메서드를 무수히 만들지 말자
  • 실용적 관점에서 타협하자

예) 의존성 역전 원칙

  • 당장 필요하지 않다면 or 확장 유연성이 필요하지 않다면 👉 과한 설계가 될 수 있음
  • 만약 변경이 발생하면? 👉 그때 확장하는 설계를 새로 하면 된다
    • 거의 발생하지 않을 미래의 일에 투자하는 비용이 더 크다

 


 

⛳️ 10. 동시성

목차

  1. 동시성이 필요한 이유
  2. 여러 스레드를 돌리는 어려움
  3. 안전한 동시성 프로그래밍 규칙

 

동시성 프로그래밍이란? 

동시성과 병렬

출처 : https://kwahome.medium.com/concurrency-is-not-parallelism-a5451d1cde8d

  • 어플리케이션을 효율적으로 실행하기 위해 멀티코어를 온전히 활용하도록 구현하는 방식
  • 외부 서비스의 응답을 기다리면서 아무 일도 하지 않으면 CPU 사이클이 낭비된다 👉 낭비되는 자원을 줄이자
  • 동시성 & 병렬성 구현
    • 언어 레벨에서 하드웨어의 멀티코어를 적절하게 사용하도록 지원 - 동시성만 잘 신경써서 개발하면 된다
    • 클라이언트가 아닌 어플리케이션 관점
      • 내 어플리케이션의 효율성을 높여야 한다
      • 내 어플리케이션이 돌아가는 머신의 환경이 효율적으로 돌아가야 한다.

 

1. 동시성이 필요한 이유

  • 동시성 = 결합을 없애는 전략
  • 무엇과 언제를 분리
    • 애플리케이션 구조와 효율이 극적으로 나아진다
동시성 채택의 이유
  1. 구조적 개선
  2. 응답 시간, 작업 처리량 개선 요구사항
동시성에 대한 오해
  1. 동시성은 항상 성능을 높여준다 👉 동시성은 때로 성능을 높여준다
    • 여러 스레드가 프로세서 공유 가능, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 높아진다
    • Java ) Servlet
      • 하나의 서블릿에 여러 thread가 접근 가능
        • 멤버 변수들은 Not Thread Safe
        • Thread Safe한 자료구조도 일부 상황에서는 안전하지 않을 수 있음
        • 어떤 상황까지 Thread Safe한 지 잘 이해해야 함
  2. 동시성을 구현해도 설계는 변하지 않는다 👉 동시성을 구현하면 설계를 바꿔야 한다.
  3. Web이나 EJB와 같은 컨테이너를 사용하면 동시성을 이해할 필요가 없다 👉 컨테이너를 사용해도 동시성을 이해해야 한다
    • 어플리케이션이 컨테이너를 통해 멀티 쓰레드를 사용하는 것 - 컨테이너의 동작을 이해해야 함
    • 동시수정, 데드락과 같은 문제를 피할 수 있는지를 알아야 한다
동시성에 대한 진실
  1. 동시성은 다소 부하를 유발한다
  2. 동시성은 복잡하다
  3. 동시성으로 인한 버그는 재현하기 어렵다
  4. 근본적인 설계 전략을 재고해야 한다

 

2. 여러 스레드를 돌리는 어려움

  • 잠재적인 경로가 무수히 많다.
  • 대다수 경로는 올바른 결과를 내놓고 일부가 잘못된 결과를 내놓는다

 

3. 안전한 동시성 프로그래밍 원칙

  1. 단일 책임 원칙
    • 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다
      • 동시성 관련 코드는 다른 코드와 분리하라
        • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다
        • 동시성 코드는 독자적인 난관이 있다. 다른 코드에서 겪는 난관보다 훨씬 어렵다.
        • 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다
  2. 자료 범위를 제한하라
    • 객체 하나를 공유하는 두 세르다 - 서로 간섭해서 예상치 못한 결과 발생
    • 객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호
    • 동시 수정 문제로부터 피하기 위함
    • 이런 임계영역의 수를 최소하
      • 보호할 임계영역을 빼거나 모든 임계영역을 보호했는지 확인하는 것도 시간적 비용 발생
  3. 자료 사본을 사용하라
  4. 공유 자본을 줄이려면 최대한 공유하지 않는 게 좋다
    • 객체를 복사해 읽기 전용으로 사용
    • 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져온다
    • 사본을 사용하는 방식으로 내부 잠금을 없애 수행시간을 절약하는 것이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다
  5. thread는 가능한 독립적으로 구현한다
  6. 다른 스레드와 자료를 공유하지 않는다
    • 각 thread는 클라이언트 요청 하나를 처리
    • 모든 자료는 비공유 출처(request)에서 가져오며 로컬 변수에 저장
    • 마치 독자적인 시스템에서 동작하는 것처럼 요청 처리
  1. 라이브러리를 이해하라
  2. 동기화하는 메서드 사이에 존재하는 의존성을 이해하라
  3. 공유 객체 하나에는 메서드 하나만 사용하라
    • 클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지
    • 서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 이 메서드를 호출한다.
    • 연결 서버 (Adapter) - 잠금을 수행하는 중간 단계를 생성한다. "서버에서 잠금" 방식과 유사하지만 원래 서버는 변경하지 않는다.
  4. 동시성 코드를 테스트 해야 한다.
    • 테스트를 했다고 올바르다고 증명하기는 불가능하다. 하지만 위험을 충분히 낮춘다
      • 문제를 노출하는 테스트 케이스를 작성하라
      • 프로그램의 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라
      • 테스트가 실패하면 원인을 추적하라
      • 다시 돌렸더니 통과한다는 이유로 그냥 넘어가면 안 된다.
    • 코드에 보조 코드를 넣어 돌려라
      • 드물게 발생하는 오류를 자주 발생하도록 보조 코드를 추가한다.
      • 직접 구현 : wait(), sleep(), yield(), priority() 함수를 추가
      • 도구 사용 : Thread.JigglePoint.jiggle() - 무작위로 sleep(), yield() 호출
    • 동시성 코드를 실제 환경/테스트 환경에서 돌려본다
      • 다양한 요청과 상황에 동시성 코드가 정상적으로 동작하는지 확인
        • 배포하기 전에 테스트 환경에서 충분히 오랜시간 검증
        • 동시성 코드를 배포한 후에 모니터링을 통해 문제가 발생하는지 지켜본다.