////
Search
Duplicate
🚋

8. 방어적 프로그래밍

방어적 프로그래밍이란 방어적인 태도를 유지하라는 것이 아닌 프로그래밍에 대한 것이다.
이 개념은 방어적 운전에서 비롯되었는데, 이는 다른 드라이버가 어떤 행동을 할지 항상 알 수 없다는 마음가짐을 가진다.
이렇게 함으로써 만약 그들이 위험한 행동을 한다면 내가 다치지 않도록 보호한다.
방어적 프로그래밍의 핵심 아이디어는 메소드를 다른 메소드로부터 지키는 것이다.

1. 잘못된 입력 값으로부터 프로그램을 보호하기

쓰레기를 넣으면 쓰레기가 나온다.라는 표현은 엄격한 책임주의를 소프트웨어 제품에 맞게 표현한 것이다.
소프트웨어에서 좋은 프로그램은 쓰레기를 넣더라도 쓰레기가 나와선 안 된다.
좋은 프로그램은 쓰레기를 넣으면 아무것도 안 나온다.쓰레기를 넣으면 오류 메시지를 출력한다. 또는 어떤 쓰레기도 허용되지 않는다.가되어야 한다.
엉성하고 안전하지 못한 프로그램만 쓰레기를 넣으면 쓰레기가 나온다.
다음은 잘못된 입력을 처리하기 위한 일반적인 방법들이다.
외부로부터 들어오는 모든 데이터의 값을 검사하라.
파일이나 사용자, 네트워크 등 외부 인터페이스로부터 데이터를 전달받을 때, 데이터의 값이 올바른지 검증하라.
숫자 값은 허용 범위 내에 있는지, 문자열은 처리할 수 있을 정도의 길이인지 확인하라.
메소드의 모든 입력 매개변수 값을 검사하라.
잘못된 입력을 어떻게 처리할 것인지를 결정하라.
유효하지 않은 매개변수를 발견했다면 어떻게 처리해야할까?
상황에 따라 8.3절에서 설명하는 내용을 참고해 처리방법을 적절히 결정할 수 있다.
방어적 프로그래밍은 품질 개선 기법의 보조 수단으로도 유용하다.
방어적인 코드 작성의 가장 좋은 형태는 처음부터 오류를 입력하지 않는 것이다.
반복적인 설계, 코드를 작성하기 전 의사코드 작성, 코드를 작성하기 전에 시나리오 작성, 저수준 설계에 대한 테스트 등은 모두 오류를 방지하는데 도움을 주는 활동이다.
이런 품질 개선 기법들은 방어적 프로그래밍보다 우선순위가 높게 할당되어야 한다.

2. 어설션

어설션은 대개 메소드 실행 시, 프로그램이 스스로를 검사할 수 있도록 사용하는 코드다.
크고 복잡한 프로그램과 높은 신뢰도를 보장해야 하는 프로그램에서 특히 유용하다.
인터페이스가 가정과 일치하지 않는 경우나 코드를 수정할 때 코드에 흘러들어 간 오류 등을 더욱 빠르게 찾아낼 수 있다.
일반적으로 두 개의 인자를 갖는데, 참이 되어야 하는 조건에 해당하는 표현식과 참이 아닐 경우 표시할 메시지를 갖는다.
다음은 변수 denominator0이 아닌 값으로 가정하고 있을 때의 어설션을 자바로 작성한 것이다.
assert denominator != 0 : "denominator is unexpectedly equal to 0.";
Java
복사
코드에서 가정한 것을 문서화하고 예상치 못한 조건을 찾아내기 위해서 어설션을 사용하라.
입력 매개변수의 값이 가정한 범위 안에 있는지
파일이나 스트림이 메소드가 시작할 때 열려있는지
일반적으로 배포되는 코드에서는 어설션 메시지를 사용자에게 보여주지 않는다. 어설션은 주로 개발과 유지보수에 사용하기 때문이다.

자신만의 어설션 메커니즘 구축하기

