회사에서 테스트 코드를 작성을 위해 성공, 실패 케이스 관련하여 테스트 케이스를 분류하고 있었다.
테스트 케이스를 분류하던 중 몇 가지 의문이 드는 지점들을 발견했는데
거의 비슷한 케이스의 코드 상황을 어떤 개발자는 조건문으로 처리하고 어떤 개발자는 try-catch를 사용하여 처리하였다.
이것을 보면서 회사 사람들과 예외에 관련해서 논의한 내용을 정리한다.
파이썬, 장고로 웹 서버를 개발하고 있는 상황에서 웹 개발자라면 예외를 피할 수 없는 상황들이 많이 있다고 생각한다.
우선 웹 서버를 구현하려면 네트워크, 서버, DB 이렇게 구축이 되어야 우리가 흔히 인식하고 있는 웹서버를 하나 만들었다고 할 수 있다.
보통 예외 처리라고 한다면 작성자가 처리할 수 없는 부분에서 발생하는 케이스들을 처리하기 위해 사용한다고 생각한다.
웹 서버를 만든다고 할 때 기본적으로 프레임워크를 사용한다면 많은 것을 대신해 주지만 그 외의 부분들은 개발자가 직접 예외 처리를 해줘야 한다.
네트워크 에러, DB 연결 및 쿼리 시 에러 등 해당 케이스들은 개발자가 임의 처리하기 힘든 부분이며 같이 프로덕트를 만드는 사람끼리 논의를 통해 정해야 된다고 생각한다.
따라서 이런 부분에선 예외 처리가 필수로 들어가야 된다고 생각한다.
그럼 네트워크와 DB 호출 상황을 제외한 상황에서 예외 처리가 필요 없을까?
딱 잘라서 말할 순 없지만 외부 라이브러리 호출이 아니라면 필요 없다고 생각한다.
사실 웹 서버를 구축해서 네트워크에 문제가 없고 DB 관련 호출 시 문제가 없었다면 2가지 케이스로 나뉜다고 생각한다.
1. 내가 작성하지 않은 부분에서 발생한 버그
2. 내가 작성한 부분에서 발생하는 버그
1번의 경우 외부 라이브러리가 아니라면 작성한 개발자를 문책한다. 테스트 코드 작성을 통해 충분히 막을 수 있다고 생각한다.
파이썬을 사용하기에 코드 작성 중에 생각보다 AttributeError, IndexError가 많이 발생한다.
다만 이런 부분이 코드 작성 중엔 충분히 발생할 수 있으나 코드가 PR 되고 머지되는 과정에서 수정된 상태로 반영이 되어야 한다고 생각한다.
2번의 경우도 1번의 경우와 마찬가지라고 본다.
따라서 내부에서 작성된 코드들(보통의 경우 비즈니스 로직이라 볼 수 있다.)은 테스트 코드를 통해 막던지, 상호 리뷰를 통해 반드시 막아야 한다.
다만 예외를 안 쓰려고 하다 보니 조건문을 떡칠하게 되는 참사들이 발생하게 된다...
기존에 예외로 하던걸 조건문을 이용해서 덕지덕지 바르다 보니 차리리 예외 처리를 하는 게 더 편하다는 생각이 들 정도다...
그래서 우리는 assert를 이용해서 해당 문제를 해결해 보려고 한다.
아까 1번 케이스의 경우 내가 작성하지 않은 함수 혹은 클래스에서 발생하는 경우가 대부분이다.
2번 케이스의 경우는 내가 작성한 함수 혹은 클래스에서 발생하는 경우가 대부분이다.
함수의 시그니처는 함수의 이름, 매개변수 개수, 매개변수 타입으로 볼 수 있다.
다만 파이썬은 동적 언어이므로 매개변수의 타입은 강제되지 않는다. 그럼에도 협업을 위해 type hint를 남겨서 명확하게 한다.
함수의 선조건은 이 함수를 실행하기 위해 필요한 조건이다.
예를 들면 int를 받지만 자연수만 받아야 할 경우와 같다.
함수의 후조건은 선조건의 만족된 상태에서 함수의 기능이 정상 동작하는 것이다.
우선 함수의 시그니처는 정말 특별한 경우가 아니라면 바꾸지 않는 것을 원칙으로 했다.
누군가가 참조해서 쓰고 있다면 충분한 소통 후 변경을 기본으로 하고 해당 부분은 리뷰를 통해 최대한 막는 것으로 한다.
아니면 특정 프로젝트 하위 디렉토리에 넣고 공통 디렉토리에 넣는 것은 지양하자고 정했다.
예외를 쓰지 않는 대신 조건문 떡칠의 문제는 함수의 선조 건과 관련이 있는데
해당 문제는 assert를 활용하여 함수의 시작 부분에서 확인하도록 변경하였다.
# 나누기 함수가 존재한다고 가정했을 때 분모에 0이 들어오는 것을 조건문이 아닌 assert를 이용해 막는다.
def divide(numerator: int, denominator: int) -> int:
assert(denominator > 0, "분모는 0보다 커야합니다.")
return numerator // denominator
함수의 후조건의 경우 assert로도 가능하지만 assert로 확인하기에 케이스가 방대한 경우가 더 많기 때문에
테스트 코드를 통해 방지하는 것으로 결정하였다.
사실 위의 예제 코드를 보면 조건문과 assert 차이점은 조건문의 인덴트가 한 번 더 들어가냐? 정도밖에 안 보인다.
추가로 assert에 걸리게 되면 AssertionError가 발생한다.
결국 이렇게 되면 외부에서 사용 시 AssertionError에 대한 핸들링이 필요한 거 아니야라는 생각이 들 수 있다.
조건문의 경우 조건을 걸어서 문제가 발생하면 다시 또 어떻게 처리할 것인가에 봉착하게 된다.
예외를 던질 것인가? 어떤 값을 리턴해서 이 함수에 문제가 있다고 알릴 것인가?
결국 조건문을 이용한 처리를 해도 어떻게 호출자에게 전달할 것인가라는 문제가 발생한다.
그래서 assert를 활용해야 된다고 생각하는데
만약 AssertionError가 발생하면 호출을 잘못한 거지 함수가 잘못된 게 아니기 때문이다.
문제의 케이스들은 외부 -> 내부 호출이 아니라 내부 -> 내부 호출인 경우기 때문에 이런 케이스까지 예외를 외부로 표출할 필요가 없다.
또한 assert의 경우 실행 옵션에 따라 주석처럼 무시가 가능하기 때문이다.
조건문을 활용해서 사용했을 경우 릴리스 버전 배포 시마다 주석 처리를 할 수도 없기 때문에 임의로 코드를 뺄 수가 없다.
하지만 assert의 경우 내부 -> 내부 호출 케이스는 테스트, QA, 상호 리뷰 과정에서 문제를 인지해서 충분히 수정이 가능하기 때문에
릴리스 버전 배포 시 해당 코드를 무력화시키고 나가도 큰 문제가 없다고 보증할 수 있다.
python의 경우 -O, -OO 옵션을 실행 시 넣으면 assert 문이 무시된다.
파이썬 공식 문서(-OO로 전체 검색)
맞다. 릴리스 버전에서 우리가 캐치하지 못한 혹은 악의적인 목적으로 호출을 해버릴 경우 assert 문이 주석 처리됐기 때문에
막을 수 없다.
따라서 이렇게 핸들링은 외부 -> 내부 호출을 받는 과정에서 최대한 배제하고 내부 -> 내부 호출에서만 활용하는 것이 좋다.
또한 캐치하지 못한 케이스로 인해 발생한 경우 테스트 코드 추가와 QA 과정에서 추가 검증을 하여 지속적으로 보완해 나가야 한다.
assert가 만능은 아니다.
다만 무의미한 예외 처리와 조건문 남발보다는 코드상에서 확실한 구분을 위해 필요하다고 생각한다.
개발자가 개발하는 과정에서 exception이 발생하면 그것도 함수 혹은 클래스 호출 시 발생하면 해당 코드를 확인할 수밖에 없다.
이러한 수고로움 줄이기 위해 함수의 목적과 기능을 명확히 하고 엉뚱한 호출을 방지하기 위해라도 assert 도입이 필요하다고 생각한다.
그래서 비즈니스 로직만큼은 개발자들끼리 바보 같은 실수를 하지 않게 assert를 도입해서 막아야 한다고 생각한다.