실용주의 프로그래머 읽어보기 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.