클린코드 - 18.동시성 Ⅱ

2022-02-27

동시성 Ⅱ

  • 클라이언트/서버
    • 애플리케이션이 어디서 시간을 보내는지
      • I/O - 소켓 사용, 데이터베이스 연결, 가상 메모리 스와핑 기다리기 등에 시간을 보낸다.
      • 프로세서 - 수치 계산, 정규 표현식 처리, 가비지 컬렉션 등에 시간을 보낸다.
      • 프로그램이 주로 프로세서 연산에 시간을 보낸다면, 새로운 하드웨어를 추가해 성능을 높인다.
      • 프로그램이 주로 I/O 연산에 시간을 보낸다면 동시성이 성능을 높여주기도 한다.
    • 다중 스레드 프로그램을 깨끗하게 유지하려면 잘 통제된 몇 곳으로 스레드 관리를 모아야 한다. 스레드를 관리하는 코드는 스레드만 관리해야 한다.(단일 책임 원칙)
    • 동시성은 그 자체가 복잡한 문제이므로 다중 스레드 프로그램에서는 단일 책임 원칙이 특히 중요하다.
  • 가능한 실행 경로
    • 중단이 불가능한 연산을 원자적 연산이라고 한다.
    • 프레임: frame - 모든 메서드 호출에는 프레임이 필요하다. 프레임은 반환 주소, 메서드로 넘어온 매개변수, 메서드가 정의하는 지역 변수를 포함한다. 프레임은 호출 스택을 정의할 때 사용하는 표준 기법이다. 현대 언어는 호출 스택으로 기본 함수/메서드 호출과 재귀적 호출을 지원한다.
    • 지역 변수 - 메서드 범위 내에 정의되는 모든 변수를 가리킨다. 정적 메서드를 제외한 모든 메서드는 기본적으로 this라는 지역 변수를 갖는다. this는 현재 객체, 즉 현재 스레드에서 가장 최근에 메시지를 받아 메서드를 호출한 객체를 가리킨다.
    • 피연산자 스택 - JVM이 지원하는 명령 대다수는 매개변수를 받는다. 피연산자 스택은 이런 매개변수를 저장하는 장소다. 피연산자 스택은 표준 LIFO(last in, first out) 자료 구조다.
    • 어떤 연산이 안전하고 안전하지 못한지 파악할 만큼 메모리 모델을 이해하고 있어야 한다. 전처리 연산과 후처리 연산 모두 (++) 원자적 연산이라 오해하는 사람이 많은데, ++ 연산은 분명히 원자적 연산이 아니다.
    • 공유 객체/값이 있는 곳, 동시 읽기/수정 문제를 일으킬 소지가 있는 코드, 동시성 문제를 방지하는 방법을 알고 있어야 한다.
  • 라이브러리를 이해하라
    • Executor 프레임워크는 스레드 풀링으로 정교한 실행을 지원한다. Executor는 java.util.concurrent 패키지에 속하는 클래스다.
    • 애플리케이션에서 스레드는 생성하나 스레드 풀을 사용하지 않는다면 혹은 직접 생성한 스레드 풀을 사용한다면 Executor 클래스를 고려한다. 코드가 깔끔해지고, 이해하기 쉬워지고, 크기가 작아진다.
    • Executor 프레임워크는 스레드 풀을 관리하고, 풀 크기를 자동으로 조정하며, 필요하다면 스레드를 재사용한다. 게다가 다중 스레드 프로그래밍에서 많이 사용하는 Future도 지원한다. 또한 Runnable 인터페이스를 구현한 클래스는 물론 Callable 인터페이스를 구현한 클래스도 지원한다. Callable 인터페이스는 Runnable 인터페이스와 유사하지만 결과 값을 반환한다. 결과 값은 다중 스레드 환경에서 흔히 요구되는 사항이다.
    • 최신 프로세서는 스레드를 차단하지 않고도(non blocking) 안정적으로 값을 갱신한다. 자바 5 VM은 이를 이용한다.
    • 다중 스레드 환경에서 안전하지 않은 클래스
      • SimpleDateFormat
      • 데이터베이스 연결
      • java.util 컨테이너 클래스
      • 서블릿
    • java.util.concurrent 패키지가 제공하는 집합 클래스는 스레드에 안전한 메서드를 제공한다.
  • 메서드 사이에 존재하는 의존성을 조심하라
    • 메서드 사이에 존재하는 의존성 때문에 나타나는 버그는 시스템을 출시하고도 오랜 시간이 지나서야 발생하고, 추적하기도 어렵다. 해결 방안은 세 가지다.
      • 실패를 용인한다.
      • 클라이언트를 바꿔 문제를 해결한다. 클라이언트-기반 잠금 메커니즘을 구현한다.
      • 서버를 바꿔 문제를 해결한다. 서버에 맞춰 클라이 언트도 바꾼다. 즉, 서버-기반 잠금 메커니즘을 구현한다.
      • 일반적으로 서버-기반 잠금이 더 바람직하다. 이유는 다음과 같다.
        1. 코드 중복이 줄어든다. 클라이언트-기반 잠금 메커니즘은 각 클라이언트가 알아서 서버를 잠궈야 한다. 잠금 메커니즘을 서버에 구현하면 클라이언트는 자유롭게 객체를 사용할뿐더러 클라이언트에 잠금 코드를 추가할 필요도 없어진다.
        2. 성능이 좋아진다. 단일스레드 환경으로 시스템을 배치할 경우 (다중 스레드 서버를 단일 스레드 서버로) 서버만 교체하면 오버헤드가 줄어든다.
        3. 오류가 발생할 가능성이 줄어든다. 잠금을 잊어버리는 바람에 오류가 발생할 위험은 프로그래머 한 명으로 제한된다.
        4. 스레드 정책이 하나다. 클라이언트-기반 잠금은 각 클라이언트가 정책을 구현하는 반면, 서버-기반 잠금은 서버 한곳에서 정책을 구현한다.
        5. 공유 변수 범위가 줄어든다. 클라이언트가 공유 변수 자체를 모르거나 공유 변수가 잠긴 방식을 모른다. 모두가 서버에 숨겨진다. 문제가 생기면 살펴볼 곳이 적다.

          서버 코드에 손대지 못한다면 ADAPTER 패턴을 사용해 API를 변경한 후 잠금을 추가한다. 아니면, 더 좋은 방법으로, 스레드에 안전하며 인터페이스가 확장된 집합 클래스를 사용한다.

  • 작업 처리량 높이기
    • 코드에서 synchronized 블록등 동기화 영역은 언제나 작을수록 좋다.
    • 순서에 무관한 프로그램이라면 다중 스레드가 단일 스레드에 비해 처리율이 훨씬 높다.
  • 데드락
    • 다음 네 가지 조건을 모두 만족하면 데드락이 발생한다.
      1. 상호배제
        • 여러 스레드가 한 자원을 공유하나 그 자원은 여러 스레드가 동시에 사용하지 못하며 개수가 제한적이라면 상호 배제 조건을 만족한다. 데이터베이스 연결, 쓰기용 파일 열기, 레코드 락, 세마포어 등과 같은 자원이다.
      2. 잠금 & 대기
        • 일단 스레드가 자원을 점유하면 필요한 나머지 자원까지 모두 점유해 작업을 마칠 때까지 이미 점유한 자원을 내놓지 않는다.
      3. 선점 불가
        • 한 스레드가 다른 스레드로부터 자원을 빼앗지 못한다. 자원을 점유한 스레드가 스스로 내놓지 않는 이상 다른 스레드는 그 자원을 점유하지 못한다.
      4. 순환 대기
        • 죽음의 포옹이라고도 한다. T1, T2라는 스레드 두 개가 있으며 R1, R2라는 자원 두 개가 있다고 가정하자. T1이 R1을 점유하고 T2가 R2를 점유한다. 또한 T1은 R2가 필요하고 T2도 R2가 필요하다.
    • 상호 배제 조건 깨기
      • 동시에 사용해도 괜찮은 자원을 사용한다.
      • 스레드 수 이상으로 자원 수를 늘인다.
      • 자원을 점유하기 전에 필요한 자원이 모두 있는지 확인한다.
      • 불행하게도 대다수 자원은 그 수가 제한적인 데다 동시에 사용하기도 어렵다. 게다가 첫 번째 자원을 사용하고 나서야 두 번째로 필요한 자원이 밝혀지는 경우도 없지 않다.
    • 잠금 & 대기 조건 깨기
      • 대기하지 않으면 데드락이 발생하지 않는다. 각 자원을 점유하기 전에 확인한다. 만약 어느 하나라도 점유하지 못한다면 지금까지 점유한 자원을 몽땅 내놓고 처음부터 다시 시작한다. 이 방법은 잠재적인 문제가 몇 가지 있다.
        • 기아 - 한 스레드가 계속해서 필요한 자원을 점유하지 못한다.
        • 라이브락 - 여러 스레드가 한꺼번에 잠금 단계로 진입하는 바람에 계속해서 자원을 점유했다 내놨다를 반복한다. 단순한 CPU 스케줄링 알고리즘에서 특히 쉽게 발생한다.
      • 두 경우 모두가 자칫하면 작업 처리량을 크게 떨어뜨린다. 기아는 CPU 효율을 저하시키는 반면 라이브락은 쓸데 없이 CPU만 많이 사용한다.
      • 이런 전략이 비효율적으로 보일지도 모르지만 아무 대책이 없는 경우보다는 좋다. 모든 전략이 실패할 때 언제든 꺼내 들 카드라는 장점이 있다.
    • 선점 불가 조건 깨기
      • 데드락을 피하는 또 다른 전략은 다른 스레드로부터 자원을 뺏어오는 방법이다. 일반적으로 간단한 요청 메커니즘으로 처리한다. 필요한 자원이 잠겼다면 자원을 소유한 스레드에게 풀어달라 요청한다. 소유 스레드가 다른 자원을 기다리던 중이었다면 자신이 소유한 자원을 모두 풀어주고 처음부터 다시 시작한다.
      • 앞 전략과 비슷하지만 스레드가 자원을 기다려도 괜찮다는 이점이 있다. 그러면 처음부터 다시 시작하는 횟수가 줄어든다. 하지만 이 모든 요청을 관리하기가 간단하지 않다.
    • 순환 대기 조건 깨기
      • 데드락을 방지하는 가장 흔한 전략이다. 대다수 시스템에서는 모든 스레드가 동의하는 간단한 규약이면 충분하다.
      • R1을 점유한 T1이 R2를 기다리고 R2를 점유한 T2가 R1을 기다리는 앞서 예제에서 T1과 T2가 자원을 똑같은 순서로 할당하게 만들면 순환 대기는 불가능해진다.
      • 좀 더 일반적으로, 모든 스레드가 일정 순서에 동의하고 그 순서로만 자원을 할당한다면 데드락은 불가능하다. 그러나 이 전략 역시 문제를 일으킬 소지가 있다.
        • 자원을 할당하는 순서와 자원을 사용하는 순서가 다를지도 모른다. 그래서 맨 처음 할당한 자원을 아주 나중에야 쓸지도 모른다. 즉, 자원을 꼭 필요한 이상으로 오랫동안 점유한다.
        • 때로는 순서에 따라 자원을 할당하기 어렵다. 첫 자원을 사용한 후에야 둘째 자원 ID를 얻는다면 순서대로 할당하기란 불가능하다.

          데드락을 피하는 전략은 많다. 프로그램에서 스레드 관련 코드를 분리하면 조율과 실험이 가능하므로 통찰력이 높아져 최적의 전략을 찾기 쉬워진다.

  • 실패를 증멸할 테스트 코드 작성
    • 몬테 카를로 테스트. 조율이 가능하게 유연한 테스트를 만든다. 그런 다음, 임의로 값을 조율하면서 반복해 돌린다. 테스트가 실패하면 버그가 있다는 증거다. 테스트는 일찌감치 작성하기 시작해 통합 서버에서 계속 돌린다. 참고로, 테스트가 실패한 조건은 신중하게 기록한다.
    • 시스템을 배치할 플랫폼 전부에서 테스트를 돌린다. 반복해서 돌린다. 테스트가 실패 없이 오래 돌아갈수록 두 가지 중 하나일 확률이 높아진다.
      1. 실제 코드가 올바르다.
      2. 테스트가 부족해 문제를 드러내지 못한다.
    • 부하가 변하는 장비에서 테스트를 돌린다. 실제 환경과 비슷하게 부하를 걸어줄 수 있다면 그렇게 한다.
  • 스레드 코드 테스트를 도와주는 도구
    • IBM은 ConTest라는 도구를 내놓았다. 스레드에 안전하지 않는 코드에 보조 코드를 더해 실패할 가능성을 높여주는 도구다. 다음은 ConTest를 사용하는 방법이다.
      • 실제 코드와 테스트 코드를 작성한다. 다양한 부하 상황에서 여러 사용자를 시뮬레이션하는 테스트도 빼놓지 않는다.
      • ConTest로 실제 코드와 테스트 코드에 보조 코드를 추가한다.
      • 테스트를 실행한다.

        보조 코드를 추가한 클래스는 훨씬 더 빠르고 확실히 실패한다.


