소프트웨어를 개발하는 과정

소프트웨어는 대략적으로 기획 -> 설계 -> 구현 -> 테스트 -> 배포 단계를 거쳐 완성해나간다고 알고있다. 이 중에서 테스트는 정말 애매한데, 언제, 얼만큼, 누굴 위해 작성해야 하는지 알려주는 사람을 본 적이 없기 때문이다. 게다가 테스트코드를 작성해야한다고 말하면 찬성하는 사람도 거의 없었다.

더군다나 AI기술이 소프트웨어 개발에 편입되면서 테스트코드의 중요성이 훨씬 높아졌다고 생각하기 시작하면서 테스트코드가 얼마나 중요한지 더 확신할 수 있게 되었다. 이 문서에는 내 역량의 한계로 테스트 기법이나 노하우 등은 담을 수 없고, 다만 몇년동안 테스트 코드에대해 고민해본 내용을 적당히 정리해보기위해 작성한다.

테스트 코드는 누굴위해 만드는가

테스트 코드를 작성할 때 항상 드는 생각이 있었다. 어떤 코드를 테스트 해야 하는가. 폴더 구조에서 가장 상단에있는 소스 모듈부터 그냥 다 만들면 되는지. 작성중에 기획이 변경되거나 개발 계획이 틀어지거나, 개인적으로 코드 구현 과정에서 처음 생각한것과 달라졌을 때 테스트 코드를 어떻게 해야 할지.

특히나 현대의 웹서비스를 개발중일 때는 시장 트렌드도 변하고 요구도 변하고 사용자층도 너무나 빠르게 변하기 때문에 변경이 자주 발생하는 문제도 있다.

흔히 테스트 코드를 작성할 때는 커버리지도 함께 측정해야 한다고들 한다. 그리고 이 커버리지를 신경써서 개발하다보면 반드시 코드의 예외 케이스를 함께 고려할 수밖에 없다. 왜냐하면 그 부분을 빼먹으면 커버리지가 오르지 않기 때문에. 만약 예외 케이스를 처리하는 코드를 삭제하면, 코드는 실행중에 발생하는 예외에 대처할 수 없기 때문에 미완성으로 출시되게 된다.

그럼 이제 테스트는 요구사항을 안전하게 처리하기 위해 만드는 것이라고 정리해볼 수 있다.

웹서비스의 경우

프로그램은 반드시 User Interface가 있어야 한다. 그래야 사용자와 상호작용을 하면서 필요한 기능을 제대로 제공할 수 있기 때문이다. 웹서비스는 보통 3-tier에 layered architecture를 기본은로 하고 있는데, 각 layer 중에서도 서로 다른 플랫폼간의 데이터 교환이 발생하는 지점들이 있다. 일반적인 개발팀은 각 플랫폼 개발자들과 함께 그 지점들에 대한 원칙을 맞춘다. 이때 실제 개발해야하는 제품의 기능과 사용자층, 요구사항과 제약조건 들이 반드시 고려되어야 하기 때문에 이 시점에서 정상/비정상, 예상되는 예외케이스와 처리 절차 등을 정의하는 것이다.

Backend 영역에서

다른 모든 프로그램에 있는 대원칙인 [Input] -> [Processing] -> [Output] 이 여기에서도 마찬가지로 적용되기 때문에, 우리는 이 [Input]과 [Output]을 우선 타겟으로 삼아서 테스트 코드를 만들어야 하는 것이다. 어떤 형식의 입력을 넣었을 때 어떤 출력이 나오는지 여기서 결정할 수 있기 때문이다. 할 수 있다면 이 지점을 처리하는 최종 모듈의 커버리지는 100%에 맞추는 것이 좋다.

또 다른 지점은 DB와의 통신 구간이다. 여기는 코드가 자주 바뀌지 않지만, 오히려 자주 바꾸면 안되기때문에 변경을 감지하기위한 용도로 쓸 수 있다. 변경이 발생해서 테스트를 통과하지 못하면 변경했던 코드를 되돌려야 한다. 안그러면 코드를 변경하면서 DB를 함께 수정해야 하고, 이게 영향을 미칠 다른 위험요소를 반드시 고려해야 하기 때문에 개발시간이 말도안되게 늘어날 수 있기 때문이다.

Frontend 영역에서

UI가 존재하는 이 영역은 컨텍 포인트가 크게 두군데이다. 하나는 사용자가 직접 접근하는 지점과 Backend 서버와 통신을 위한 API호출 지점이다. API호출 지점은 Backend 영역과의 통신 프로토콜을 약속하고있기 때문에 HTTP 요청 스펙과 일치하는지 테스트하고 변경되지 않도록 보존하기 위한 목적이 있다.