C++, Java를 포함한 많은 프로그래밍 언어가 어설션을 기본으로 제공한다.
자신만의 어설션 루틴을 구축하는 것은 언어의 제한을 받는 프로그래밍이 아닌 언어를 활용한 프로그래밍의 좋은 예다.

어설션 사용 지침

발생이 예상되는 상황에 대해서는 오류 처리 코드를 사용하되 절대로 발생해서는 안 되는 조건에 대해서는 어설션을 사용하라.
어설션은 절대로 발생해서는 안 되는 조건을 검사한다.
오류 처리 코드는 그렇게 자주 발생하지는 않지만 코드를 작성한 작성자가 예상하는 범위 내에 있고 제품 코드에서 처리해야 하는 비정상적인 환경을 다룬다.
오류 처리 코드로 처리하면 프로그램이 매끄럽게 상황을 처리하여 정상흐름으로 돌아가는 반면 어설션을 통해 처리하면 프로그램이 충돌하게 되어 문제를 해결하기 위해 새로운 버전을 배포해야 한다.
실행할 가능성이 있는 코드를 어설션 내에 두지 않는다.
코드를 어설션에 입력하면 어설션 기능을 사용하지 않을 때 컴파일러가 코드를 제거할 확률이 높아진다.
실행문은 별도의 줄에 입력하고 그 결과를 상태 변수에 할당한 다음, 상태 변수를 테스트하라.
선행 조건과 후행 조건을 문서화하고 검증하는 데 어설션을 사용하라.
선행 조건과 후행 조건이 사용될 때 각 메소드나 클래스는 프로그램의 나머지 부분과 하나의 계약을 형성한다.
선행 조건은 메소드나 클래스에서 다른 메소드를 호출하너가 객체를 생성하기 전 반드시 참이어야하는 행동이다. 이는 메소드를 호출하는 쪽에서 반드시 지켜야 한다.
후행 조건은 메소드나 클래스를 호출하고 난 후에 반드시 참이어야 하는 조건이다. 후행 조건은 호출된 코드나 클래스가 지켜야하는 약속이다.
어설션은 선행 조건과 후행 조건을 설명하기에 매우 유용하다.
매우 견고한 코드를 작성하기 위해서는 어설션은 무조건 포함하고 그다음에 오류를 처리하라.

3. 오류 처리 기법

이전과 같은 값을 반환한다.
온도를 읽는 소프트웨어가 만약 값을 읽지 못했다면 아마도 마지막에 읽었던 값과 같은 값을 반환할 것이다.
하지만 현금 인출기에서 거래를 인증하는 과정에서 자신의 은행 계좌를 불러오지 못했다고 해서 이전 사용자의 은행 계좌를 불러오고 싶지는 않을 것이다.
경고 메시지를 파일에 기록한다.
잘못된 데이터를 감지하면 경고 메시지를 파일에 기록한 다음 계속해서 실행할 수도 있다.
이 접근 방법은 가장 가까운 유효값으로 대체하거나 다음에 오는 유효값으로 대체하는 기법같이 다른 기법과 함께 사용될 수 있다.
로그를 사용할 것이라면 공개할 수 있는지 혹은 암호화해서 보호해야 하는지 고려해보도록 한다.
오류 코드를 반환한다.
시스템의 특정한 부분만 오류를 처리하도록 할 수 있다. 다른 부분은 오류를 처리하지 않고 오류가 감지되었다는 것을 보고하고 호출 계층 어딘가의 메소드가 오류를 처리할 것이라고 믿는다.
시스템의 나머지 부분에 오류가 발생했음을 알리는 구체적인 메커니즘은 다음 중 하나일 수 있다.
상태 변수에 값을 설정한다.
함수의 반환 값으로 상태 값을 반환한다.
프로그래밍 언어에서 기본 제공하는 예외 메커니즘을 사용해 예외를 던진다.
이 방법은 구체적인 오류 보고 메커니즘보단 오류를 직접 처리할 것인지, 단순히 발생했다는 것을 보고만 할 것인지 결정하는 것이 더 중요하다.
보안이 중요하다면 호출 메소드가 항상 반환 오류 코드를 검사하고 있는지 확인하라.
오류 처리 메소드나 객체를 호출한다.
오류 처리를 전역적인 오류 처리 메소드나 오류 처리 객체에 집중시키는 것이다.
장점은 오류를 처리해야하는 부분이 집중될 수 있어서 디버깅이 쉬워진다는 점이다.
단점은 전체 프로그램이 중심이 되는 이 기능에 대해서 알게 되고 그 기능과 매우 밀접하게 결합하게 된다는 점이다.
오류가 발생한 곳에서 오류 메시지를 출력한다.
이 방법은 오류 처리의 오버헤드를 최소화한다. 하지만 사용자 인터페이스 메시지가 전체 응용 프로그램에 영향을 끼치게 된다.
또한 시스템의 잠재적인 공격자에게 너무 많은 정보를 알려주는 것을 주의해야 한다.
상황에 따라 가장 잘 작동하는 방법으로 오류를 처리한다.
종료한다.
어떤 시스템은 오류를 발견할때마다 종료된다. 이는 안전성이 매우 중요한 응용 프로그램에서 유용하다.

