개발자가 코드를 다 짰다고 끝난 게 아닙니다. 사실 진짜 싸움은 지금부터죠. 바로 '테스팅(Testing)'입니다. 이번 포스팅에서는 소프트웨어 테스팅의 목적과 단계, 그리고 요즘 개발자들의 필수 덕목인 TDD(테스트 주도 개발)까지 확실하게 정리해 드립니다.
1. 프로그램 테스팅이란?
테스팅은 프로그램이 의도한 대로 동작하는지 확인하고, 사용하기 전에 결함을 발견하기 위해 수행하는 활동입니다.
1) 테스팅의 핵심
- 목적: 단순히 "잘 돌아간다"를 보여주는 것뿐만 아니라, "어디가 고장 났는지" 찾아내기 위함입니다.
- 한계: 테스팅은 에러가 존재함(Presence)을 증명할 수는 있지만, 에러가 없음(Absence)을 증명할 수는 없습니다. (아무리 테스트해도 우리가 못 찾은 버그는 있을 수 있다는 뜻!)
- V&V 프로세스: 테스팅은 더 큰 개념인 검증 및 확인(Verification & Validation) 프로세스의 일부입니다.
2) 테스팅의 두 가지 목표
- 유효성 만족 증명 (Validation Testing)
- "고객이 달라고 한 기능이 다 있나?"
- 개발자와 고객에게 소프트웨어가 요구사항을 충족함을 보여줍니다.
- 성공적인 테스트 = 시스템이 의도한 대로 작동함.
- 결함 발견 (Defect Testing)
- "혹시 이상한 짓 하면 뻗어버리나?"
- 소프트웨어의 동작이 올바르지 않거나 명세와 다른 상황을 찾아냅니다.
- 성공적인 테스트 = 시스템이 고장 나게 만듦 (버그를 찾았으니까 성공!)
여기서 최근 검증(Verification)과 확인(Validation)이 지켜지지 않아 문제가 된 "K사 ID 노출 이슈"를 살펴볼까요?'
문제 시나리오: K사는 오픈채팅방의 익명성을 보장한다고 했으나, 기술적 허점(임시 ID 생성 방식)으로 인해 해커가 실제 사용자 정보를 추적할 수 있었습니다. K사는 "설계된 대로 구현했으므로 기술적 오류는 없다"고 주장하지만, 사용자는 "익명성이 깨졌다"고 반발하는 상황
https://www.catchsecu.com/archives/22374
2024년 대기업 개인정보 문제…카카오톡, 카카오페이 개인정보 총정리 | 캐치시큐
안녕하세요. 캐치시큐입니다. 카카오의 개인정보 문제가 가장 큰 이슈였는데요. 카카오페이의 동의 없는 개인정보 제공과 카카오톡의 개인정보 유출 사건입니다. 카카오톡의 경우 국내 기업 중
www.catchsecu.com
K사가 의존성 높은(Dependable) 소프트웨어를 개발하지 못한 원인은 검증(Verification)과 확인(Validation)의 차이에서 찾을 수 있습니다.
- Verification(검증) 관점: K사는 초기 설계 명세서에 따라 임시 ID 생성 로직을 구현했습니다. 즉, "명세서대로 올바르게 만들었는가(Are we building the product right?)"에 대해서는 성공했다고 볼 수 있어, 회사 측은 문제가 없다고 주장하는 것입니다.
- Validation(확인) 관점: 그러나 사용자가 진정으로 원했던 요구사항은 '완벽한 익명성 보장'이었습니다. 결과적으로 개인정보가 유출되었으므로, "올바른 제품을 만들었는가(Are we building the right product?)"에 대한 Validation(확인)에는 실패한 것입니다.
따라서 K사는 명세서 준수 여부(Verification)에만 집중하고, 실제 사용자의 핵심 요구사항인 익명성이 보장되는지(Validation)를 충분히 검토하지 못한 것이 근본 원인입니다. 이렇듯 V & V test를 만족시키지 못하면 실제 사용자 요구사항을 반영하지 못해 해당 해당 서비스는 큰 손실로 이어질 수 있습니다.
2. 정적 검증 vs 동적 검증
소프트웨어를 검사하는 방법은 크게 두 가지로 나뉩니다.
1) 소프트웨어 인스펙션 (Software Inspection) - 정적 검증
- 방식: 프로그램을 실행하지 않고, 소스 코드나 문서를 눈으로 보고 분석합니다.
- 장점:
- 시스템을 실행하기 전(초기 단계)에 오류를 잡을 수 있습니다.
- 테스트하다가 하나 터지면 멈춰야 하는 동적 테스트와 달리, 한 번 검토할 때 여러 오류를 동시에 찾을 수 있습니다.
- 실무 예시: GitHub의 PR(Pull Request) 코드 리뷰, 정적 분석 툴(Lint) 사용.
개발을 한 두번 해봤다면 알게 모르게 Lint를 사용해봤을 것입니다. 여기서 Lint에 대해 조금 자세히 살펴보겠습니다.
Lint란?
- 정의: 소스 코드를 실행하지 않고(정적), 프로그래밍 오류, 버그, 스타일 오류, 의심스러운 구조 등을 찾아내는 도구입니다.
- 어원: 옷에 붙은 보풀(Lint)을 떼어내는 도구에서 유래했습니다. 코드에 붙은 지저분한 보풀(버그, 안 좋은 패턴)을 제거한다는 뜻입니다.
- 사용 목적:
- 버그 예방: 실행 전 컴파일러가 잡지 못하는 잠재적 오류(Null Pointer, 무한 루프 등) 식별.
- 코딩 컨벤션 준수: 팀원 간 코드 스타일 통일 (들여쓰기, 변수 명명법 등).
- 코드 품질 향상: 복잡도 관리, 사용하지 않는 변수 제거 등.
다음은 각 언어 생태계에서 가장 많이 쓰이는 '국룰' 도구들입니다.
① Python (파이썬)
파이썬은 인터프리터 언어라 실행 전 오류를 잡는 게 중요합니다.
- 대표 도구: Pylint, Flake8, Black(포맷터)
- 🔍 사용 예시 (Pylint):Pythonprint(add(1, 2))`
# bad_code.py
def add(x, y):
result = x + y # 변수 사용 후 반환 안 함 (Pylint 경고)
return
print(add(1, 2))
- Lint 실행 결과:
- R1710: Either all return statements in a function should return an expression, or none of them should. (inconsistent-return-statements) (함수가 값을 반환하다가 말다가 합니다. 일관성 있게 수정하세요.)
② JavaScript / TypeScript (자바스크립트)
웹 프론트엔드 생태계의 필수품입니다.
- 대표 도구: ESLint, Prettier(스타일)
- 🔍 사용 예시 (ESLint):JavaScript
// bad_code.js
var x = 10;
if (x == "10") { // 느슨한 비교(==) 사용 (ESLint 경고)
console.log("Equal");
}
- Lint 실행 결과:
- Expected '===' and instead saw '=='. (eq) (타입까지 비교하는 ===를 사용하세요. ==는 예기치 않은 버그를 유발합니다.)
③ Java (자바)
엔터프라이즈 환경에서 코드 품질과 스타일을 엄격하게 관리합니다.
- 대표 도구: SonarQube (종합 품질 관리), Checkstyle (스타일), PMD (버그 패턴), SpotBugs
- 🔍 사용 예시 (PMD):Java
// BadCode.java
public class BadCode {
public void doSomething() {
try {
// ... something
} catch (Exception e) {
// 아무것도 안 함 (Empty Catch Block)
}
}
}
- Lint 실행 결과:
- EmptyCatchBlock: Avoid empty catch blocks (예외를 잡고 아무 처리도 하지 않으면 에러 추적이 불가능합니다. 로그라도 남기세요.)
④ C / C++
메모리 관리와 미정의 동작(Undefined Behavior)을 잡는 데 집중합니다.
- 대표 도구: Clang-Tidy, Cppcheck
- 🔍 사용 예시 (Cppcheck):C++
// bad_code.cpp
void func() {
int a[10];
a[10] = 0; // 배열 범위 초과 (Out of bounds)
}
- Lint 실행 결과:
- error: Array 'a[10]' accessed at index 10, which is out of bounds. (배열 인덱스는 0~9까지입니다. 10번지에 접근하면 메모리 오염이 발생합니다.)
실무에서는 보통 2가지 방법으로 활용됩니다.
- IDE 연동: VS Code나 IntelliJ 같은 에디터에 Lint 플러그인을 설치하면 코드를 짤 때 실시간으로 빨간 줄을 그어주어 즉시 수정할 수 있습니다.
- CI/CD 파이프라인: GitHub Actions 등에 Lint 검사를 넣어두면, Lint를 통과하지 못한 코드는 아예 배포(Merge)되지 않도록 강제하여 코드 품질을 유지합니다.
2) 소프트웨어 테스팅 (Software Testing) - 동적 검증
- 방식: 실제 데이터를 넣고 프로그램을 실행(Run)해서 동작을 관찰합니다.
- 특징: 성능이나 신뢰성 같은 비기능적 요구사항은 실행해 봐야만 알 수 있으므로 테스팅이 필수적입니다.
3. 개발 테스팅 (Development Testing)
개발자가 코드를 짜는 동안(배포 전) 수행하는 모든 테스트 활동입니다.
1) 유닛 테스팅 (Unit Testing)
- 정의: 개별 객체나 메서드 같은 작은 단위(Unit)를 테스트합니다.
- 특징: 다른 부분과 격리하여 독립적으로 검사합니다.
- 코드 작성 구조: Setup, Call, Assertion, Tear-down
- 전략: 파티션 테스트(Partition Test), 가이드라인 테스트
실제로 유닛테스트는 특히 간단한 프로젝트에서 최소한의 테스트로 많이 사용하는 편입니다. 실제로 사용하는 테스트 코드 작성 구조와 테스트 전략에 대해 자세히 살펴봅시다.
Unit Test 구조
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
// 1. 테스트 대상 클래스 (실제 구현 코드)
class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// 2. 테스트 클래스
class CalculatorTest {
private Calculator calc;
// [1] Setup (준비): 각 테스트가 실행되기 직전에 실행됨
@BeforeEach
void setUp() {
System.out.println("\n[Setup] 계산기 인스턴스를 생성합니다.");
calc = new Calculator();
}
// [2] Call (호출) & [3] Assertion (검증)
@Test
void testAddTwoNumbers() {
System.out.println("[Test] 덧셈 기능을 테스트합니다.");
// [2] Call: 실제 메서드를 실행
int result = calc.add(3, 5);
// [3] Assertion: 결과가 예상값(8)과 같은지 검증
assertEquals(8, result, "3 + 5는 8이어야 합니다.");
}
// [4] Teardown (정리): 각 테스트가 끝난 직후에 실행됨
@AfterEach
void tearDown() {
System.out.println("[Teardown] 자원을 정리합니다.");
calc = null;
}
}
어떤 테스트 전략을 사용하던 보통 위의 코드처럼 4가지 구조(Setup, Call, Assertion, Teardown)를 가집니다.
- Setup (@BeforeEach)
- 테스트 메서드가 실행되기 전에 매번 자동으로 실행됩니다.
- calc = new Calculator();를 통해 깨끗한 상태의 객체를 준비합니다.
- Call
- calc.add(3, 5)를 호출하여 테스트하려는 실제 기능을 수행합니다.
- Assertion (assertEquals)
- assertEquals(기대값, 실제값) 메서드로 결과가 맞는지 확인합니다.
- 값이 다르면 테스트는 Fail(실패)로 기록됩니다.
- Teardown (@AfterEach)
- 테스트 메서드가 실행된 후에 매번 자동으로 실행됩니다.
- 사용한 객체를 초기화하거나, 열려 있는 리소스를 닫는 등의 뒷정리를 합니다.
- 각 테스트가 이전 테스트에 영향을 받지 않고 독립적으로 실행되도록 보장합니다.
이제 기본적인 테스트 구조를 알았으니 두 가지 전략을 살펴봅시다.
Unit Test 전략 1: Partition Test
파티션 테스트에서는 입력 데이터와 출력 결과를 공통된 특성을 가진 동등 분할(Equivalence Partition) 그룹으로 나눕니다. 각 그룹에서 대표값을 뽑아 테스트하면 전체를 테스트한 효과를 냅니다. 파티션 테스트 전략을 사용하면 모든 입력가능한 값을 다 실행해보지 않고 대푯값만 테스트함으로써 유닛 테스트의 효율성을 극대화할 수 있습니다. 아래는 유명한 GCD(최소 공배수 구하기) 코드입니다.
import pytest
def gcd(a, b):
# a가 b보다 작으면 두 값을 교체하여 a >= b가 되도록 함
if a < b:
a, b = b, a
if b == 0:
return a
else:
return gcd(b, a % b)
@pytest.mark.parametrize("a, b, expected", [
(4, 2, 2),
(9, 3, 3),
(4, 8, 4),
(12, 18, 6)
])
def test_gcd(a, b, expected):
assert gcd(a, b) == expected
위의 테스트처럼 input 모두 b가 GCD에 해당하는 경우[(4, 2), (9, 3)]뿐만 아니라 output의 partition도 고려해줄 필요가 있습니다. 따라서 a가 GCD이거나 a, b가 아닌 다른 수가 gcd인 경우에 대한 테스트를[(4, 8, 4)와 (12, 18, 6)] 균등하게 진행하여 output의 결과도 고려해 주어야 합니다.
Unit Test 전략 2: 가이드라인 테스트
가이드라인 기반 테스팅(Guideline-based Testing)은 이름 그대로 "이런 상황에서 에러가 자주 나더라"라는 경험적인 지침(Guideline)을 바탕으로 테스트 케이스를 만드는 전략입니다.
파티션 테스팅이 수학적이고 논리적인 분류라면, 가이드라인 테스팅은 개발자의 '경험'과 '직관'에 조금 더 의존하는 실전 팁 모음집이라고 보시면 됩니다.
- 정의: 프로그래머들이 흔히 저지르는 실수 패턴을 분석하여, 그 실수가 발생할 법한 상황을 집중적으로 테스트하는 방법입니다.
- 특징: "여기쯤 버그가 숨어있겠지?"라고 의심 가는 곳을 콕 집어서 공격하는 방식입니다.
이 책에서 제시하는 대표적인 가이드라인은 다음과 같습니다.
- 버퍼 오버플로우: 입력 허용량보다 훨씬 긴 데이터를 넣어본다.
- 극단적인 계산 결과: 계산 결과가 너무 크거나 작아서 변수에 다 담기지 못하게 유도한다.
- 에러 메시지 확인: 시스템이 낼 수 있는 모든 종류의 에러 메시지를 한 번씩 다 뜨게 만들어본다.
- 반복 입력: 똑같은 입력을 연달아 계속 넣어본다.
가이드라인 테스팅을 적용해 회원가입 기능을 테스트한다고 가정해 봅시다.
- 가이드라인 ① (버퍼 테스트):
- 상황: 아이디 입력창은 보통 20자 제한임.
- 테스트: 여기에 1,000자의 글자를 복사+붙여넣기 해본다. (서버가 뻗거나 앱이 종료되는지 확인)
- 가이드라인 ② (반복 테스트):
- 상황: '가입 완료' 버튼이 있음.
- 테스트: 버튼을 1초에 10번 다다다닥! 클릭해본다. (중복 가입이 되거나 DB 에러가 나는지 확인)
- 가이드라인 ③ (계산 테스트):
- 상황: 나이 입력칸.
- 테스트: 999999999살을 넣어본다. (숫자형 변수 범위를 넘어서는지 확인)
즉, 파티션 테스팅이 "정석적인 검사"라면, 가이드라인 테스팅은 "일부러 고장 내보는 스트레스 테스트"에 가깝습니다.
2) 컴포넌트 테스팅 (Component Testing)
- 정의: 여러 유닛을 합쳐서 만든 컴포넌트(인터페이스)가 잘 작동하는지 테스트합니다.
- 특징: 컴포넌트 내부 구현보다는 인터페이스(입출력)가 명세대로 동작하는지에 집중합니다.
3) 시스템 테스팅 (System Testing)
- 정의: 모든 컴포넌트를 통합한 전체 시스템을 테스트합니다.
- 특징: 컴포넌트 간의 상호작용 오류를 찾고, 성능이나 보안 같은 비기능적 요구사항도 이때 확인합니다.
4. 테스트 주도 개발 (TDD: Test-Driven Development)
"코드를 짜고 테스트하는 게 아니라, 테스트를 먼저 짜고 코드를 채운다"는 혁신적인 방법론입니다. TDD 또한 내용이 너무 방대하고 많아 자세히 접해보고 싶다면 최범균님의 아래 영상을 참고하시는 것을 권장드립니다.
https://www.youtube.com/watch?v=WBVjBwKx47I&list=PLwouWTPuIjUj_QqgXlFsqjUwyC0-5dZ_q
1) TDD 프로세스
- 기능 식별: 구현할 작은 기능 하나를 정합니다.
- 테스트 작성: 그 기능을 검증하는 테스트 코드를 먼저 짭니다. (아직 기능이 없으니 실행하면 당연히 실패/Fail 🔴)
- 코드 구현: 테스트를 통과할 수 있는 최소한의 코드를 작성합니다.
- 테스트 실행: 테스트가 성공/Pass하는지 확인합니다. (🟢)
- 리팩토링: 코드를 깔끔하게 다듬고 다시 테스트합니다.
2) TDD의 장점
- 코드 커버리지: 모든 코드에 대해 테스트가 자연스럽게 만들어집니다.
- 디버깅 시간 단축: 에러가 나면 방금 짠 그 코드만 보면 되니까 원인 찾기가 쉽습니다.
- 문서화 효과: 테스트 코드 자체가 "이 기능은 이렇게 쓰는 거야"라고 알려주는 예제 역할을 합니다.
5. 릴리즈 테스팅 (Release Testing)
개발팀이 아니라 별도의 테스트 팀이 수행하는 단계입니다. 고객에게 배포하기 전에 "진짜 써도 되는지" 검증합니다.
1) 요구사항 기반 테스팅
- 요구사항 명세서를 펴놓고 하나씩 체크합니다.
- 예시: "사용자가 로그인하면 메인 페이지로 이동해야 한다" -> 로그인 시도 -> 메인 페이지 뜨는지 확인.
2) 시나리오 테스팅
- 실제 사용자가 쓰는 것처럼 시나리오를 짜서 테스트합니다.
- 예시(사진 앱): 사진 찍기 -> 필터 적용 -> 저장 -> SNS 공유 -> 잘 올라갔는지 확인.
6. 사용자 테스팅 (User Testing)
아무리 우리가 테스트를 잘해도, 실제 사용자의 환경이나 데이터는 예측할 수 없습니다.
1) 알파 테스팅 (Alpha Testing)
- 개발사 내부의 다른 직원이나 소수의 사용자가 개발 환경에서 테스트합니다.
2) 베타 테스팅 (Beta Testing)
- 소프트웨어의 초기 버전(Beta)을 일반 사용자에게 공개하여 써보게 합니다.
- 사용자들의 실제 피드백과 버그 리포트를 받아 수정합니다.
- 예시: 게임 출시 전 '오픈 베타 테스트(OBT)'.
3) 인수 테스팅 (Acceptance Testing)
- 고객(발주사)이 "이 정도면 돈 주고 살만하다(인수하겠다)"라고 결정하기 위해 수행하는 최종 테스트입니다.
- 이 테스트를 통과해야 정식으로 납품이 완료됩니다.
7. 정리
테스팅은 버그를 찾는 Defect Testing과 요구사항 만족을 확인하는 Validation Testing으로 나뉩니다. 개발자는 유닛-컴포넌트-시스템 테스팅을 거치며, 최근에는 TDD를 통해 코드 품질을 높이는 추세입니다. 마지막으로 릴리즈 및 사용자 테스팅을 통해 최종 검증을 마쳐야 비로소 좋은 소프트웨어가 탄생합니다!
'Software 개발 > SW Engineering' 카테고리의 다른 글
| [SW Engineering] 10. 의존성 시스템(Dependable System) (0) | 2025.12.17 |
|---|---|
| [SW Engineering] 09. 유지보수와 레거시 전략(Software Evolution) (0) | 2025.12.17 |
| [SW Engineering] 07. 디자인과 구현(Design and Implement) (1) | 2025.12.17 |
| [SW Engineering] 06. 아키텍쳐 설계(Architectural Design) (0) | 2025.12.17 |
| [SW Engineering] 05. 요구사항 공학(Requirement Engineering) (1) | 2025.12.16 |
경이로운 BE 개발자가 되기 위한 프로그래밍 공부 기록장
도움이 되었다면 "❤️" 또는 "👍🏻" 해주세요! 문의는 아래 이메일로 보내주세요.