Post

실용주의 프로그래머 읽어보기 4주차

실용주의 프로그래머 읽어보기 4주차

들어가며

이 포스트는 데이비드 토머스, 앤드류 헌트의 「실용주의 프로그래머」 Topic21 ~ 27까지 읽고 개인적으로 학습한 내용을 정리한 글입니다.

  • 책: 실용주의 프로그래머
  • 저자: 데이비드 토머스, 앤드류 헌트
  • 출판사: 인사이트
  • 챕터: Topic 20 ~ Topic 28

핵심 내용 정리

Topic 21. 텍스트 처리

Tip 35 텍스트 처리 언어를 익혀라

  • 프로그래밍 언어, 명령어들은 훌륭한 텍스트 처리 언어다
    • 이들을 사용해 유틸리티를 만들거나 아이디어를 프로토타이핑 해볼 수 있다

Topic 22. 엔지니어링 일지

  • 엔지니어링 일지를 쓰면 좋은 점
    • 기억보다 더 믿을만 하다
    • 진행 중인 작업과 직접적인 관계가 없는 발상을 일단 쌓아 놓을 수 있는 곳이 생긴다. 위대한 발상을 잊어버릴 걱정이 없이 지금 하는 일에 계속 집중할 수 있다
    • 메모를 시작하자마자 메모의 주제인 당신이 방금 전까지 하던 일이 실은 말도 안되는 것을 깨닫게 될 수도 있다

4장 실용주의 편집증

실용주의 프로그래머는 자기 자신 역시 믿지 않는다
어느 누구도, 심지어는 자기 자신도 완벽한 코드를 작성할 수 없음을 알기 때문에 실용주의 프로그래머는 자신의 실수에 대비한 방어책을 마련한다

Topic 23. 계약에 의한 설계

  • 정직한 거래를 보장하는 최선의 해법 중 하나는 ‘계약’ 이다
  • 계약
    • 자신과 상대편의 권리 및 책임을 정의
    • 한쪽이 계약을 어겼을 경우 대응도 계약 사항에 포함된다

DBC

  • Design By Contract
    • 계약에 의한 설계
    • 프로그램의 정확성을 보장하기 위해 소프트웨어 모듈의 권리와 책임을 문서화하고 합의하는 데 초점을 맞춘다

선행 조건(precondition)

  • 루틴이 호출되기 위해 참이어야 하는 것
  • 루틴의 요구 사항
  • 루틴의 선행 조건이 위반된 경우 루틴이 호출되어서는 안된다

후행 조건(postcondition)

  • 루틴이 자기가 할 것이라고 보장하는 것
  • 루틴이 완료되었을 때 세상의 상태
  • 루틴에 후행 조건이 있다는 것은 곧 루틴이 종국에는 종료될 것이라는 걸 의미, 무한 반복은 허용되지 않는다

클래스 불변식

  • 호출자 입장에서 볼 때 이 조건이 언제나 참인 것을 클래스가 보장한다
  • 루틴의 내부 처리 도중에는 불변식이 참이 아닐 수도 있지만, 루틴이 끝나고 호출자로 제어권이 반환되는 시점에 불변식이 참이 되어야 한다

    루틴과 그 루틴을 호출하려는 코드 간의 계약
    만약 호출자가 루틴의 모든 선행 조건을 충족한다면 해당 루틴은 종료 시 모든 후행 조건과 불변식이 참이 되는 것을 보장

Tip 37 계약으로 설계하라

클래스 불변식과 함수형 언어

  • 클래스 불변식의 진짜 의미는 ‘상태(state)’이다

DBC와 테스트 주도 개발

  • DBC가 테스트 주도 개발보다 장점
    • DBC는 테스트 환경 구성이나 mock이 필요 없다
    • DBC는 모든 입력값에 대해 성공과 실패를 정의한다
    • TDD와 다른 테스트는 빌드 과정 중’테스트’할 때만 수행된다. 하지만 DBC와 단정문은 영원하다. 설계, 개발, 배포, 유지 보수 전체에 걸쳐 사용된다
    • TDD는 테스트 중 코드 내의 내부 불변식을 확인하는 것에 초점을 두지 않는다. 그보다 공개 인터페이스를 확인하는 블랙박스 방식에 가깝다
    • DBC는 방어적 프로그래밍보다 더 효율적이고 더 DRY하다. 방어적 프로그래밍에서는 아무도 데이터를 검증하지 않은 상황에 대비하기 위해 모든 사람이 데이터를 검증한다

DBC 구현

  • 코드를 작성하기 전에 유효한 입력 범위 결정, 경계 조건, 루틴이 뭘 전달하는 지 약속하는 것 등이 더 나은 소프트웨어를 작성하는 데 엄청난 도움이 된다

