들어가며
Spring MVC 프로젝트를 Spring Boot로 마이그레이션하면서 가장 먼저 고민한 것은 "마이그레이션 후 기존 기능이 정상적으로 동작하는지 어떻게 검증할 것인가"였다.
기존 방식대로라면 마이그레이션 후 브라우저를 열고 하나하나 클릭하며 확인해야 했다. 로그인, 회원가입, 관리자 페이지, 마이페이지 등 모든 기능을 수동으로 검증하는 것은 시간이 오래 걸리고, 놓치는 부분이 생길 수밖에 없었다.
특히 이번 마이그레이션은 단순히 프로젝트를 Spring Boot에서 실행되게 만드는 작업이 아니었다. 이후 Spring Security 도입, JPA 전환, Redis 세션 적용처럼 내부 구조를 계속 바꿔야 하는 작업 이었다.
겉으로 보이는 화면과 기능은 그대로 유지되어야 하지만, 내부 구현은 계속 달라질 예정이었다. 그래서 본격적인 마이그레이션 전에 테스트 코드를 먼저 작성하기로 했다.
테스트 코드가 있으면 마이그레이션 후 테스트를 실행하는 것만으로 기존 기능의 정합성을 1차 검증할 수 있다. 나에게 테스트 코드는 새 기능을 검증하기 위한 도구라기보다, 기존 기능을 지키기 위한 안전장치에 가까웠다.
마이그레이션 실행 흐름
1. 테스트 코드 작성
2. Spring Security 도입
3. JPA 전환
4. Redis 세션 적용
5. GitHub Actions CI/CD 구성
테스트 코드 작성 방식 선택 - BDD(Behavior Driven Development)
처음에는 테스트코드를 JUnit5 기본 방식으로 작성했다. 그런데 테스트 코드를 작성하다 보니 준비 단계에서 when()을 쓰고, 실행 단계에서도 when을 사용하니 작성하면서도 헷갈리는 부분이 있었다.
그래서 given/when/then 패턴과 맞지 않는다는 것을 느꼈고, 다른 방식을 찾아보았다.
테스트 코드를 작성하면서 단순히 테스트 개수를 늘리는 것보다, 나중에 다시 봤을 때 의도를 이해할 수 있는 테스트를 작성하는 것이 더 중요하다고 생각했다.
✔️ Mockito 기본 방식
when(userDao.selectUser("testUser")).thenReturn(mockUser);
✔️ BDD 방식
given(userDao.selectUser("testUser")).willReturn(mockUser); ✔️ 준비
SessionUser result = userService.login("testUser", "password"); ✔️ 실행
assertThat(result).isNotNull(); ✔️ 검증
BDD 방식은 given/when/then 패턴과 자연스럽게 매핑되어, 테스트 코드의 의도가 명확하게 드러났다.
또한 then().should()로 메서드 호출 여부를 검증하는 것도 가독성이 좋아서 BDD 방식을 선택했다.
테스트 범위 정하기
프로젝트 전체 기능을 한 번에 테스트하기는 어려웠다. 이 프로젝트는 팀 프로젝트였고, 수강 신청, 강의, 게시판, 댓글 등 여러 기능이 있었다. 그중 내가 주로 담당한 영역은 회원과 관리자 기능이었다.
그래서 테스트 범위도 먼저 내 담당 영역으로 좁혀, 회원 영역에서는 다음 흐름을 중심으로 테스트했다.
처음부터 모든 테스트를 완벽하게 작성하려고 했지만 오히려 진도가 나가지 않았기에, "내가 구조를 바꿀 때 가장 자주 깨질 수 있는 부분"부터 테스트했다.
Controller 테스트
Controller 테스트는 사용자의 요청과 응답 흐름을 확인하기 위해 작성했으며, 사용한 방식은 @WebMvcTest와 MockMvc다. 처음에는 Security까지 같이 테스트하는 것이 더 좋은 것 아닌가 생각했다. 하지만 이 단계의 목적은 컨트롤러 요청/응답 흐름을 확인하는 것이었다.
Security는 별도의 테스트에서 검증하는 것이 더 명확하다고 판단했다. Controller 테스트에 Security까지 섞이면, 테스트가 실패했을 때 컨트롤러 문제인지 권한 설정 문제인지 구분하기 어렵기 때문이다.
Service 테스트
Service 테스트는 실제 비즈니스 로직을 확인하기 위해 작성했으며, 사용한 방식은 MockitoExtension이다.
Service 테스트에서는 DB, 메일, Supabase Storage 같은 외부 의존성을 mock으로 대체했다.
예를 들어 비밀번호 찾기 기능은 사용자 정보가 일치하면 임시 비밀번호를 발급하고 메일을 보내야 한다. 하지만 테스트에서 실제 메일을 보낼 필요는 없다. 중요한 것은 "메일 발송 로직이 호출되었는지"와 "예외 상황에서 올바르게 실패하는지"다.
이렇게 외부 의존성을 mock 처리하니 테스트가 빨라졌고, 서비스 로직의 조건 분기에 집중할 수 있었다.
테스트를 작성하면서 발견한 점
테스트를 작성하면서 단순히 "기능이 맞는지"만 확인한 것은 아니었다. 기존 코드의 책임이 어디에 있는지도 더 잘 보이기 시작했다. 가장 대표적인 부분이 로그인이다.
기존 프로젝트에서는 컨트롤러가 로그인 요청을 받고, 사용자 정보를 조회하고, 세션에 저장하는 흐름을 가지고 있었다. 그리고 인터셉터가 로그인 여부와 관리자 권한을 확인했다.
하지만 Spring Security를 도입하면 로그인 요청은 Security 필터가 처리한다. 컨트롤러가 직접 로그인 로직을 가질 필요가 줄어든다. 이 차이를 테스트를 보면서 더 명확하게 알 수 있었다.
기존 Controller 테스트에는 로그인 성공/실패 흐름이 들어있었지만, Security 전환 이후에는 이 테스트의 책임이 바뀌어야 한다. 로그인 컨트롤러 테스트가 아니라 Security 필터 체인, UserDetailsService, 로그인 성공/실패 핸들러 테스트로 분리하는 것이 더 맞다.
즉, 테스트 코드를 작성하면서 오히려 앞으로 리팩토링해야 할 방향이 보였다.
Spring Security 도입 전에 도움이 된 부분
테스트 코드 덕분에 Spring Security를 도입할 때 기준을 잡을 수 있었다. Security 적용 후에는 다음 흐름을 우선 확인했다.
1. 비로그인 사용자가 보호 URL에 접근하면 로그인 화면으로 이동하는가
2. 일반 사용자가 /admin/**에 접근하면 권한 없음 처리가 되는가
3. PENDING 계정으로 로그인하면 상태에 맞는 에러 메시지가 출력되는가
4. 로그인 성공 후 SessionUser가 세션에 저장되는가
5. 로그아웃 후 인증 정보와 세션이 정리되는
이 과정에서 기존 인터셉터가 담당하던 로그인 여부 확인과 관리자 권한 확인은 Spring Security로 옮기는 것이 맞다고 판단했다.
반대로 게시판 접근 권한이나 수강 신청 기간 체크처럼 다른 도메인의 비즈니스 규칙은 무리하게 제거하지 않았다. 이 부분은 내가 담당한 범위를 벗어나기도 하고, 단순 인증/인가와는 성격이 다르기 때문이다.
부족한 점
현재 작성한 테스트는 마이그레이션을 위한 첫 번째 안전망으로, 아직 부족한 부분도 많다. Security 필터를 포함한 인증/인가 테스트, CustomUserDetailsService 계정 상태별 로그인 실패 테스트 등..
특히 지금은 Controller 테스트에서 Security 필터를 제외하고 있기 때문에, 실제 권한 정책은 별도의 Security 테스트로 반드시 보강해야 한다.
마무리
이번 단계에서 테스트 코드는 단순히 정답을 확인하는 도구가 아니었다. 마이그레이션 과정에서 기존 기능을 지키기 위한 기준점이었고, 동시에 코드의 책임을 다시 생각하게 만드는 도구였다.
Spring Boot 마이그레이션은 앞으로 Security, JPA, Redis, CI/CD까지 계속 이어질 예정이다. 내부 구조는 계속 바뀌겠지만, 사용자가 경험하는 기능은 유지되어야 한다.
그래서 앞으로의 작업도 테스트를 기준으로 하나씩 확인하면서 진행하려고 한다.
한 줄 회고
테스트 코드를 먼저 작성해두니 마이그레이션이 단순한 코드 이동이 아니라, 기존 기능을 유지하면서 구조를 개선하는 작업이라는 점이 더 분명해졌다.