테스트란?
작성한 코드가 의도대로 작동하는지 확인하고, 예상치 못한 문제를 방지하기 위해 코드의 기능 및 단위를 검증하는 과정이다.
테스트는 왜 필요한걸까? 코드를 작성하고 직접 실행해서 확인하면 되지 않을까? 할 수 있지만 해당 방식은 한계가 있다.
💡 기능이 늘어날수록 모든 경우를 손으로 테스트하는 것은 불가능에 가까워지고, 어느 한 부분을 수정했을 때 다른 부분이 버그가 생길 수 있다. 이러한 문제를 해결하기 위해 테스트 코드를 작성이 필요하다.

단위 테스트(Unit Test)
단위 테스트는 하나의 클래스 또는 메서드 단위를 외부 의존성 없이 독립적으로 검증하는 테스트이다. 외부 의존성이란 DB, 네트워크, 파일 시스템 등을 뜻하며, 이들은 Mock이나 Stub으로 대체해서 테스트 로직에만 집중한다.
@Test
void calculateDiscount() {
✔️ 외부 의존성 x
DiscountPolicy policy = new RateDiscountPolicy();
int discount = policy.discount(member, 10000);
assertThat(discount).isEqualTo(1000);
}
💡실행 속도가 매우 빠르며, 어느 메서드의 어느 로직이 잘못됐는지 바로 파악이 가능하기에, 실패 원인이 명확하다. 하지만 모듈 간 연동, DB 연결 및 트랜잭션 같은 통합 문제는 발견하지 못한다.
통합 테스트(Integeration Test)
통합 테스트는 두 개 이상의 컴포넌트가 함께 동작할 때 올바르게 연동되는지 검증하는 테스트이다. DB, 외부 API, Spring 컨테이너 등 실제 인프라를 함께 사용해서 단위 테스트로는 잡을 수 없는 연동 문제를 발견한다.
@SpringBootTest
@Transactional
class ItemRepositoryTest {
@Autowired ItemRepository itemRepository;
@Test
void saveAndFind() {
✔️ 실제 DB에 저장 후 조회
itemRepository.save(new Item("itemA", 1000, 5));
Item found = itemRepository.findById(...);
assertThat(found.getItemName()).isEqualTo("itemA");
}
}
💡 Service -> repository -> DB까지의 흐름이 실제로 올바르게 동작하는지, 즉 여러 계층이 조합됐을 때의 동작을 실제 환경에 가깝게 검증한다.
실제 환경에 가깝게 검증하기에 신뢰도가 높으며, 실제 연동 문제를 발견할 수 있지만 실행 속도가 느리고 실패 원인이 여러 계층에 걸쳐 있어 디버깅이 어려울 수 있다.
데이터베이스 연동 테스트가 필요한 이유
Repository 계층은 단순한 자바 로직이 아니라 실제 데이터베이스와 통신하기 때문에, 인터페이스 단위 테스트만으로는 SQL 오류나 트랜잭션 문제를 잡아낼 수 없다. 이런 경우에는 실제 DB에 연결해서 구현체를 직접 검증하는 통합 테스트가 필요하다.
테스트 환경 분리: 로컬 DB vs 테스트 DB
운영·개발용 DB와 테스트용 DB는 반드시 분리해야 한다. 테스트 중에 생성된 더미 데이터가 개발 DB를 오염시키면, 다른 개발자의 작업에 영향을 줄 수 있다.
| 환경 | 데이터베이스 | 용도 | 데이터 유지 |
| 로컬 개발 | H2 TCP / MySQL | 기능 개발 | 유지 |
| 단위·통합 테스트 | H2 인메모리(임베디드) | 자동화 테스트 | 테스트 종료 시 소멸 |
| CI/CD 파이프라인 | H2 인메모리 또는 컨테이너 DB | 빌드 검증 | 파이프라인 종료 시 소멸 |
Spring에서는 src/test/resources/application.properties를 별도로 두어 테스트 전용 설정을 관리하는 것이 일반적이다.
임베디드 DB : 로컬 DB와 테스트 DB 분리(Spring Boot)
임베디드 DB란 별도의 DB 서버를 설치하고 실행하지 않아도 되는 DB이다. 애플리케이션 안에 DB 자체를 내장해서 JVM 메모리 위에서 함께 실행된다. 애플리케이션이 실행될 때 같이 켜지고, 꺼질 때 같이 꺼진다. 데이터도 메모리에만 존재하기 때문에 종료되면 모두 사라진다.
Spring Boot는 src/main과 src/test의 application.properties를 각각 따로 읽는다. 이 구조를 활용하면 로컬 DB와 테스트 DB를 간단하게 분리할 수 있다.
src/main/resources/application.properties → 로컬 개발용 (실제 DB)
src/test/resources/application.properties → 테스트용 (H2 임베디드)
💡 로컬 개발용 application.properties 파일에 DB 연결 설정을 해놓고, test 파일에는 어떠한 설정을 하지 않아도 된다.
✔️ (main) application.properties 로컬 DB 연결 설정
spring.profiles.active=local
spring.datasource.url= 필요 주소
spring.datasource.username=아이디
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=사용 드라이버 명칭
✔️ (test) application.properteis 설정 불필요
spring.profiles.active=test
💡 테스트를 실행하면 Spring Boot는 src/test/resources의 설정을 우선적으로 읽는다. 이때 spring.datasource.url이 없으면 별도의 DB 서버 없이 JVM 메모리 안에서 H2를 직접 실행한다. 애플리케이션이 종료되면 H2도 함께 종료되고 데이터도 모두 사라진다.
H2란?
H2는 자바로 만들어진 경량 관계형 DB이다. 일반적인 MySQL, PostgreSQL처럼 서버 모드로도 사용할 수 있고, 임베디드 모드로도 사용할 수 있다. 용량이 작고 설정이 거의 없어 테스트 환경에서 표준처럼 쓰인다. Spring Boot도 H2 의존성만 있으면 별도 설정 없이 자동으로 임베디드 모드로 연결해준다.
테스트의 원칙