단정문

  • 몇몇 언어는 조건문을 실행 시점에 확인하는 단정문을 사용해 부분적으로나마 흉내 낼 수 있다
  • 단정문을 이용해 DBC로 가능한 모든 것을 할 수는 없다
    • 객체 지향 언어에서는 상속 계층을 따라 단정문이 밑으로 전파되도록 할 수 없다
    • 새 코드에 수작업으로 단정문을 일일이 복사 붙여넣기 해야함
    • 즉 계약이 자동으로 집행되지 않는다
  • ‘이전’ 값이라는 개념이 없음
    • ‘이전’값은 메서드 진입 시점에 갖고 있던 값
  • 일반적인 런타임 시스템과 라이브러리가 계약을 지원하도록 설계되지 않았음

DBC와 일찍 멈추기

  • DBC는 우리의 “일찍 작동을 멈춰라”라는 개념과 잘 어울린다
    • 단정문이나 DBC 방식을 사용하면 선행 조건, 후행 조건, 불변식을 검증하면 더 일찍 멈추고, 문제에 대한 보다 정확한 정보를 알려줄 수 있다

의미론적 불변식(semantic invariant)

  • 의미론적 불변식
    • 의미론적 불변식을 사용하면 어겨서는 안 되는 요구 사항을 표현할 수 있다
    • 어겨서는 안 되는 고정된 규칙인 요구 사항과 얼마든지 없어질 수 있는 단순한 정책을 혼동하지 말아야 한다
      • 의미론적 불변식은 무언가가 품은 진짜 의미의 중심이 되어야 하며, 훨씬 역동적으로 변하는 비즈니스 규칙처럼 일시적인 정책에 영향을 받으면 안된다
    • 불변식의 자격이 있는 요구 사항을 찾았다면 자신이 작성하는 모든 문서에 잘 드러나도록 만들어야한다

      동적 계약과 에이전트

  • 자율 에이전트면 계약 내용이 변경될 수 있다
  • 에이전트 기술에 의존하는 어떤 시스템이건 계약의 합의가 결정적으로 중요하다
  • 수동으로 계약을 만들 수 없다면, 자동화도 불가능하므로, 나중에 소프트웨어를 설계하게 되면 계약 역시 설계해야한다

Topic 24. 죽은 프로그램은 거짓말은 하지 않는다

  • 우리 중 대다수가 코드를 작성할 때 파일이 성공적으로 닫혔는지, 혹은 트레이스 문이 우리 예상대로 찍혔는지 확인하지 않았던 적이 있을 것이다
  • 실용주의 프로그래머는 만약 오류가 발생했다면 정말로 뭔가 나쁜 일이 생긴 것이라고 자신에게 이야기한다

잡은 후 그냥 놓아주는 것은 물고기뿐

Tip 38 일찍 작동을 멈춰라

  • 실용적인 코드는 새로운 예외가 자동으로 전파된다

망치지 말고 멈춰라

  • 가능한 한 빨리 문제를 발견하면 좀 더 일찍 시스템을 멈출 수 있으니 더 낫다
  • 프로그램을 멈추는 것이 할 수 있는 최선인 경우가 흔하다
  • 일반적으로 죽은 프로그램이 끼치는 피해는 이상한 상태의 프로그램이 끼치는 피해보다 훨씬 적은 법이다

Topic 25. 단정적 프로그래밍

Tip 39 단정문으로 불가능한 상황을 예방하라

  • “그런 일은 절대 일어나지 않을거야”라고 생각이 든다면 확인하는 코드인 assertion을 사용하라
    1
    
    assert result != null && result.size() > 0 : "XYZ의 결과값이 비어있음"
    

단정과 부작용

  • 문제를 발견하려고 넣은 코그다 오히려 새로운 문제를 만드는 결과를 낳기도 한다
  • 예시
    1
    2
    3
    4
    5
    
    while (iter.hasMorElement()){
      assert(iter.nextElement() != null);;
      Object obj = iter.nextElement();
      ...
    }
    
  • assert(iter.nextElement()) 로 인해 원소 다음 iterator를 이동시키는 부작용이 있다

단정 기능을 켜 둬라

  • 테스트가 모든 버그를 발견하지 못한다
    • 프로그램이 조금만 복잡해져도 테스트가 어려워지기 때문
  • 낙관주의자들은 여러분의 프로그램이 험한 세상에서 돌아간다는 사실을 잊는다
    • 실제로 네트워크 선이 끊어지는 일이 존재한다

Topic 26. 리소스 사용의 균형

Tip 40. 자신이 시작한 것은 자신이 끝내라

