검증의 책임을 어디에 둘 것인가?
이번 미션에서 가장 깊이 고민한 부분은 “검증을 어디서, 어떻게 수행해야 하는가”였다. 이전 미션에서도 한 번 다뤘던 주제였지만, 이번에는 검증 항목이 훨씬 다양해지면서 그 중요성이 더 크게 다가왔다. 처음에는 하나의 검증 함수에 모든 조건을 담으려 했지만, 검증의 종류가 늘어날수록 로직이 복잡해졌고, 결국 세부적인 검증 함수로 분리하게 되었다. 이 과정에서 중복되는 로직을 공통화하여 재사용하도록 개선했지만, 구조가 깔끔해질수록 새로운 의문이 생겼다. 바로 “검증은 과연 어디에서 수행해야 하는가?”였다.
초기에는 입력 단계에서 모든 검증을 처리하려 했다. 사용자 입력 오류가 대부분의 문제를 일으킨다고 생각했기 때문이다. 그러나 테스트 코드를 작성하면서 Lotto 객체 내부에도 자체 검증이 존재한다는 점을 보고 관점을 바꾸게 되었다. 입력 단계의 검증은 “사용자의 입력값이 올바른가”를 확인하는 것이고, 도메인 내부의 검증은 “이 객체가 스스로의 무결성을 유지할 수 있는가”를 보장하는 것이다. 이 차이를 인식한 뒤, Lotto 객체는 번호 중복 여부나 숫자의 개수처럼 객체의 유효성을 보장하는 최소한의 검증만 담당하도록 수정했다. 반면 입력값 자체의 오류나 형식 문제는 입력 계층에서 처리하도록 명확히 분리했다.
이 과정을 통해 나만의 기준을 세울 수 있었다.
“도메인의 무결성을 지키는 검증은 도메인에서 수행하고, 행위나 입력 과정에서 발생하는 검증은 해당 계층에서 수행한다.”
하지만 테스트를 진행하면서 또 다른 문제가 드러났다. 잘못된 입력이 들어왔을 때 프로그램이 단순히 종료되는 것이 아니라 다시 입력을 받아야 한다는 요구사항이 있었다. 이를 해결하기 위해 입력 계층에서 try-catch와 재귀 호출을 통해 반복 입력을 시도했지만, 그 방식은 입력 객체 내부에서 프로그램의 흐름이 순환하는 문제를 만들었다.
이 문제를 해결하기 위해 에러를 컨트롤러에서 받아 처리하고, 입력 함수를 다시 호출하는 구조로 변경했다. 결과적으로 검증 로직의 위치를 명확히 분리하면서도 프로그램의 흐름이 자연스럽게 이어지도록 만들 수 있었다. 입력 단계에서의 유효성 검증과 도메인 내부의 비즈니스 규칙 검증을 완전히 분리하는 과정은 쉽지 않았지만, 그 덕분에 같은 검증이라도 메시지의 주체와 맥락에 따라 다르게 처리해야 한다는 점을 배웠다. 입력 오류와 도메인 제약 위반은 모두 검증 실패이지만, 사용자에게 전달해야 하는 메시지는 전혀 다르기 때문이다. 이번 경험을 통해 “검증의 위치와 책임을 명확히 정의하지 않으면 코드가 쉽게 복잡해지고 유지보수가 어려워진다”는 사실을 확실히 깨달았다.
테스트를 작성하는 이유
테스트를 작성하는 이유는 단순히 코드의 정답을 확인하기 위해서가 아니라, 코드가 의도한 대로 작동하는지를 즉각적으로 검증하기 위해서라고 생각한다. 단위 테스트를 작성하면서 각 함수가 제대로 동작하는지를 빠르게 확인할 수 있었고, 그 과정에서 코드의 구조와 의도를 더욱 명확히 이해할 수 있었다. 문제를 작은 단위로 쪼개어 테스트하다 보니 오류의 원인을 빠르게 찾아낼 수 있었고, “큰 단위의 테스트보다 작은 단위의 테스트가 더 효과적이다”라는 피드백의 의미를 직접 체감할 수 있었다.
테스트를 진행하면서 특히 인상 깊었던 점은, 단순히 “에러가 발생하는가”를 확인하는 수준을 넘어 놓쳤던 요구사항을 테스트를 통해 발견했다는 것이다. 예를 들어, 사용자가 잘못된 입력을 했을 때 프로그램이 단순히 종료되는 것이 아니라 다시 입력을 받아야 한다는 요구사항을 테스트를 통해 명확히 인식하게 되었다. 테스트를 작성하기 전에는 “입력 오류 → 종료”가 자연스러운 흐름이라고 생각했지만, 실제 시나리오를 테스트로 구현해보니 프로그램이 한 번의 실패로 끝나는 것은 사용자 경험상 부자연스럽다는 점을 깨달았다.
이를 해결하기 위해 입력 계층에서 에러를 감지한 뒤, 컨트롤러에서 재입력을 유도하는 구조로 변경했다. 이후 테스트 코드를 통해 그 흐름이 실제로 정상 작동하는지도 검증했다. 이처럼 테스트는 기능의 정답 여부를 확인하는 도구를 넘어, 요구사항이 올바르게 반영되었는지를 검증하고, 문서에 명시되지 않은 암묵적 요구사항까지 드러내는 과정이 되었다. 또한 통합 테스트를 작성하면서 전체 프로그램의 흐름이 유기적으로 잘 이어지는지, 그리고 재입력이나 예외 상황에서도 정상적으로 동작하는지를 확인할 수 있었다. 예를 들어, 로또 번호를 잘못 입력했을 때 프로그램이 중단되지 않고 안내 메시지와 함께 다시 입력을 요청하는지 여부를 테스트로 검증했다. 이를 통해 테스트는 단순히 코드의 결과를 확인하는 수단이 아니라, 사용자 시나리오 전체를 검증하는 실질적인 요구사항 테스트 도구라는 확신을 얻었다.
처음 README를 작성할 때는 어떤 기능이 필요하고 어떤 예외가 발생할지를 예상하며 계획을 세웠다. 그러나 실제 구현과 테스트를 반복하면서 그 예상이 점점 구체화되고 보완되는 과정을 경험했다. 결국 “README에서 예측 → 구현에서 실현 → 테스트에서 검증 및 보완”으로 이어지는 순환이 완성되었고, 이 과정을 통해 테스트의 진정한 목적은 단순한 검증이 아니라 요구사항을 구체화하고 성장의 방향을 제시하는 학습 도구임을 깨달았다.
단위테스트와 통합테스트..?
이번 미션을 진행하면서 단위 테스트와 통합 테스트의 역할과 경계에 대해 깊이 고민하게 되었다. 피드백에서 “작은 단위부터 시작해 점차 큰 단위로 확장하라”는 조언을 받았고, 이를 바탕으로 가장 작은 단위인 함수별 테스트부터 작성하기 시작했다. 처음에는 각 함수가 의도한 대로 작동하는지를 확인하는 수준이었지만, 점차 규모를 확장하면서 도메인 단위, 서비스 단위, 그리고 컨트롤러 단위의 통합 테스트로 이어지는 계층적 구조를 구축했다. 이 과정을 통해 작은 단위의 테스트가 쌓여 전체 시스템의 안정성을 보장하는 구조로 발전한다는 점을 직접 체감할 수 있었다.
테스트 단계를 확장하면서 자연스럽게 “상위 테스트에서 하위 단위를 어디까지 포함해야 하는가?”에 대한 고민도 생겼다. 예를 들어, 함수 단위에서는 그 함수의 내부 로직만 검증하면 되지만, 서비스 단위로 올라가면 이미 검증된 함수를 내부적으로 호출하는 구조가 된다. 이때 하위 함수까지 다시 검증해야 하는지, 아니면 그 함수의 신뢰성을 전제로 상위 로직만 테스트해야 하는지가 모호했다. 결국 단위 테스트는 ‘기능의 정확성’을, 통합 테스트는 ‘기능 간 연결과 흐름’을 검증한다는 원칙을 세웠다. 즉, 테스트는 중복 검증이 아니라 단위 간 신뢰를 전제로 상위 수준의 시나리오를 검증하는 구조로 나아가야 한다는 것이다.
이 원칙에 따라 테스트를 설계하면서 각 계층의 역할을 명확히 구분했다. 도메인 테스트에서는 각 도메인이 올바르게 생성되고 내부 규칙이 잘 적용되는지를 검증하는 데 집중했다. 예를 들어 Lotto 객체가 유효한 번호 6개로만 구성되는지, 중복된 값이 허용되지 않는지를 확인했다. 서비스 테스트에서는 도메인 간의 상호작용과 기능 단위 동작을 검증했다. 로또 발행이나 당첨 결과 계산처럼 실제 로직이 조합되는 과정을 테스트하면서, 도메인 간 연결이 정상적으로 이루어지는지를 확인했다. 마지막으로 컨트롤러 테스트에서는 프로그램의 전체 흐름을 통합적으로 검증했다. 사용자가 잘못된 입력을 했을 때 예외가 발생하고, 이후 재입력을 통해 다시 흐름이 이어지는 시나리오를 테스트함으로써, 단순히 코드의 동작 여부를 넘어 실제 사용자 경험에 가까운 흐름을 점검할 수 있었다. 결국 단위 테스트는 프로그램의 뼈대를 단단히 다지고, 통합 테스트는 그 뼈대 위에서 전체 시스템이 유기적으로 동작하는지를 보장하는 역할을 한다는 것을 배웠다. 두 테스트는 서로의 영역을 침범하지 않으면서도 서로를 전제로 보완하는 관계라는 점이 이번 미션을 통해 가장 크게 얻은 깨달음이었다.
'Programming' 카테고리의 다른 글
| 우테코 2주차 회고 (0) | 2025.11.26 |
|---|---|
| Discord에 500 Error Webhook으로 알림 보내기 (0) | 2025.11.26 |
| AOP랑 OOP랑 뭐가 다르지? (0) | 2025.09.26 |
| SOLID 원칙: 객체 지향 설계의 5가지 기본 원칙 (0) | 2025.09.18 |