사용자가 접근하는 UI는 위의 테스트 방법과 방식이 다를 수 밖에 없는데, 사용자가 상호작용을 시도할 것으로 예상되는 행동과 특정한 목적으로 사용하는 시나리오를 테스트 해야 한다. UI 테스트 또한 외부의 요구사항에 의해 변경되기 때문에 코드 내부를 유지보수하는 도중에 변경이 생기지 않도록 코드를 보호하는 목적으로 만들어야 한다.

Database

개발자인 내가 DB 엔진을 검증할 필요는 없지만 요구조건에 맞춰 구조가 갖춰져있고 DDL이 잘 정의되어있는지 스키마를 검증할 필요는 있다. 다양한 데이터구조가 있을 수 있고, 최근에는 Data lake 등의 아주 전통적인 데이터 저장소와는 다른 개념이 필요한 저장공간도 있기 때문에 상황에 맞춰 테스트 전략을 세워야 한다.

예를들어, 관계형 데이터베이스라면 테이블의 분포와 이름, 그리고 Entity의 정의(데이터 타입, 속성 등)와 Relation 간의 cascade 정책이 올바른지도 확인이 필요하다. 또한 데이터간의 관계가 의도한대로 갖춰져있는지 파악할 필요가 있는데, 잘 튜닝된 SQL 구문들이 있다면 그 구문에 대한 정합성을 검증하는 테스트가 필요하고, 그렇지 않고 코드 안에서 객체 모델로 데이터구조를 관리하고있다면 위의 영역과 마찬가지로 코드 수준에서 테스트해볼 수 있을 것이다. 관계형 데이터베이스는 데이터가 삽입되기 시작하면 구조를 변경하는 것이 매우 어렵기 때문에 초기의 설정과 구성에 대한 테스트를 마친 뒤에는 외부에서 Database로 저장되는 데이터가 올바른지 여부를 테스트하는 것이 주 목적이 되고, 논리적인 데이터 모델과 물리적 구조 사이에서 변환하는 로직이 특별한 요구사항의 추가/변경이 없을 때는 코드가 변경되지 않도록 보존하는 것을 목적으로 한다.

NoSQL의 경우는 좀 더 간편하다고 생각될수 있지만 입력되는 데이터 구조의 변경이 쉽다고 해서 검증이 필요없는 것은 아니다. 실수로 field key 이름에 오타가 섞이거나 없는 field를 나도 모르게 만들도록 코드를 수정할 수도 있다. 따라서 큰 규모의 데이터를 다룰 때 데이터 규격의 정합성을 검증하기 위해서는 스키마를 정의하는 저장공간을 따로 두거나, MongoDB 처럼 JSON Schema validation 과 같은 기능을 사용해서 검증할 수 있다. 테스트는 관계형 데이터베이스와 마찬가지로 데이터 삽입 때 정합성을 검증하는 코드 자체를 테스트하거나, 정의되어있는 데이터모델이 데이터 스키마 혹은 별도의 validation 규칙을 만족하는지 테스트하고 이 테스트 또한 명시적인 업데이트가 이뤄지기 전까지는 바뀌지 않아야 한다.

결론

문서에서는 아키텍처가 복잡하면서 많이 다뤄지고있는 일반적인 웹서비스의 3-tier 아키텍처를 대상으로 이리저리 작성을 했는데, 내부 로직을 추가로 검증하기위한 테스트 코드는 당연히 많이 만들어져서 설계 단계에서 예상하는 입력 값의 범위 안에서 내부 프로세스의 커버리지를 충분히 채울 수록 코드에 대한 신뢰가 쌓인다. 또한 잘 배포된 시스템의 내부에 버그를 제거하거나 기능을 추가할 때, 기존에 만들어져있던 기능들이 최소한 endpoint 수준에서 기획, 설계 과정에서 합의한 내용을 벗어나지 않는지 빠르게 검증하기 위해서 생각할 수 있는 최소한의 테스트 코드를 지침처럼 만들어봤다.

전체적인 내용을 한번 더 요약한다면 하나의 서비스단위를 생각했을 때 서비스와 서비스, 서비스와 유저, 그리고 서비스와 외부의 자원간에 만들어지는 프로토콜 정도는 테스트를 해야 한다고 할 수 있다. 현실적으로 그보다 더 내부에있는 모듈들은 중요도에따라 테스트 우선순위를 생각할 수 있지만 필수적으로 할 필요는 없고, 오히려 interface를 기준으로 테스트 코드를 작성한 뒤 커버리지에서 벗어나는 코드들은 과감히 쳐낼 필요도 있다.

기획이 너무 빠르게 변해서 테스트코드를 모두 변경할 수 없다고 해도 가능하면 이정도는 하는게 최근에 LLM을 이용해서 코드를 작성할 때 도움이 되었던 경험이있었는데 계속 테스트를 해볼 예정이다.


This site uses Just the Docs, a documentation theme for Jekyll.