Tip 41 지역적으로 행동하라

  • 자신이 시작한 것은 자신이 끝내라
    • 리소스를 할당하는 함수나 객체가
    • 리소스를 해제하는 책임 역시 져야한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigDecimal;

public class Customer {
    private String name;
    private RandomAccessFile customerFile;
    private BigDecimal balance;

    public Customer(String name) {
        this.name = name;
    }

    public void readCustomer() throws IOException {
        customerFile = new RandomAccessFile(name + ".rec", "rw");
        String balanceStr = customerFile.readLine();
        balance = new BigDecimal(balanceStr);
    }

    public void writeCustomer() throws IOException {
        customerFile.seek(0);
        customerFile.writeBytes(balance.toString() + "\n");
        customerFile.close();
    }

    public void updateCustomer(BigDecimal transactionAmount) throws IOException {
        readCustomer();
        balance = balance.add(transactionAmount);
        writeCustomer();
    }
}

  • read_customer 와 write_customer 루틴이 긴밀하기 결합되어 있다
  • 더 나은 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.RandomAccessFile;
import java.io.IOException;
import java.math.BigDecimal;

public class Customer {
    private String name;
    private BigDecimal balance;
    
    public Customer(String name) {
        this.name = name;
    }
    
    public void readCustomer(RandomAccessFile customerFile) throws IOException {
        String balanceStr = customerFile.readLine();
        balance = new BigDecimal(balanceStr);
    }
    
    public void writeCustomer(RandomAccessFile customerFile) throws IOException {
        customerFile.seek(0);
        customerFile.writeBytes(balance.toString() + "\n");
    }
    
    public void updateCustomer(BigDecimal transactionAmount) throws IOException {
        RandomAccessFile file = new RandomAccessFile(name + ".rec", "rw");
        readCustomer(file);
        balance = balance.add(transactionAmount);
        writeCustomer(file);
        file.close();
    }
}

코드 비교(책에는 없는 내용)

비교 분석

  • 의존성 관리
    • 첫 번째 코드: customerFile이 클래스 필드로 관리되어 메서드 간 상태가 공유됩니다.
    • 두 번째 코드: 파일 핸들을 매개변수로 전달하여 의존성이 명확합니다.

자원 관리

  • 첫 번째 코드: readCustomer()에서 파일을 열고 writeCustomer()에서 닫습니다. 이는 메서드 간 의존성을 만들어 단독 호출 시 문제가 발생할 수 있습니다.
  • 두 번째 코드: updateCustomer()에서 파일을 열고 닫아 자원 관리가 한 메서드 내에서 완결됩니다.

메서드 독립성:

  • 첫 번째 코드: 메서드 간 숨겨진 의존성이 있어 단독 테스트가 어렵습니다.
  • 두 번째 코드: 각 메서드가 필요한 자원을 매개변수로 받아 독립적으로 동작합니다.

불필요한 import:

  • 첫 번째 코드: 사용하지 않는 여러 import 문이 있습니다.
  • 두 번째 코드: 필요한 import만 포함되어 있습니다.

두 번째 코드가 더 좋은 이유

  • 관심사의 분리: 파일 열기/닫기와 데이터 읽기/쓰기 작업이 명확히 분리되어 있습니다.
  • 테스트 용이성: 메서드가 독립적이어서 단위 테스트가 용이합니다.
  • 명확한 의존성: 필요한 자원을 매개변수로 전달하여 의존성이 명시적입니다.
  • 자원 관리: 파일 열기와 닫기가 같은 메서드 내에서 처리되어 자원 누수 위험이 적습니다.
  • 코드 재사용성: 다른 파일 객체에도 같은 메서드를 사용할 수 있어 유연합니다.
  • 이러한 설계는 객체지향 프로그래밍의 원칙과 더 잘 부합하며, 유지보수와 확장에 더 적합합니다.

중첩 할당

  • 리소스를 할당한 순서의 역순으로 해제하라
  • 코드의 여러 곳에서 동일한 구성의 리소스들을 할당하는 경우에는 언제나 같은 순서로 할당해야 deadlock 가능성을 줄 일수 있다

객체와 예외

  • 객체 지향 언어로 프로그래밍을 한다면 리소스를 클래스 안에 캡슐화하는 것이 유용할 수 있다

균형 잡기와 예외

  • 예외를 지원하는 경우 리소스 해제가 골치아플 수 있다
  • 이를 해결하는 두 가지 방법
    • 변수 스코프 사용하기
    • try-catch 블록에서 finally 절을 사용한다

나쁜 예외 처리 방식

