우당탕탕 우리네 개발생활

[클린코드] 12~13장 정리(창발성, 동시성) 본문

tech

[클린코드] 12~13장 정리(창발성, 동시성)

미스터카멜레온 2024. 6. 17. 22:54
<클린코드> 책을 통해 공부한 내용을 정리하기 위해 작성하였습니다. 제 개인적인 각색과 의견이 첨가되어 있어 실제 책의 내용과는 차이가 있을 수 있습니다.

12장. 창발성

- 리팩터링 단계에서는 소프트웨어 설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮다.

  • 응집도 높이기
  • 결합도 낮추기
  • 관심사 분리하기
  • 시스템 관심사를 모듈로 나누기
  • 함수와 클래스 크기 줄이기
  • 더 나은 이름 선택하기

- 코드는 개발자의 의도를 분명히 표현해야 한다. 개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다. 그래야 결함이 줄어들고 유지보수 비용이 적게 든다.

  • 우선, 좋은 이름을 선택한다.
  • 둘째, 함수와 클래스 크기를 가능한 줄인다.
  • 셋째, 표준 명칭을 사용한다. 예를 들어, 디자인 패턴은 의사소통과 표현력 강화가 주요 목적이다. 클래스가 COMMAND나 VISITOR와 가은 표준 패턴을 사용해 구현된다면 클래스 이름에 패턴 이름을 넣어준다.
  • 넷째, 단위 테스트 케이스를 꼼꼼히 작성한다. 테스트 케이스는 소위 '예제로 보여주는 문서'다.
  • 위 요소들 모두 중요하지만 표현력을 높이는 가장 중요한 방법은 노력이다.

- 클래스와 함수 수를 줄이는 작업도 중요하지만, 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 더 중요하다는 뜻이다.

13장. 동시성

- 동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)를 분리하는 전략이다. 스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다. 그래서 호출 스택을 살펴보면 프로그램 상태가 곧바로 드러난다.

 

- 하지만 구조적 개선만을 위해 동시성을 채택하는 건 아니다. 어떤 시스템은 응답 시간과 작업 처리량(throughput) 개선이라는 요구사항으로 인해 직접적인 동시성 구현이 불가피하다.

 

- 동시성에 대한 일반적인 미신과 오해

  • 동시성은 항상 성능을 높여준다.
    • 동시성은 때로 성능을 높여준다. 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독집적인 계산이 충분히 많은 경우에만 성능이 높아진다. 어느 쪽도 일상적으로 발생하는 상황은 아니다.
  • 동시성을 구현해도 설계는 변하지 않는다.
    • 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.
    • 실제로는 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다. (동시 수정과 데드락은 이 장 후반에 설명한다.)

- 동시성과 관련된 타당한 생각

  • 동시성은 다소 부하를 유발한다. 성능 측면에서 부하가 걸리며, 코드도 더 짜야 한다.
  • 동시성은 복잡하다. 간단한 문제라도 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다. 그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다.
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

- 동시성 코드는 다른 코드와 분리하라.

  • 단일책임원칙(SRP)은 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙이다. 동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다.

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

  • 방법
    • 애초부터 자료를 공유하지 않기
    • 읽기 전용으로 공유하기
    • 객체를 복사해 사용한 후 해당 사본에서 결과를 가져오기
  • 공유 객체를 피하는 방법이 있다면 코드가 문제를 일으킬 가능성도 아주 낮아진다. 사본으로 동기화를 피할 수 있다면 내부 잠금을 없애 절약한 수행 시간이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.

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

  • 예를 들어, JAVA의 synchronized lock

- 깔끔하게 종료하는 다중 스레드 코드를 짜야 한다면 시간을 투자해 올바로 구현하기 바란다.

  • 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다. 가장 흔히 발생하는 문제가 데드락이다. 즉, 스레드가 절대 오지 않을 시그널을 기다린다.

- 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라. 시스템 실패를 '일회성'이라 치부하지 마라.

 

- 스레드 코드 테스트하기

  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
    • 일반적인 방법으로, 스레드가 호출하는 POJO를 만든다. POJO는 스레드를 모른다. 따라서 스레드 환경 밖에서 테스트가 가능하다. POJO에 넣는 코드는 많을수록 더 좋다.
    • 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.
  • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라.
    • 예를 들어, Object.wait(), Object.sleep() 등과 같은 메서드를 추가해 코드를 다양한 순서로 실행한다.
    • 흔들기 기법을 사용해 오류를 찾아내라

- 무엇보다 동시성 코드를 작성할 때는 가장 먼저 SRP를 준수한다. POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다. 즉, 스레드 코드는 최대한 집약되고 작아야 한다는 의미이다.

 

- 동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다. 예를 들어, 여러 스레드가 공유 자료를 조작하거나 자원 풀을 공유할 때 동시성 오류가 발생한다. 루프 반복을 끝내거나 프로그램을 깔끔하게 종료하는 등 경계 조건의 경우가 까다로우므로 특히 주의한다.

 

- 깔끔한 접근 방식을 취한다면 코드가 올바로 돌아갈 가능성이 극적으로 높아진다.