견고함 대 정확성

오류 처리의 방식은 일반적으로 소프트웨어의 종류에 따라서 달라진다.
정확성은 절대로 부정확한 결과를 반환할 수 없다는 것을 의미한다. 부정확한 결과를 반환하는 것보다 아무 결과도 반환하지 않는 것이 더 좋다.
견고함은 부정확한 결과를 만들어 내더라도 소프트웨어가 작동할 수 있도록 계속 무언가를 하는 것을 의미한다.

오류 처리를 위한 상위 수준에서의 설계

오류 처리 방법이 매우 많으므로 프로그램 전체에서 일관된 방법으로 유효하지 않은 매개변수를 처리해야 한다.
잘못된 매개변수에 대한 일반적인 접근 방법을 선택하는 것은 아키텍처 수준이나 상위 수준에서의 설계에 관한 결정이며 둘 중 한 수준에서 처리해야 한다.

4. 예외

예외는 코드가 오류나 예외적인 이벤트를 메소드를 호출한 코드에 전달할 수 있는 특수한 방법이다.
어떤 메소드에서 코드가 어떻게 처리해야 하는지를 모르는 예외적인 상황에 부딪히면 예외를 던진다.
오류가 발생한 상황을 전혀 인식하지 못하는 코드는 오류를 해석하고 오류를 처리할 수 있는 다른 영역에 제어를 넘긴다.
예외는 상속과 공통적인 특성을 갖는데, 신중하게 사용하면 복잡성을 줄일 수 있으나 무분별하게 사용하면 이해하기 불가능한 코드를 만들어낸다.
예외를 적절하기 사용하기 위한 팁은 다음과 같다.
책임을 전가하기 위해서 예외를 사용하지 않는다.
오류를 발생한 코드에서 처리할 수 있다면 직접 처리하는 게 좋다.
올바른 추상화 수준에서 오류를 던진다.
메소드는 메소드의 인터페이스에서 일관된 추상화를 제공해야 한다. 클래스도 마찬가지다.
메소드에서 발생하는 예외는 다른 데이터형과 마찬가지로 메소드 시그니처의 일부다.
예외를 발생시킨 모든 정보를 예외 메시지에 포함한다.
모든 예외는 코드가 예외를 던질 때 감지된 특정한 환경에서 발생한다.
만약 배열 인덱스 오류 때문에 예외가 던져졌다면 예외 메시지에 배열 인덱스의 최댓값과 최솟값, 잘못된 인덱스의 값이 포함되도록 한다.
비어있는 catch 블록을 피한다.
서드파티 라이브러리가 던지는 예외를 파악한다.

5. 오류로 인한 손해를 막기 위한 방책