1
2
3
4
5
6
try {
    thing = allocate_resource();
    process(thing);
}finally{
    deallocate(thing);
}
  • 이럴 경우 thing이 없으면 할당한 적이 없는 리소스를 해제하려고 시도함
  • 더 나은 방식
    1
    2
    3
    4
    5
    6
    
    thing = allocate_resource();
    try {
      process(thing);
    }finally{
      deallocate(thing);
    }
    

    리소스 사용의 균형을 잡을 수 없는 이유

  • 동적인 자료 구조를 사용하는 프로그램에서 자주 리소스 할당 기본 패턴이 맞지 않는 경우도 있다
    • 한 루틴에서 메모리의 일정 영역을 할당한 다음 어떤 더 큰 구조에 그것을 연결한 후, 한동안 그대로 쓰는 식
    • 이 경우 메모리 할당에 대한 의미론적 불변식을 정하는 것하면 해결할 수 있다
    • 한군데 모은 자료 구조 안의 자료를 누가 책임지는지 정해 놓아야 한다
  • 최상위 구조의 메모리 할당을 해제하는 경우의 처리 방법
    • 최상위 구조가 자기 안에 들어있는 하위 구조들을 해제할 책임을 진다. 하위 구조들은 또다시 재귀적으로 자기 안에 들어 있는 자료들을 해제할 책임을 지고, 이런 식으로 반복한다
    • 최상위 구조가 그냥 할당 해제된다. 최상위 구조가 참조하던 하위 구조들은 연결이 끊어져서 다른 곳에서 참조되지 않으면 외톨이가 된다
    • 최상위 구조가 하나라도 하위 구조를 가지고 있으면 자신의 할당 해제를 거부한다

균형을 점검하기

  • 대부분의 애플리케이션에서 보통 리소스의 종류별로 wrapper를 만들고 그 wrapper들이 모든 할당과 해제 기록을 보관한다
  • wrapper를 사용해 상태가 올바른지 점검하라

Topic 27. 헤드라이트를 앞서가지 말라

Tip 42 작은 단계들을 밟아라. 언제나.

  • 언제나 신중하게 작은 단계들을 밟아라.
  • 더 진행하기 전에 피드백을 확인하고 조정하라
  • 자신이 볼 수 있는 미래까지만 고려해야한다
  • 자신의 코드를 더 적절한 무언가로 대체하기 쉽게 설계하라

블랙 스완

Tip 43 예언하지 말라

  • 대부분의 경우 내일은 오늘과 거의 같을 것이다. 하지만 확신하지는 말라

Topic 28. 결합도 줄이기

Tip 44 결합도가 낮은 코드가 바꾸기 쉽다

  • 우리의 운명은 둘 중 하나다. 바꿔야하는 곳을 모두 찾아내느라 시간을 들이거나, 딱 하나만 바꾸고 결합된 다른 것들은 잊은 채 왜 프로그램이 죽는지 고민하느라 시간을 들이거나

열차 사고

1
2
3
4
5
public void applyDiscount(customer, order_id,discount){
    totals = customer.orders.find(order_id).getTotals();
    totals.grandTotal = totals.grandTotal - discount;
    totals.discount = discount;
}
  • 이 코드의 객체간 노출 방향
    • customer > orders 컬렉션 > orders 컬렉션의 find, totals, grandTotal을 알기 위한 discount
    • 총 5가지를 알아야 한다
  • 더 좋은 방향
    1
    2
    3
    
    public void applyDiscount(customer, order_id,discount ){
      customer.findOrder(order_id).getTotals().applyDiscount(discount);
    }
    
  • 고객 객체에서 바로 주문 객체를 가져옴

데메테르 법칙

1
2
3
4
5
6
7
// 좋지 않은 방식
amount = customer.orders.last().totals().amount;

// 더 나은 바식
people.sort_by({person | person.age})
.first(10)
.map({ person | person.name })

연쇄와 파이프라인

  • 파이프 라인은 메서드 호출로 이루어진 열차 사고와 다르다
    • 파이프라인에는 숨겨진 구현 세부 사항에 의존하지 않기 때문
    • 파이프라인의 함수에서 반환하는 데이터는 반드시 다음 함수가 처리할 수 있는 형식이어야 한다

글로벌화의 해악

  • 전역 데이터는 여러 가지 방법으로 코드의 결합도를 높인다

singleton도 전역 데이터다

  • 당신의 코드에 있는것이 싱글턴뿐이더라도, 외부로 노출된 인스턴스 변수가 잔뜩 있는 싱글턴은 여전히 전역 데이터다

외부 리소스도 전역 데이터다

Tip 45 전역적이어야 할 만큼 중요하다면 API로 감싸라

상속은 결합을 늘린다

This post is licensed under CC BY 4.0 by the author.