💡 테스트에서 가장 중요한 원칙 중 하나는 각 테스트 메서드는 서로의 존재를 몰라야 한다는 것이다. 특정 테스트가 DB에 데이터를 삽입한 채로 종료되면, 그 이후에 실행되는 테스트가 예상치 못한 데이터를 마주쳐 실패할 수 있다. 이를 테스트 오염(Test Pollution)이라고 한다.
@Transactional - 트랜잭션 롤백을 활용한 데이터 롤백
테스트가 끝난 뒤 삽입한 데이터를 깔끔하게 지우는 가장 좋은 방법은 트랜잭션 롤백이다. 테스트를 트랜잭션 안에서 실행하고, 끝나고 무조건 롤백해버리면 DB는 테스트 이전 상태로 되돌아간다.
Spring은 테스트 클래스 또는 메서드에 @Transactional을 붙이면 자동으로 위의 과정을 처리해준다. 일반 서비스 코드에서 @Transactional은 로직 성공 시 커밋하지만, 테스트에서는 특별하게 동작하여 항상 롤백된다.
내부 동작
1. Transaction Intercept - 트랜잭션 선점
테스트 메서드가 실행되기 전, Spring의 AOP 프록시가 메서드를 가로채서 트랜잭션을 먼저 열어둔다. 이후 테스트 코드가 실행되는 동안 모든 DB 작업은 해당 트랜잭션 안에서 수행된다.
2. Transaction Propagation:REQUIRED - 계층간 트랜잭션 공유
@Transactional의 기본 전파 옵션은 REQUIRED다. 이미 트랜잭션이 존재하면 새로 만들지 않고 기존 것에 합류한다는 뜻이다. 이로 인해, 테스트 -> Service -> Repository -> JPA 까지 전부 같은 DB 커넥션을 공유하게 된다.
3. Forced Rollback - 강제 롤백
@ 일반 서비스 코드에서 @Transactional은 로직이 성공하면 커밋한다. 하지만 테스트에서는 다르다.
Spring 테스트 프레임워크의 TransactionalTestExecutionListner가 @transactional을 감지해서, 테스트가 끝나면 커밋 대신 강제로 롤백을 호출 한다. 이로 인해 개발자가 데이터를 직접 지우지 않아도 된다.
4. Read Your Own Writes - 커밋 전 SELECT가 되는 이유
INSERT 직후 커밋하지 않아도, 같은 테스트 안에서 SELECT는 정상적으로 이루어진다. 이는 같은 트랜잭션(같은 커넥션) 안에서는 커밋 여부와 무관하게 본인이 변경한 데이터를 읽을 수 있기 때문이다. 당연히 다른 트랜잭션에서는 보이지 않는다.
5. Automatic Rollback on failure - 예외 발생 시 자동 롤백
테스트 중간에 예외가 발생해 강제로 종료되더라도 상관 없다. 한 번도 커밋하지 않은 상태이기 때문에, DB 커넥션이 끊기는 순간 자동으로 롤백 된다.
✔️ 테스트 종료 시 자동 롤백
@Transactional
@SpringBootTest
class ItemRepositoryTest {
@Autowired
private ItemRepository itemRepository;
@Test
void findItems() {
✔️ INSERT — 같은 트랜잭션에서 실행됨
itemRepository.save(new Item("item1", 1000, 5));
itemRepository.save(new Item("item2", 2000, 3));
itemRepository.save(new Item("item3", 3000, 1));
✔️ SELECT — 아직 커밋 전이지만 같은 트랜잭션이므로 조회 가능
List<Item> result = itemRepository.findAll();
assertThat(result).hasSize(3);
}
✔️ 테스트 종료 -> 자동 롤백
}
정리
1. 트랜잭션 시작
테스트 메서드 또는 클래스에 @Transactional이 있으면 실행 전에 트랜잭션 먼저 시작.
2. 테스트 로직 실행(모든 코드가 트랜잭션 범위 내)
INSERT, SELECT 등 모든 DB 작업이 하나의 트랜잭션 안에서 수행. 트랜잭션 전파로 인해 Service, Repository 레이어도 같은 트랜잭션에 참여
3. 검증(assertThat 등)
insert한 데이터를 아직 커밋 전이지만 같은 트랜잭션 내의 SELECT로 조회하여 검증
4. 자동 롤백
테스트가 끝나면, Spring이 강제로 rollback() 호출(커밋한 적이 없으므로 DB는 변화x)
'Spring' 카테고리의 다른 글
| [Spring Boot] JPA, Spring Data JPA, QueryDSL의 이해 (0) | 2026.05.16 |
|---|---|
| [Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃) (0) | 2026.05.13 |
| [Spring] - AOP(관점 지향 프로그래밍)의 이해 (0) | 2026.04.28 |
| [Spring] - BCryptPasswordEncoder를 이용한 비밀번호 암호화와 보안 원리 (1) | 2026.04.16 |
| [Spring] 싱글톤 패턴과 싱글톤 컨테이너 정리 (0) | 2026.03.23 |