본문 바로가기

코딩 스탠다드/코드 컴플리트

[코드 컴플리트 2] 8장 방어적프로그래밍

방어적 프로그래밍: 다른 루틴의 잘못으로 인한 것이라도 루틴에 잘못된 데이터가 들어왔을 때 작성한 루틴에 아무런 문제가 발생하지 않도록 하는 것. 

 

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