방어적 프로그래밍에서 바리케이드를 치는 하나의 방법은 특정한 인터페이스를 안전한 지역으로 가는 경계로 사용하는 것이다.
안전한 지역의 경계를 지나는 데이터의 유효성을 검사하고 데이터가 유효하지 않다면 적절하게 대응한다.
이와 동일한 접근 방법을 클래스 수준에서도 사용할 수 있다.
클래스의 공개 메소드는 데이터가 안전하지 않다고 가정하고 데이터를 검사하고 깨끗하게 만들어 사용할 책임이 있다.
데이터가 클래스의 공개 메소드에 전달되고 나면 비공개 메소드는 그 데이터가 안전하다고 판단할 수 있다.

방어 시설과 어설션 사이의 관계

방어 시설의 외부에 있는 메소드는 데이터를 보증할 수 없기 때문에 오류 처리를 사용해야 한다.
방어 시설의 내부에 있는 메소드는 어설션을 사용해야 하는데, 메소드에 전달되는 데이터가 검증된 후 전달되기 때문이다.
방어 시설을 사용하면 아키텍처 수준에서 오류를 처리하는 방법을 결정할 수 있다. 방어 시설 내, 외부에 둘 코드를 결정하는 것은 아키텍처 수준에서의 결정이다.

6. 디버깅 보조 도구

방어적 프로그래밍의 또 다른 핵심은 오류를 빠르게 발견되는 데 도움이 되는 디버깅 보조 도구를 사용하는 것이다.

제품의 제약 사항을 개발 버전에 무의식적으로 적용하지 않는다

배포 버전은 빠르게 실행되어야 한다. 개발 버전은 느리게 실행되어도 괜찮다.
배포 버전은 자원을 아껴야 한다. 개발 버전은 자원을 마음껏 사용해도 된다.
배포 버전은 위험한 연산을 사용자에게 노출해선 안 된다. 개발 버전은 안전망 없이 사용할 수 있는 연산을 추가로 가져도 된다.
개발 중에는 개발을 좀 더 원활히 진행하도록 도와주는 도구를 사용하는 데 속도와 자원을 양보하도록 한다.

디버깅 보조 도구를 초기에 도입한다

보조 도구를 사용하면 프로젝트 전반에 도움을 줄 것이다.

공격적인 프로그래밍 기법을 사용한다.

예외는 개발 중에도 눈에 띄어야하고 배포되었을때는 정상 흐름으로 반환 후 처리되어야 한다.
이러한 접근 방법을 공격적인 프로그래밍이라고 했다.
다음은 공격적으로 프로그램을 작성할 수 있는 몇몇 방법들이다.
assert가 프로그램을 중단하게 한다. 고칠수 밖에 없도록 만들어라.
메모리 할당 오류를 발견할 수 있게 할당된 모든 메모리를 완벽하게 채운다.
파일 형식과 관련된 오류를 발견하기 위해서 할당된 파일이나 스트림을 완벽하게 채운다.
객체를 삭제하기 전에 쓰레기 데이터로 채운다.
가능하다면 배포된 소프트웨어에서 어떤 오류가 발생하고 있는지를 확인할 수 있도록 오류 로그 파일의 접근성을 올리는 방법들을 찾아보자.

요점정리

제품 코드는 “쓰레기를 입력하면 쓰레기가 나온다”라는 말보다 정교한 방법으로 오류를 처리해야 한다.
방어적 프로그래밍 기법은 오류를 찾거나 수정하기 쉽고 제품 코드에 손상을 덜 입힌다.
어설션은 특히 큰 시스템, 신뢰성이 높은 시스템, 빠르게 코드가 변경되는 시스템에서 오류를 초기에 발견하는 데 도움이 된다.
잘못된 입력 데이터를 처리하는 방법에 대한 결정은 오류 처리와 고수준 설계에서 핵심적인 결정 사항이다.
예외는 코드의 정상적인 흐름과 다른 차원에서 오류를 처리하는 방법을 제공한다. 예외는 조심스럽게 사용하면 개발자에게 유용한 도구이며 다른 오류 처리 기법과 견주어가며 사용해야 한다.