Read More

클린코드 - 17.냄새와 휴리스틱

2022-02-27

냄새와 휴리스틱

  • 주석
    • 부적절한 정보, 쓸모 없는 주석, 중복된 주석, 성의 없는 주석, 주석 처리된 코드는 바람직하지 않다.
  • 환경
    • 빌드는 간단히 한 단계로 끝나야 한다.
    • 모든 단위 테스트는 한 명령으로 돌아야 한다.
  • 함수
    • 함수에서 인수 개수는 작을수록 좋다. 아예 없으면 가장 좋다.
    • 출력 인수는 직관을 정면으로 위배한다. 함수에서 뭔가의 상태를 변경해야 한다면 출력 인수를 쓰지 말고 함수가 속한 객체의 상태를 변경한다.
    • boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다.
    • 플래그 인수는 혼란을 초래하므로 피해야 한다.
    • 아무도 호출하지 않는 함수는 삭제한다.
  • 일반
    • 이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다. 현실적으로는 여러 언어가 불가피하지만 각별한 노력을 기울여 소스 파일에서 언어 수와 범위를 최대한 줄이도록 애써야 한다.
    • 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다. 당연한 동작을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵다.
    • 코드는 올바로 동작해야 한다. 모든 경계 조건, 모든 구석진 곳, 모든 기벽, 모든 예외는 우아하고 직관적인 알고리즘을 좌초시킬 암초다. 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성해야 한다.
    • 안전 절차를 무시하면 위험하다. 컴파일러 경고 일부를 꺼버리면 빌드가 쉬워질지 모르지만 자칫하면 끝없는 디버깅에 시달린다. 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 바람직하지 않다.
    • 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. 추상화 수준을 높였으므로 구현이 빨라지고 오류가 적어진다.
    • 똑같은 코드가 여러 차례 나오는 중복은 간단한 함수로 교체한다.
    • 추상화는 저차원 상세 개념에서 고차원 일반 개념을 분리한다. 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다. 고차원 개념과 저차원 개념을 섞어서는 안 된다.
    • 기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다. 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 한다.
    • 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다. 부실하게 정의된 인터페이스는 반드시 호출해야 하는 온갖 함수를 제공한다. 그래서 결합도가 높다.
    • 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다. 클래스가 제공하는 메서드 수는 작을수록 좋다. 함수가 아닌 변수 수도 작을수록 좋다. 클래스에 들어있는 인스턴스 변수 수도 작을수록 좋다.
    • 자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라. 하위 클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 마라. 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라.
    • 실행되지 않는 죽은 코드는 시스템에서 제거하라.
    • 변수와 함수는 사용되는 위치에 가깝게 정의한다. 지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다. 비공개 함수는 처음으로 호출한 직후에 정의한다.
    • 어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다. 간단한 일관성만으로도 코드를 읽고 수정하기가 쉬워진다.
    • 아무도 사용하지 않는 변수, 아무도 호출하지 않는 함수, 정보를 제공하지 못하는 주석 등 쓸데없이 코드만 복잡하게 만드는 잡동사니는 없애야 한다.
    • 서로 무관한 개념을 인위적으로 결합하지 않는다. 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한 위치에 넣어버리는 부주의한 행동을 하지 않는다.
    • 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안 된다.
    • 선택자 인수는 목적을 기억하기 어려울 뿐 아니라 각 선택자 인수가 여러 함수를 하나로 조합한다. 부울 인수, enum, int 등 함수 동작을 제어하려는 인수는 하나 같이 바람직하지 않다. 일반적으로, 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.
    • 코드를 짤 때는 의도를 최대한 분명히 밝힌다.
    • 일반적으로 static 함수보다 인스턴스 함수가 더 좋다. 조금이라도 의심스럽다면 인스턴스 함수로 정의한다. 반드시 static 함수로 정의해야겠다면 재정의할 가능성은 없는지 꼼꼼히 따져본다.
    • 프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하는 방법이다.
    • 이름만으로 분명하지 않기에 구현을 살피거나 문서를 뒤적어야 한다면 더 좋은 이름으로 바꾸거나 아니면 더 좋은 이름을 붙이기 쉽도록 기능을 정리해야 한다.
    • 대다수 괴상한 코드는 사람들이 알고리즘을 충분히 이해하지 않은 채 코드를 구현한 탓이다. 구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라. 알고리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.
    • 한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다. 논리적인 의존성만으로는 부족하다. 의존하는 모듈이 상대 모듈에 대해 뭔가를 가정하면 (즉, 논리적으로 의존하면) 안 된다. 의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.
    • if/else 혹은 switch/case 문보다 다형성을 사용하라
    • 팀은 업계 표준에 기반한 구현 표준을 따라야 한다. 구현 표준은 인스턴스 변수 이름을 선언하는 위치, 클래스/메서드/변수 이름을 정하는 방법, 괄호를 넣는 위치 등을 명시해야 한다. 표준을 설명하는 문서는 코드 자체로 충분해야 하며 별도 문서를 만들 필요는 없어야 한다.
    • 매직 숫자는 명명된 상수로 교체하라
    • 코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충 결정해서는 안 된다. 코드에서 모호성과 부정확은 의견차나 게으름의 결과다. 어느 쪽이든 제거해야 한다.
    • 설계 결정을 강제할 때는 규칙보다 관례를 사용한다. 명명 관례도 좋지만 구조 자체로 강제하면 더 좋다.
    • 조건을 캡슐화하라. 부울 논리는 이해하기 어렵다. 조건의 의도를 분명히 밝히는 함수로 표현하라.
    • 부정 조건은 피하라. 부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건으로 표현한다.
    • 함수는 한 가지만 해야 한다. 함수를 여럿으로 나눈다.
    • 함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다.
    • 코드 구조를 잡을 때는 이유를 고민하라. 그리고 그 이유를 코드 구조로 명백히 표현해야 한다. 구조에 일관성이 없어 보인다면 남들이 맘대로 바꿔도 괜찮다고 생각한다.
    • 경계 조건을 캡슐화하라. 경계 조건은 빼먹거나 놓치기 십상이다. 경계 조건은 한 곳에서 별도로 처리한다. 코드 여기저기에서 처리하지 않는다.
    • 함수는 추상화 수준을 한 단계만 내려가야 한다. 함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.
    • 추상화 수준 분리는 리팩터링을 수행하는 가장 중요한 이유 중 하나다.
    • 추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안 된다. 대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다. 설정 관련 상수는 최상위 단계에 둔다. 그래야 변경하기도 쉽다. 설정 관련 변수는 나머지 코드에 인수로 넘긴다. 저차원 함수에 상수 값을 정의하면 안 된다.
    • 일반적으로 한 모듈은 주변 모듈을 모를수록 좋다. 자신이 직접 사용하는 모듈만 알아야 한다.
  • 자바
    • 긴 import 목록을 피하고 와일드카드를 사용하라. 긴 import 목록은 읽기에 부담스럽다. 명시적인 import 문은 강한 의존성을 생성하지만 와일드카드는 그렇지 않다.
    • 상수는 상속하지 않는다. 상수를 상속 계층 맨 위에 숨겨두는 대신 static import를 사용해야 한다.
    • public static final int는 코드에서 의미를 잃어버리기도 한다. enum은 이름이 부여된 열거체에 속하기 때문에 그렇지 않다. 또한, 메서드와 필드도 사용할 수 있다. int보다 훨씬 더 유연하고 서술적인 강력한 도구다.
  • 이름
    • 이름은 성급하게 정하지 않는다. 서술적인 이름을 신중하게 고른다. 소프트웨어 가독성의 90%는 이름이 결정한다. 신중하게 선택한 이름은 추가 설명을 포함한 코드보다 강력하다.
    • 적절한 추상화 수준에서 이름을 선택하라. 구현을 드러내는 이름은 피해야 한다. 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택한다.
    • 가능하다면 표준 명명법을 사용한다. 기존 명명법을 사용하는 이름은 이해하기 더 쉽다. 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.
    • 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.
    • 긴 범위는 긴 이름을 사용하라. 이름 길이는 범위 길이에 비례해야 한다. 범위가 작으면 아주 짧은 이름을 사용해도 괜찮다. 하지만 범위가 길어지면 긴 이름을 사용한다.
    • 인코딩을 피하라. 이름에 유형 정보나 범위 정보를 넣어서는 안 된다.
    • 이름으로 부수 효과를 설명하라. 함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다. 이름에 부수 효과를 숨기지 않는다.
  • 테스트
    • 테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다. 테스트 케이스가 확인하지 않는 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전하다.
    • 커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다. 커버리지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기가 쉬워진다.
    • 사소한 테스트를 건너뛰지 않는다. 사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.
    • 때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확신하기 어렵다. 불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 @Ignore를 붙여 표현한다. 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달려있다.
    • 경계 조건은 각별히 신경 써서 테스트한다. 알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.
    • 버그는 서로 모이는 경향이 있다. 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다.
    • 때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.
    • 테스트 커버리지 패턴을 살펴야한다. 통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 실패 원인이 드러난다.
    • 테스트는 빨라야 한다. 느린 테스트 케이스는 실행하지 않게 된다.

