기존 테스트를 진행한 방식은 직접 HTTP 요청을 보내 확인하는 방식으로 정상 값과 틀린 값을 만들어내서 API 문서 만드는 방식으로 진행했었다.
이에 대해 불편한 점은 다음과 같다.
- 실행 후 의도되지 않은 결과 값이 나온 경우 수정하고 다시 Run을 진행 후 요청을 보내야한다.
- Application.yml의 설정된 DDL-AUTO값이 CREATE 혹은 CREATE-DROP가 아닌 경우 값이 들어가 매번 요청을 다르게 보내거나 DB에 저장된 값을 지워야한다.
- 실질적으로 내가 원하는 결과값이 맞는지 읽어봐야한다.
이러한 불편함을 해결하기 위해 Spring에서 지원하는 JUnit 테스트를 적용 해보았다.
JUnit
Java에서 가장 널리 사용되는 테스트 프레임 워크로, 어노테이션 기반으로 테스트 코드를 작성하고 실행 결과를 자동으로 검증해준다.
JUnit6는 JUnit Platform + JUnit Jupiter + JUnit Vintage로 구성되며, Java 17이상을 요구한다.
Spring Boot 4.X부터는 기본 포함되어 있다.
※ JUnit Vintage(JUnit 3,4 호환)는 Deprecated되어 JUnit Jupiter로 마이그레이션이 권장된다.
Test Code
테스트는 다음과 같이 나뉜다.

이중 Service 단위 테스트를 우선적으로 알아보자
Service 단위 테스트
단위 테스트를 진행하는 방법은 크게 두가지가 존재한다.
- 메모리 기반 Map Repository 객체 사용
- Mockito Mock 사용
단위 테스트가 존재하는 이유는 Spring과 DB I/O 없이 빠른 테스트를 진행할 수 있다는 점이며 실제 DB를 사용하지 않기 때문에 트랜잭션이나 제약조건등이 반영이 안된다는 단점이 존재한다.
메모리 기반 Map Repository 객체 사용
해당 방식은 사용할 DB가 정해지지 않은 상황이거나, @Mock 방식을 사용하지 않고 순수 자바 방식으로 검증을 하고 싶을 때 사용 가능하다.
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public void clearStore() {
store.clear();
}
}
Map등을 사용해서 Repository처럼 만들어서 사용하는 방식이다.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach //각 테스트 전에 실행
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach //각 테스트 후에 실행
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
}
하지만 해당 방식의 문제점은 다음과 같다.
- Member 필드 변경시 MemoryMemberRepository또한 수정이 들어가야한다.
- MemoryMemberRepository 자체를 별도로 관리 해야 하는 유지보수적인 부담이 존재한다.
- 해당 방식 또한 기존에 넣은 값들이 보존되기 때문에 각 테스트의 독립성을 유지하기 위해 초기화를 매번 진행 해야한다.
Mockito Mock 방식
Mockito Mock 방식은 테스트를 진행하고자하는 Service가 아닌 Repository나 진행하고자하는 Service에서 사용되는 다른 Service로직을 가짜(Mock)으로 사용해서 진행되는 로직이 어떤 값을 반환할지 미리 설정해서 사용이 가능하다.
Mockito는 spring-boot-starter-test에 포함 되어있어 스프링 부트를 사용하게 되면 프로젝트의 의존성이 주입이된다.
@ExtendWith(MockitoExtension.class)
class AuthServiceUnitTest {
@Mock
MemberService memberService;
@Mock
PasswordEncoder encoder;
@InjectMocks
AuthServiceImpl authService;
@Nested
@DisplayName("회원가입")
class Signup {
private final Address address = new Address("서울시","동작구","사당동");
@Test
@DisplayName("[HAPPY] 회원가입이 정상적으로 작동한다.")
public void signup_validRequest_success(){
//given
AuthSignupRequestDto dto = new AuthSignupRequestDto("test@test.com", "이름", "password", address);
Member savedMember = Member.builder()
.id(1L)
.email(dto.email())
.build();
given(encoder.encode(any())).willReturn("encodePassword");
//when
when(memberService.register(any())).thenReturn(savedMember);
String message = authService.signup(dto);
//then
assertThat(message).isEqualTo("회원가입 성공 ID : 1");
}
@Test
@DisplayName("[Exception] 중복된 이메일로 회원가입하면 예외처리가 정상작동한다.")
public void signup_DuplicateEmail_ExceptionThrown(){
//given
AuthSignupRequestDto dto = new AuthSignupRequestDto("test@test.com", "이름", "password", address);
given(memberService.isEmailDuplicated(dto.email())).willReturn(true);
//when & then
assertThatThrownBy(() -> authService.signup(dto))
.isInstanceOf(InvalidInputException.class)
.hasMessage("중복된 이메일입니다.");
}
}
}
@ExtendWith를 통해서 Mockito를 JUnit에서 사용하도록 해주며, 로직을 검증하기 위해 기존에 사용된 의존성을 가지는 객체들을 @Mock을 통해서 가짜 객체임을 명시하고, @InjectMocks를 통해 가짜 객체가 주입될 Service객체를 명시해준다.
Mock방식은 필요한 로직만을 구현하면 되어 간편하게 테스트 로직을 작성이 가능하며, Mock은 한번의 테스트가 진행되면 Mock 객체는 다시 사용되지 않기 때문에 초기화를 진행하지 않아도 된다.
해당 테스트에서 사용된 방식은 given과 when이 존재하는데 차이점은 라이브러리가 다르고 메서드명이 다르다는 것 뿐이다.
Mockito 기본 로직은 when(...).thenReturn(...) 방식이였으나
BDD스타일 (given, when, then 을 나눈 방식)과 맞지 않고, 미리 준비하는 단계인 given이 맞단 판단에 추후 추가되어
BDDMockito라이브러리를 통해 given(...).willReturn(...)방식이 추가되었다.
추가적으로 Mock을 사용하더라도 다음과 같은 단점이 존재한다.
- 반환값을 미리 설정해야 해서 실제 로직과 다르게 동작할 수 있다.
- given혹은 when을 누락할 경우 null을 반환하여 예상치 못한 결과가 나올 수 있다.
두 방식을 표로 비교 해보자.
| 메모리 Map 방식 | Mock 방식 | |
| 초기화 | @AfterEach 필요 (기존 테스트에 사용된 값 지우기) | 불필요 |
| 유지보수 | MemoryRepository 별도 관리 | 불필요 |
| 설정 난이도 | 낮음 | 낮음 |
| 실제 로직 반영 | Repository 로직 직접 구현 | 반환값 미리 설정 |
| 사용 시점 | DB 미정, 순수 자바 검증 | 일반적인 단위 테스트 |
일반적인 상황에서는 Mock 방식이 유지보수 부담이 적고 간편하여 더 많이 사용되며, DB가 정해지지 않은 초기 개발 단계나 순수 자바로 검증을 하고 싶은 경우에 메모리 Map방식을 선택하게 된다.
'BackEnd > 자바 스프링' 카테고리의 다른 글
| [JAVA] Test code : 2. Service 통합 테스트 (TestContainers 적용) (0) | 2026.05.05 |
|---|---|
| [JAVA] Test code : 0. Assertions (0) | 2026.05.05 |
| [JAVA Spring] 트랜잭션 심화 (0) | 2026.04.01 |
| [Java Spring] Transaction (0) | 2026.03.22 |
| [Java Spring] 웹 서버 구조와 IoC/DI 핵심 개념 (0) | 2026.03.18 |