방어적 프로그래밍: 다른 루틴의 잘못으로 인한 것이라도 루틴에 잘못된 데이터가 들어왔을 때 작성한 루틴에 아무런 문제가 발생하지 않도록 하는 것.
8.1 잘못된 입력으로부터 프로그램 보호
- 외부로부터 들어오는 모든 데이터의 값을 검사하라.
- 데이터가 허용가능한 범위안에 있는지 확인
- 문자열이 목적에 부합되는 타당한 값인지
- 시스템을 공격하려는 데이터(버퍼 오퍼플로 시도, SQL 명령문 주입, HTML이나 XML코드 주입, 정수 오버플로, 시스템 호출에 전달되는 데이터)
- 잘못된 입력을 어떻게 처리할 것인지를 결정하라.
8.2 어설션
- 루틴이나 매크로 실행시 프로그램이 스스로 검사할 수 있도록 사용하는 코드. 참이면 예상되로 작동 중.
- 크고 복잡한 프로그램과 높은 신뢰도를 보장해야하는 프로그램에서 특히 유용
- 예상치 못한 조건을 찾아내기위해.
- 어설션은 일반적으로 개발버젼에서는 코드에 포함되어 컴파일 되지만 제품에서는 제외.
<Details>
(다음과 같은 가정검사에 사용 가능)
- 입력(또는 출력) 매개변수의 값이 예상된 범위 안에 들어가는지
- 파일이나 스트림이 루틴이 시작할 때(또는 끝날 때) 열려 있는지(또는 닫혀 있는지)
- 파일이나 스트림이 루틴이 시작할 때(또는 끝날 때) 시작(또는 끝)에 있는지
- 파일이나 스트림이 읽기 전용이나 쓰기 전용, 읽기/쓰기로 열려 있는지
- 입력만 가능한 변수의 값이 루틴에 의해서 변경되지 않는지
- 포인터가 Null이 아닌지
- 루틴에 전달되는 배열이나 다른 컨테이너가 적어도 X개의 데이터 요소를 포함할 수 있는지
- 테이블이 실제 값을 포함할 수 있도록 초기화 되었는지
- 컨테이너가 루틴이 시작할 때(또는 끝날 때) 비어(또는 채워)있는지
- 매우 최적화되어 있고 이해하기 어려운 루틴의 결과가 수행 속도는 느리지만 이해하기 쉬운 루틴의 결과와 일치하는지.
<사용지침>
- 발생이 예쌍되는 상횡에 대해서는 오류 처리코드를 사용하되, 절대로 발생해서는 안되는 조건에 대해서는 어설션을 사용하라.
- 실행할 가능성이 있는 코드를 어설션 내에 입력하지 ㅇ낳는다.
- 어설션 기능을 사용하지 않을 때 컴파일러가 코드를 제거할 확률이 높아진다.
- 선행조건과 후행조건을 문서화 하고 검증하는데 어설션을 사용하라.
- 매우 견고한코드를 작성하기 위해서는 어설션은 무조건 포함하고 그 다음에 오류를 처리하라.
- 둘중에 하나가 아닌 둘다.
<Ex JAVA>
assert denominator != 0: "denominator is unexpedly equal to 0.";
8.3 오류 처리 기법
- 중립적인 값을 반환한다.
- 수식이라면 0을 반환, 문자열이라면 빈 문자열.
- 다음에 오는 유효한 데이터로 대체한다.
- 이전과 같은 값을 반환한다.
- 가장 가까운 유효한 값으로 대체한다.
- 경고 메시지를 파일에 기록한다.
- 경고 메시지를 파일에 기록한 다음에 계속해서 실행하는 방법을 선택 할 수 있다.
- 오류 코드를 반환한다.
- 오류가 감지되었다는 것을 보고하고 호출 계층의 위에 있는 다른 루틴이 오류를 처리할 것이라고 믿는 것.
<Detail 구체적 매커니즘들>
- 상태 변수에 값을 설정한다.
- 함수의 리턴 값으로 상태 값을 반환한다.
- 프로그래밍 언어에서 기본 제공하는 예외 매커니즘을 사용하여 예외를 던진다.
- 오류 처리 루틴이나 객체를 호출한다.
- 장점: 오류를 처리해야하는 부분이 집중 될 수 있어서 디버깅이 쉬워진다.
- 단점1 : 전체 프로그램의 중심이 되는 기능이 이 기능과 밀접하게 결합할 것.
- 단점2: 시스템 코드를 재사용 원할때 오류 처리 코드도 가져와야 -> 버퍼오버런 발생시 공격자가 핸들러 루틴이나 객체 주소 손상 가능
- 오류가 발생한 곳에서 오류 메시지를 출력한다.
- 일관성 있는 사용자 인터페이스를 작성해야 할 때, UI 시스템의 나머지 부분을 분명하게 구분하려할때,
- 소프트웨어를 다른 언어로 지역화하려 할떄 어려움 겪을 수도 있음.
- 시스템의 잠재적인 공격자에게 너무 많은 내용을 알려주는 것을 주의
- 상황에 따라 가장 잘 작동하는 방법으로 오류를 처리한다.
- 모든 오류를 상황에 맞게 처리.
- 각 개발자에게 엄청난 유연성 제공하나 시스템 전체적 성능이 정확성과 견고성 관련 요구사항 만족 시키지 못하는 위험.
- 종료한다
- 안전성이 중요한 응용 프로그램은 견고함 보다 정확성. 잘못된 결과 보다 아무것도 반환 하지 않는게 좋음.
- 개인용 응용프로그램은 정확성보다 견고함. 일반적으로 결과가 있는 것은 소프트웨어가 종료되는 것보다 좋다.
- 오류 처리 방법은 많으므로 프로그램 전체에서 일관된 방법으로 따라야 한다.
8.4 예외
- 예외: 코드가 오류나 예외적인 이벤트를 루틴을 호출한 코드에 전달할 수 있는 특수한 방법.
- 신중한 사용으로 복잡성을 줄일 수도 있으나 무분별하게 사용시 이해하기 거의 불가능한 코드를 만들 수도 있다.
- 예외를 사용해 무시되어서 안되는 오류를 프로그램의 다른 부분에 알린다.
- 예외의 가장 큰 이득은 절대로 무시하지 못할 방법으로 오류가 발생한 상황을 알릴 수 있는 능력.
- 정말로 예외적인 조건인 경우에만 예외를 던져라
- 복잡성 증가 사이에서 균형
- 또한, 예외는 루틴을 호출하는 코드가 호출된 코드의 내부에서 어떤 예외 던질지 알아야해 캡슐화 약화(복잡성 관리 반함.)
- 책임을 전가하기 위해서 예외를 사용하지 않는다.
- 생성자와 소멸자에서 예외를 잡을 수 없다면 생성자와 소멸자에서 예외를 던지지 않는다.
- 올바른 추상화 수준에서 오류를 던진다.
<EX>
class Employee {
public TaxId GetTaxId() throws EOFException{ -> Employee클래스의 세부적인 루틴 이해 하도록 해서 캡슐화 깨짐.
}
가 아닌
public TaxIdGetTaxId() throws EmployeeDataNotAvailable{
}
}
- 예외를 발생시킨 모든 정보를 예외 메시지에 포함.
- 비어있는 catch블록을 피한다. (적어도 설명이나 로그나 파일로 기록.)
- 라이브러리 코드가 던지는 예외를 파악
- 중앙 집중화된 예외 보고 시스템 구축을 고려
- 프로젝트의 예외 사용을 규격화
- 정말로 예외처리가 필요한지 고려
- 실행 오류에 대한 가장 좋은 응답은 습득한 모든 자원을 해제하고 픅로그램 멈추는 것.
- 예외의 대안을 고려
<Details 대안 예시>
- 오류를 직접 처리
- 오류 코드를 사용하여 오류를 전달
- 디버그 정보를 파일로 기록
- 시스템 종료.
=> 예외로 처리시 언어에 의존하는 전형적인 예.
8.5 오류로 인한 손해를 막기 위한 방책.
- 유효성을 검증하는 바리케이드 역할을 하는 클래스들을 만든다.
- 데이터를 입력할때 적절한 타입으로 변환한다.
- 이를 통해 아키텍처 수준에서 오류를 처리할수 있다.
- 또한, 어설션과 오류 처리를 확실하게 구별할 수 있다.
- 방어 시설의 외부에 있는 루틴은 데이터에 대해 확실하게 가정할 수 없기에 오류 처리를 사용.
- 방어 시설의 내부에 있는 루틴은 어설션을 사용하여 데이터 살균후 내보내기.
8.6 디버깅 보조 도구
- 제품의 제약 사항을 개발 버전에 무의식적으로 적용하지 않는다.
- 개발 버전은 안전망 없이 사용할 수 있는 연산을 추가로 가져가되 됨.
- 개발 중에는 개발을 좀 더 원활히 진행 하도록 도와주는 도구 사용에 속도와 자원 양보
- 디버깅 보조 도구를 초기에 도입
- 공격적인 프로그래밍 기법을 사용한다.
- 예외적인 경우 개발 중에 눈에 띄어야 하고 배포 버전에서는 복구가 가능한 방법으로 처리 되어야 함.
- 개발 중에는 눈에 최대한 띄도록 그리고 제품에서는 에러 로그 파일에 작성 같이 우아한 처리.
<Ex 공격적 프로그래밍>
- assert가 프로그램을 중단하게 한다.
- 메모리 할당 오류를 발견할 수 있게 할당된 모든 메모리를 완벽하게 채운다.
- 파일 형식과 관련된 오류를 발견하기 위해서 할당된 파일이나 스트림을 완벽하게 채운다.
- 객체를 삭제하기 전에 쓰레기 데이터로 채운다.
- 개발하고 있는 소프트웨어에 적합하다면 배포된 소프트웨어에서 어떤 오류가 발생하고 있는지 확인 할 수 있게 오류 로그 파일을 이메일로 보내도록 프로그램 설정.
- 디버깅 보조도구를 제거하는 계획을 세운다.
- 버전 관리 도구와 ant나 make같은 빌드 도구를 사용한다.
- 기본 제공되는 전처리기를 사용한다.
- 자신만의 전처리기를 작성한다.
- 디버깅을 위한 루틴을 작성한다. (개발시는 여러 연산. 제품에는 호출쪽으로 곧바로 제어 넘기거나 간단한 스텁루틴으로 대체)
8.7 제품 코드를 얼마나 방어적으로 프로그래밍 할 것인지 정하기
- 제품 개발시에는 가능한 오류가 보이지 않도록 하고 프로그램이 복구되거나 품위있게 실패하는 편이 나음.
- 중요한 오류를 검사하는 코드는 남겨두라.
- 사소한 오류를 검사하는 코드를 제거하라.
- **"제거"는 물리적으로 코드 제거가 아니라 특정한 코드를 제외하고 프로그램을 컴파일 하는 기법을 사용하거나 로그 파일 이용.
- 심각한 충돌을 발생시키는 코드를 제거하라.
- 개발 시에는 눈에 띄게 충돌 시키는게 좋지만 충돌 전 프로그램이 작업을 저장 할 수 있도록 하자. 그렇지 못하면 코드 제거.
- 프로그램이 우아하게 충돌하도록 돕는 코드를 남겨두라.
- 작업을 모두 처리한 후 종료할 수 있도록 하기.
- 기술 지원을 위해서 오류를 기록.
- 제품 코드에 디버깅 보조 도구를 남기되, 작동 방식을 변경하지 않는 방법을 고려.
- 또는 파일에 남기기
- 오류 메시지가 친절한지 확인.
- 혹시 모를 고객에 대한 유출 때문에.
8.8 방어적 프로그래밍에 대해서 한 번 더 고민하기
- 방어적 프로그래밍이 지나치면 그 자체로 문제. 프로그램 비대해 지고 느려질 것.
- 방어적일 필요가 있는 곳을 생각해 본후 그에 따라 방어적 프로그래밍의 우선 순위 결정.
'코딩 스탠다드 > 코드 컴플리트' 카테고리의 다른 글
[코드 컴플리트 2] 7장 고급루틴 (0) | 2024.01.03 |
---|