Read More

클린코드 - 16.SerialDate 리팩터링

2022-02-27

SerialDate 리팩터링

http://www.jfree.org/jcommon/index.php에서 JCommon 라이브러리를 제공한다. JCommon 라이브러리를 뒤져보면 org.jfree.date라는 패키지가 있으며, 여기에 SerialDate라는 클래스가 있다.

Read More

클린코드 - 14.점진적인 개선

2022-02-27

점진적인 개선

  • 단순한 개념을 구현하는데 코드가 너무 많이 필요한 한 가지 이유는 장황한 언어인 자바를 사용하는 탓이다. 자바는 정적 타입 언어라서 타입 시스템을 만족하려면 많은 단어가 필요하다.
  • 프로그래밍은 과학보다 공예에 가깝다. 깨끗한 코드를 짜려면 먼저 지저분한 코드를 짠 뒤에 정리해야 한다.
  • 먼저 1차 초안을 쓰고, 그 초안을 고쳐 2차 초안을 만들고, 계속 고쳐 최종안을 만든다. 깔끔한 작품을 내놓으려면 단계적으로 개선해야 한다.
  • ‘돌아가는’프로그램을 그 상태가 어떻든 그대로 버려두는 행위는 자살 행위다.
  • 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름 아래 구조를 크게 뒤집는 행위다. 어떤 프로그램은 그저 그런 ‘개선’에서 결코 회복하지 못한다. ‘개선’ 전과 똑같이 프로그램을 돌리기가 아주 어렵기 때문이다.
  • TDD는 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다. TDD는 시스템을 망가뜨리는 변경을 허용하지 않는다. 변경을 가한 후에도 시스템이 변경 전과 똑같이 돌아가야 한다.
  • 리팩터링을 하다보면 코드를 넣었다 뺐다 하는 사례가 아주 흔하다. 단계적으로 조금씩 변경하며 매번 테스트를 돌려야 하므로 코드를 여기저기 옮길 일이 많아진다.
  • 리팩터링은 루빅 큐브 맞추기와 비슷하다. 큰 목표 하나를 이루기 위해 자잘한 단계를 수없이 거친다. 각 단계를 거쳐야 다음 단계가 가능하다.
  • 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다. 적절한 장소를 만들어 코드를 분리해도 설계가 좋아진다.
  • 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.

Read More