•
방어적 프로그래밍이란 방어적인 태도를 유지하라는 것이 아닌 프로그래밍에 대한 것이다.
•
이 개념은 방어적 운전에서 비롯되었는데, 이는 다른 드라이버가 어떤 행동을 할지 항상 알 수 없다는 마음가짐을 가진다.
◦
이렇게 함으로써 만약 그들이 위험한 행동을 한다면 내가 다치지 않도록 보호한다.
•
방어적 프로그래밍의 핵심 아이디어는 메소드를 다른 메소드로부터 지키는 것이다.
1. 잘못된 입력 값으로부터 프로그램을 보호하기
•
쓰레기를 넣으면 쓰레기가 나온다.라는 표현은 엄격한 책임주의를 소프트웨어 제품에 맞게 표현한 것이다.
◦
소프트웨어에서 좋은 프로그램은 쓰레기를 넣더라도 쓰레기가 나와선 안 된다.
◦
좋은 프로그램은 쓰레기를 넣으면 아무것도 안 나온다.나 쓰레기를 넣으면 오류 메시지를 출력한다. 또는 어떤 쓰레기도 허용되지 않는다.가되어야 한다.
◦
엉성하고 안전하지 못한 프로그램만 쓰레기를 넣으면 쓰레기가 나온다.
•
다음은 잘못된 입력을 처리하기 위한 일반적인 방법들이다.
◦
외부로부터 들어오는 모든 데이터의 값을 검사하라.
▪
파일이나 사용자, 네트워크 등 외부 인터페이스로부터 데이터를 전달받을 때, 데이터의 값이 올바른지 검증하라.
•
숫자 값은 허용 범위 내에 있는지, 문자열은 처리할 수 있을 정도의 길이인지 확인하라.
◦
메소드의 모든 입력 매개변수 값을 검사하라.
◦
잘못된 입력을 어떻게 처리할 것인지를 결정하라.
▪
유효하지 않은 매개변수를 발견했다면 어떻게 처리해야할까?
▪
상황에 따라 8.3절에서 설명하는 내용을 참고해 처리방법을 적절히 결정할 수 있다.
•
방어적 프로그래밍은 품질 개선 기법의 보조 수단으로도 유용하다.
◦
방어적인 코드 작성의 가장 좋은 형태는 처음부터 오류를 입력하지 않는 것이다.
◦
반복적인 설계, 코드를 작성하기 전 의사코드 작성, 코드를 작성하기 전에 시나리오 작성, 저수준 설계에 대한 테스트 등은 모두 오류를 방지하는데 도움을 주는 활동이다.
▪
이런 품질 개선 기법들은 방어적 프로그래밍보다 우선순위가 높게 할당되어야 한다.
2. 어설션
•
어설션은 대개 메소드 실행 시, 프로그램이 스스로를 검사할 수 있도록 사용하는 코드다.
◦
크고 복잡한 프로그램과 높은 신뢰도를 보장해야 하는 프로그램에서 특히 유용하다.
▪
인터페이스가 가정과 일치하지 않는 경우나 코드를 수정할 때 코드에 흘러들어 간 오류 등을 더욱 빠르게 찾아낼 수 있다.
◦
일반적으로 두 개의 인자를 갖는데, 참이 되어야 하는 조건에 해당하는 표현식과 참이 아닐 경우 표시할 메시지를 갖는다.
▪
다음은 변수 denominator를 0이 아닌 값으로 가정하고 있을 때의 어설션을 자바로 작성한 것이다.
assert denominator != 0 : "denominator is unexpectedly equal to 0.";
Java
복사
•
코드에서 가정한 것을 문서화하고 예상치 못한 조건을 찾아내기 위해서 어설션을 사용하라.
◦
입력 매개변수의 값이 가정한 범위 안에 있는지
◦
파일이나 스트림이 메소드가 시작할 때 열려있는지
•
일반적으로 배포되는 코드에서는 어설션 메시지를 사용자에게 보여주지 않는다. 어설션은 주로 개발과 유지보수에 사용하기 때문이다.
자신만의 어설션 메커니즘 구축하기
•
C++, Java를 포함한 많은 프로그래밍 언어가 어설션을 기본으로 제공한다.
•
자신만의 어설션 루틴을 구축하는 것은 언어의 제한을 받는 프로그래밍이 아닌 언어를 활용한 프로그래밍의 좋은 예다.
어설션 사용 지침
•
발생이 예상되는 상황에 대해서는 오류 처리 코드를 사용하되 절대로 발생해서는 안 되는 조건에 대해서는 어설션을 사용하라.
◦
어설션은 절대로 발생해서는 안 되는 조건을 검사한다.
◦
오류 처리 코드는 그렇게 자주 발생하지는 않지만 코드를 작성한 작성자가 예상하는 범위 내에 있고 제품 코드에서 처리해야 하는 비정상적인 환경을 다룬다.
◦
오류 처리 코드로 처리하면 프로그램이 매끄럽게 상황을 처리하여 정상흐름으로 돌아가는 반면 어설션을 통해 처리하면 프로그램이 충돌하게 되어 문제를 해결하기 위해 새로운 버전을 배포해야 한다.
•
실행할 가능성이 있는 코드를 어설션 내에 두지 않는다.
◦
코드를 어설션에 입력하면 어설션 기능을 사용하지 않을 때 컴파일러가 코드를 제거할 확률이 높아진다.
◦
실행문은 별도의 줄에 입력하고 그 결과를 상태 변수에 할당한 다음, 상태 변수를 테스트하라.
•
선행 조건과 후행 조건을 문서화하고 검증하는 데 어설션을 사용하라.
◦
선행 조건과 후행 조건이 사용될 때 각 메소드나 클래스는 프로그램의 나머지 부분과 하나의 계약을 형성한다.
▪
선행 조건은 메소드나 클래스에서 다른 메소드를 호출하너가 객체를 생성하기 전 반드시 참이어야하는 행동이다. 이는 메소드를 호출하는 쪽에서 반드시 지켜야 한다.
▪
후행 조건은 메소드나 클래스를 호출하고 난 후에 반드시 참이어야 하는 조건이다. 후행 조건은 호출된 코드나 클래스가 지켜야하는 약속이다.
◦
어설션은 선행 조건과 후행 조건을 설명하기에 매우 유용하다.
•
매우 견고한 코드를 작성하기 위해서는 어설션은 무조건 포함하고 그다음에 오류를 처리하라.
3. 오류 처리 기법
•
이전과 같은 값을 반환한다.
◦
온도를 읽는 소프트웨어가 만약 값을 읽지 못했다면 아마도 마지막에 읽었던 값과 같은 값을 반환할 것이다.
◦
하지만 현금 인출기에서 거래를 인증하는 과정에서 자신의 은행 계좌를 불러오지 못했다고 해서 이전 사용자의 은행 계좌를 불러오고 싶지는 않을 것이다.
•
경고 메시지를 파일에 기록한다.
◦
잘못된 데이터를 감지하면 경고 메시지를 파일에 기록한 다음 계속해서 실행할 수도 있다.
◦
이 접근 방법은 가장 가까운 유효값으로 대체하거나 다음에 오는 유효값으로 대체하는 기법같이 다른 기법과 함께 사용될 수 있다.
◦
로그를 사용할 것이라면 공개할 수 있는지 혹은 암호화해서 보호해야 하는지 고려해보도록 한다.
•
오류 코드를 반환한다.
◦
시스템의 특정한 부분만 오류를 처리하도록 할 수 있다. 다른 부분은 오류를 처리하지 않고 오류가 감지되었다는 것을 보고하고 호출 계층 어딘가의 메소드가 오류를 처리할 것이라고 믿는다.
◦
시스템의 나머지 부분에 오류가 발생했음을 알리는 구체적인 메커니즘은 다음 중 하나일 수 있다.
▪
상태 변수에 값을 설정한다.
▪
함수의 반환 값으로 상태 값을 반환한다.
▪
프로그래밍 언어에서 기본 제공하는 예외 메커니즘을 사용해 예외를 던진다.
◦
이 방법은 구체적인 오류 보고 메커니즘보단 오류를 직접 처리할 것인지, 단순히 발생했다는 것을 보고만 할 것인지 결정하는 것이 더 중요하다.
◦
보안이 중요하다면 호출 메소드가 항상 반환 오류 코드를 검사하고 있는지 확인하라.
•
오류 처리 메소드나 객체를 호출한다.
◦
오류 처리를 전역적인 오류 처리 메소드나 오류 처리 객체에 집중시키는 것이다.
▪
장점은 오류를 처리해야하는 부분이 집중될 수 있어서 디버깅이 쉬워진다는 점이다.
▪
단점은 전체 프로그램이 중심이 되는 이 기능에 대해서 알게 되고 그 기능과 매우 밀접하게 결합하게 된다는 점이다.
•
오류가 발생한 곳에서 오류 메시지를 출력한다.
◦
이 방법은 오류 처리의 오버헤드를 최소화한다. 하지만 사용자 인터페이스 메시지가 전체 응용 프로그램에 영향을 끼치게 된다.
◦
또한 시스템의 잠재적인 공격자에게 너무 많은 정보를 알려주는 것을 주의해야 한다.
•
상황에 따라 가장 잘 작동하는 방법으로 오류를 처리한다.
•
종료한다.
◦
어떤 시스템은 오류를 발견할때마다 종료된다. 이는 안전성이 매우 중요한 응용 프로그램에서 유용하다.
견고함 대 정확성
•
오류 처리의 방식은 일반적으로 소프트웨어의 종류에 따라서 달라진다.
◦
정확성은 절대로 부정확한 결과를 반환할 수 없다는 것을 의미한다. 부정확한 결과를 반환하는 것보다 아무 결과도 반환하지 않는 것이 더 좋다.
◦
견고함은 부정확한 결과를 만들어 내더라도 소프트웨어가 작동할 수 있도록 계속 무언가를 하는 것을 의미한다.
오류 처리를 위한 상위 수준에서의 설계
•
오류 처리 방법이 매우 많으므로 프로그램 전체에서 일관된 방법으로 유효하지 않은 매개변수를 처리해야 한다.
•
잘못된 매개변수에 대한 일반적인 접근 방법을 선택하는 것은 아키텍처 수준이나 상위 수준에서의 설계에 관한 결정이며 둘 중 한 수준에서 처리해야 한다.
4. 예외
•
예외는 코드가 오류나 예외적인 이벤트를 메소드를 호출한 코드에 전달할 수 있는 특수한 방법이다.
◦
어떤 메소드에서 코드가 어떻게 처리해야 하는지를 모르는 예외적인 상황에 부딪히면 예외를 던진다.
◦
오류가 발생한 상황을 전혀 인식하지 못하는 코드는 오류를 해석하고 오류를 처리할 수 있는 다른 영역에 제어를 넘긴다.
•
예외는 상속과 공통적인 특성을 갖는데, 신중하게 사용하면 복잡성을 줄일 수 있으나 무분별하게 사용하면 이해하기 불가능한 코드를 만들어낸다.
•
예외를 적절하기 사용하기 위한 팁은 다음과 같다.
◦
책임을 전가하기 위해서 예외를 사용하지 않는다.
▪
오류를 발생한 코드에서 처리할 수 있다면 직접 처리하는 게 좋다.
◦
올바른 추상화 수준에서 오류를 던진다.
▪
메소드는 메소드의 인터페이스에서 일관된 추상화를 제공해야 한다. 클래스도 마찬가지다.
▪
메소드에서 발생하는 예외는 다른 데이터형과 마찬가지로 메소드 시그니처의 일부다.
◦
예외를 발생시킨 모든 정보를 예외 메시지에 포함한다.
▪
모든 예외는 코드가 예외를 던질 때 감지된 특정한 환경에서 발생한다.
•
만약 배열 인덱스 오류 때문에 예외가 던져졌다면 예외 메시지에 배열 인덱스의 최댓값과 최솟값, 잘못된 인덱스의 값이 포함되도록 한다.
◦
비어있는 catch 블록을 피한다.
◦
서드파티 라이브러리가 던지는 예외를 파악한다.
5. 오류로 인한 손해를 막기 위한 방책
•
방어적 프로그래밍에서 바리케이드를 치는 하나의 방법은 특정한 인터페이스를 안전한 지역으로 가는 경계로 사용하는 것이다.
◦
안전한 지역의 경계를 지나는 데이터의 유효성을 검사하고 데이터가 유효하지 않다면 적절하게 대응한다.
•
이와 동일한 접근 방법을 클래스 수준에서도 사용할 수 있다.
◦
클래스의 공개 메소드는 데이터가 안전하지 않다고 가정하고 데이터를 검사하고 깨끗하게 만들어 사용할 책임이 있다.
◦
데이터가 클래스의 공개 메소드에 전달되고 나면 비공개 메소드는 그 데이터가 안전하다고 판단할 수 있다.
방어 시설과 어설션 사이의 관계
•
방어 시설의 외부에 있는 메소드는 데이터를 보증할 수 없기 때문에 오류 처리를 사용해야 한다.
•
방어 시설의 내부에 있는 메소드는 어설션을 사용해야 하는데, 메소드에 전달되는 데이터가 검증된 후 전달되기 때문이다.
•
방어 시설을 사용하면 아키텍처 수준에서 오류를 처리하는 방법을 결정할 수 있다. 방어 시설 내, 외부에 둘 코드를 결정하는 것은 아키텍처 수준에서의 결정이다.
6. 디버깅 보조 도구
•
방어적 프로그래밍의 또 다른 핵심은 오류를 빠르게 발견되는 데 도움이 되는 디버깅 보조 도구를 사용하는 것이다.
제품의 제약 사항을 개발 버전에 무의식적으로 적용하지 않는다
•
배포 버전은 빠르게 실행되어야 한다. 개발 버전은 느리게 실행되어도 괜찮다.
•
배포 버전은 자원을 아껴야 한다. 개발 버전은 자원을 마음껏 사용해도 된다.
•
배포 버전은 위험한 연산을 사용자에게 노출해선 안 된다. 개발 버전은 안전망 없이 사용할 수 있는 연산을 추가로 가져도 된다.
•
개발 중에는 개발을 좀 더 원활히 진행하도록 도와주는 도구를 사용하는 데 속도와 자원을 양보하도록 한다.
디버깅 보조 도구를 초기에 도입한다
•
보조 도구를 사용하면 프로젝트 전반에 도움을 줄 것이다.
공격적인 프로그래밍 기법을 사용한다.
•
예외는 개발 중에도 눈에 띄어야하고 배포되었을때는 정상 흐름으로 반환 후 처리되어야 한다.
◦
이러한 접근 방법을 공격적인 프로그래밍이라고 했다.
•
다음은 공격적으로 프로그램을 작성할 수 있는 몇몇 방법들이다.
◦
assert가 프로그램을 중단하게 한다. 고칠수 밖에 없도록 만들어라.
◦
메모리 할당 오류를 발견할 수 있게 할당된 모든 메모리를 완벽하게 채운다.
◦
파일 형식과 관련된 오류를 발견하기 위해서 할당된 파일이나 스트림을 완벽하게 채운다.
◦
객체를 삭제하기 전에 쓰레기 데이터로 채운다.
◦
가능하다면 배포된 소프트웨어에서 어떤 오류가 발생하고 있는지를 확인할 수 있도록 오류 로그 파일의 접근성을 올리는 방법들을 찾아보자.
요점정리
•
제품 코드는 “쓰레기를 입력하면 쓰레기가 나온다”라는 말보다 정교한 방법으로 오류를 처리해야 한다.
•
방어적 프로그래밍 기법은 오류를 찾거나 수정하기 쉽고 제품 코드에 손상을 덜 입힌다.
•
어설션은 특히 큰 시스템, 신뢰성이 높은 시스템, 빠르게 코드가 변경되는 시스템에서 오류를 초기에 발견하는 데 도움이 된다.
•
잘못된 입력 데이터를 처리하는 방법에 대한 결정은 오류 처리와 고수준 설계에서 핵심적인 결정 사항이다.
•
예외는 코드의 정상적인 흐름과 다른 차원에서 오류를 처리하는 방법을 제공한다. 예외는 조심스럽게 사용하면 개발자에게 유용한 도구이며 다른 오류 처리 기법과 견주어가며 사용해야 한다.