테스트
2장의 주제는 테스트 입니다. 현대 소프트웨어 개발에서 테스트의 중요성은 점점 강조되고 있습니다. 테스트가 제공하는 핵심적인 가치인 개발의 안정성 뿐만이 아니라, 테스트 가능한 코드를 작성하다 보면 따라오는 유연하고 확장성 있는 설계, 그리고 학습 도구로서의 기능까지 테스트는 우리에게 많은 가치를 제공합니다. 이 책에서는 지금까지 테스트에 사용했던 UserDaoTest 의 유용성, 그리고 한계와 문제점을 개선하며, 테스트 프레임워크 JUnit 을 스프링에서 효과적으로 사용하는 방법을 학습합니다. 또 테스트에 대해 다음과 같이 강조하며 2장을 시작합니다.
스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 묻는다면 나는 주저하지 않고 객체지향과 테스트라고 대답할 것이다. 어플리케이션은 계속 변화하고 복잡해져 간다. 그 변화에 대응하는 첫 번째 전략이 확장과 변화를 고려한 객체지향적인 설계와 IoC/DI 같은 기술이라면, 두 번째 전략은 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이다. 스프링으로 개발을 하면서 테스트를 만들지 않는다면 이는 스프링이 지닌 가치의 절반을 포기하는 셈이다.
2. 테스트
2 - 1. UserDaoTest 다시보기
테스트의 유용성
본격적으로 UserDaoTest 의 코드를 살펴보기 전에, 지금까지 UserDao 를 개선하면서 누렸던 UserDaoTest 의 유용성을 되짚어 보겠습니다. UserDao 의 문제점을 개선하면서, 우리는 수많은 코드를 수정했습니다. 메서드 추출, 클래스 분리, 팩토리의 활용 등 여러 변경을 거쳐 왔습니다. 이렇게 코드 구조를 변경해 가는 단계 단계마다, 원래의 기능이 정상적으로 동작하는지 UserDaoTest 를 통해 확인할 수 있었습니다. 만약 테스트 과정 없이 한 번에 변경을 쭉쭉 해 나가고 나서, 마지막에 와서야 코드가 제대로 동작하는지 확인한다면 어떨까요? 아마 개선 과정 내내 코드가 문제없이 잘 동작할까 하는 걱정에 불안할 것입니다. 그리고 그렇게 마지막에 테스트를 했을 때 쏟아지는 에러를 마주한다면, 그 에러가 어디서 발생했는지부터 어떤 코드부터 문제가 시작됐는지 등 문제 지점을 찾기란 쉬운 일이 아닐 것입니다. 그러나 우리는 UserDaoTest 를 통해 변경의 단계 단계마다 기능이 잘 동작함을 확인할 수 있었고, 지금까지의 변경이 기능에 문제를 일으키지 않았다는 확신과 안정감을 가지고 개발해 나갈 수 있었습니다.
이처럼 테스트는 소프트웨어 개발에 필수적입니다. 필수적이기 때문에, 사실 우리는 테스트라는 개념을 알지 못하던 때에도 테스트를 해 왔습니다. api 를 만들고 나서 swagger 나 postman으로 직접 테스트를 해 봤을 것이고, 작성한 코드의 중간 결과를 콘솔에 찍어보고서 코드에 대해 확신을 얻은 적도 있을 것입니다. 이처럼 우리는 어떤 형태로든 개발과 함께 테스트를 해 왔습니다.
수동 테스트와 UserDaoTest
UserDao 는 데이터베이스에 직접 액세스하는 계층입니다. 일반적으로, 테스트 코드 없이 UserDao 와 같은 데이터베이스 접근 계층을 테스트하려면 다음과 같은 절차가 필요합니다.
- 서비스 계층, 프레젠테이션 계층까지 포함한 모든 입출력 기능을 구현한다
- 만들어진 테스트용 웹 어플리케이션을 서버에 배치한다
- 웹 화면을 띄워 폼을 열고 값을 입력하여 등록한다
- 이를 위해서는 폼에서 값을 받아 파싱하여 User 오브젝트로 만든 뒤 UserDao 를 호출하는 기능이 구현되어 있어야 한다
UserDao 를 테스트하려던 것 뿐인데 머리아픈 일들이 너무 많이 생긴 것 같습니다. 가장 큰 문제는 테스트 대상인 UserDao 뿐만 아니라 서비스, 컨트롤러, 화면단까지 모든 계층에 대한 구현이 필요하다는 것입니다. 또 에러가 발생했을 때, 에러가 발생한 지점을 바로 찾아내기 어려운 경우가 더 많습니다. 근본적인 문제는 UserDao 에 대한 테스트인데 UserDao 가 아닌 다른 파트에서 발생한 문제가 테스트 결과에 영향을 끼친다는 것입니다.
이제 UserDaoTest 를 보겠습니다.
public class UserDaoTest {
public static void main(String[] args) throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
UserDaoTest 로 하는 테스트는 직접 수동으로 진행하는 테스트에 비해 훨씬 간결합니다.
UserDaoTest 의 코드에서 가장 돋보이는 점은 다음과 같습니다.
- 테스트의 대상이 UserDao 에 한정되어 있습니다.
UserDao 를 작성한 후 바로 수행하여 검증할 수 있고, 다른 파트의 구현이 필요하지 않습니다. 또 에러가 발생할 경우 그 원인을 UserDao 에서 찾을 수 있습니다. 즉 UserDao 의 테스트 결과에 외부 개입이 발생하지 않습니다. - main() 메서드를 통해 간단하게 실행 가능합니다.
테스트를 위해 직접 폼을 띄우고, 버튼을 누르고 테스트 값을 입력하고 하는 번거로움이 없이 main() 메서드의 실행만으로 간단하게 실행됩니다.
작은 단위 테스트
위에서 언급한 직접 진행하는 수동 테스트와 달리, UserDaoTest 의 가장 큰 특징은 그 범위가 UserDao 와 DB 계층에 한정되어 있다는 것입니다. 즉 UserDao 라는 작은 단위의 코드만을 테스트 대상으로 합니다. 이렇게 작은 단위로 나눠서 테스트 하는 것이 왜 필요할까요?
관심사의 분리는 어플리케이션 코드 뿐만이 아니라 테스트 코드에도 적용됩니다. 작은 단위의 테스트는 말하자면 관심사의 분리입니다. 테스트 대상인 관심만 테스트하고, 그 외의 계층이나 컴포넌트는 테스트에 참여시키지 않는 것입니다. 그렇게 해서 얻는 이점은 어떤 것들이 있는지 알아보겠습니다.
1. 테스트 대상에 포함되지 않는 계층이나 코드가 테스트 결과에 영향을 주지 않는다.
테스트 대상만을 독립적으로 검증하기 때문에, 외부 계층이나 코드가 테스트 결과에 영향을 주지 않습니다. 따라서 테스트가 실패한 경우, 정확히 테스트 대상 내에서 문제가 발생한 것을 알 수 있습니다.
그러나 이는 반대로 단위 테스트는 테스트 결과가 외부 요소에 영향을 받지 않도록, 독립적으로 실행되도록 작성되어야 한다는 제약이기도 합니다. 이 제약을 따르며 테스트 가능한 코드를 만들다 보면, 자연스럽게 변경과 확장에 유연한 코드를 만들게 됩니다.
2. 테스트가 실패한 경우 실패 지점을 빠르게 찾을 수 있다.
잘 만들어진 단위 테스트는 테스트 대상만을 검증합니다. 따라서 단위 테스트가 실패할 경우, 코드의 어느 부분에 문제가 있는지 빠르게 찾을 수 있습니다.
3. 작성한 코드에 대해서만 빠르게 테스트해볼 수 있다.
작성한 코드에 대한 단위 테스트만 작성되어 있다면, 테스트를 위해 다른 계층이나 외부 컴포넌트를 구현해야 할 필요가 없습니다. 검증을 위해서 단위 테스트만 실행하면 됩니다.
이처럼 작은 단위의 테스트를 작성했을 때 얻는 이점은 다양합니다. UserDaoTest 와 같이 작은 단위의 코드만을 독립적으로 테스트하는 코드를 단위 테스트 라고 합니다. 단위 테스트는 위에서 언급한 것처럼 여러 장점을 가지고 있지만, 반드시 잘 작성되어야 하며 단위 테스트로 테스트하기 좋은 코드는 자연스럽게 좋은 품질을 가지게 됩니다.
UserDaoTest 를 잘 보면 DB 의 상태를 직접 바꾸고 있습니다. 운영 환경에서는 테스트를 위해 실제 사용되는 DB 에 변경을 가할 수는 없으므로, 테스트용 DB 로 대체하는 등의 대안을 사용합니다. 간혹 DB 가 직접 사용되는 테스트는 단위 테스트가 아니라고 말하는 개발자도 있습니다. 그러나 UserDaoTest 의 경우 실행 전 매번 DB를 비워주는 방식으로 DB 상태를 관리하면서 사용하였고, 이처럼 DB 의 상태를 테스트가 관장하고 있다면 DB 의 상태까지 묶어서 테스트하는 단위 테스트로 볼 수 있습니다. 이처럼 단위는 정해진 범위가 있는 것은 아닙니다. 다만 단위는 일반적으로 작을수록 좋으며, 하나의 관심에 집중해서 테스트할 수 있는 효율적인 범위로 잡는 것이 좋습니다.
자동 수행 테스트
UserDaoTest 의 또다른 특징은 main() 메서드의 실행으로 간단하게 수행할 수 있다는 점입니다. 테스트를 위해 직접 데이터를 준비하고, 입력할 필요가 없습니다. 개발을 하면서 테스트 할 일이 정말 많은데, 그 때마다 폼을 열고 값을 준비해서 입력하고 하는 과정을 몇 번 거치다 보면 테스트가 귀찮아질지도 모릅니다. 대충 맞겠거니 하면서 일부 테스트를 아예 건너뛰는 경우가 생길 수도 있습니다. 또 데이터를 잘못 입력하는 실수가 생기는 일도 잦을 것입니다.
따라서 테스트는 간단하게, 자동으로 수행되어야 합니다. UserDaoTest 는 main() 메서드의 실행만으로 데이터 준비 없이 자동으로 테스트 할 수 있었기 때문에 코드의 수많은 개선 단계에서 반복적으로 수행될 수 있었습니다.
UserDaoTest 의 문제점
UserDaoTest 는 적절한 단위로, 자동으로 수행되도록 만들어진 테스트 코드입니다. 그러나 다음과 같이 몇 가지 문제를 가지고 있습니다.
수동 검증
UserDaoTest 는 데이터를 준비해 주고 해당데이터로 UserDao 의 기능을 호출했을 때 결과값을 제시해 줍니다. 그렇지만 결과값이 내가 원하는 값과 일치하는지 검증하는 것은 아직 개발자의 몫으로 남겨져 있습니다. 테스트 대상이 많아지고 복잡해진다면 검증에 대한 부담은 더욱 커질 수밖에 없습니다. 데이터 준비와 호출까지는 자동으로 수행해 준다고 해도, 결과값이 일치하는지에 대한 판단을 수동으로 해야 한다면 완전히 자동화 된 테스트라고 볼 수 없습니다.
수동 실행
직접 데이터를 준비하고 입력하는 수동 테스트와 다르게, UserDaoTest 는 main() 함수의 실행으로 간편하게 테스트를 수행할 수 있습니다. 그러나 역시 테스트 해야 할 기능이 많아진다면, 이렇게 일일이 테스트 대상의 main() 을 찾아 실행해 주는 것은 큰 부담이 됩니다. 그렇기 때문에 테스트 메서드를 직접 실행시켜 테스트하는것보다 체계적이고 편리하게 테스트를 할 수 있는 방법이 필요합니다.
2 - 2. UserDaoTest 개선과 JUnit
현재의 UserDaoTest 는 먼저 조회된 데이터가 입력한 데이터와 일치하는지 개발자의 눈으로 일일이 검증을 거쳐야 한다는 문제를 가지고 있습니다. 테스트를 실행하면 테스트가 실패했는지, 성공했는지에 대한 결과만을 나타낼 수 있다면 좋을 것 같습니다.
또, UserDaoTest 클래스의 main() 메서드를 실행해서만 테스트가 실행된다는 문제가 있습니다. 테스트의 수가 많아져도 간단하게 실행할 수 있고, 결과를 종합해서 확인할 수 있으며 실패 지점을 빠르게 찾을 수 있는 테스트 지원 도구가 필요할 것 같습니다.
JUnit 은 자바를 사용하는 개발자라면 한번 쯤 들어봤을 법한 테스트 지원 도구입니다. JUnit 은 자바의 표준 테스트 프레임워크의 위치에 있을 정도로 자바 개발자들 사이에선 널리 사용되어오고 있으며, 그 이름처럼 단위(Unit) 테스트 작성에 강력한 도움을 주는 도구입니다. 그렇다면 JUnit 을 사용해서, UserDaoTest 의 문제들을 개선해 보겠습니다.
JUnit 테스트로 전환
JUnit은 프레임워크입니다. 1장에서 살펴보았듯, 프레임워크의 기본 원리는 제어의 역전입니다. UserDaoTest 는 동작의 제어권이 프로그래머에게 있는 main() 메서드로서 테스트를 실행하므로, 프레임워크에 사용하기에는 적절하지 않습니다. 따라서 main() 메서드로 실행하는 테스트를 일반 public 메서드로 옮겨주고, 메서드 위에 @Test 어노테이션을 붙여 프레임워크에게 테스트 메서드라는 것을 알려줍니다. 실행을 외부에서 제어하기 때문에 반드시 제어자를 public 으로 합니다.
여기서 @Test 어노테이션은 해당 메서드가 테스트 메서드임을 나타냅니다. JUnit의 테스트 실행기는 테스트 클래스에서 @Test 가 붙은 메서드들을 스캔하여 실행합니다.
위 조건에 맞도록 UserDaoTest 의 테스트 메서드의 시그니처를 변경하면 아래와 같습니다.
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
// ...
}
}
제어자를 public 으로 명시하였고, @Test 어노테이션을 통해 테스트 메서드임을 표시했습니다. 또 일반 메서드로 변경되었으므로 어떤 논리를 테스트하는지 나타내는 적절한 이름을 붙였습니다.
또한 테스트 메서드는 반환값이 없어야 합니다. 기본적으로 파라미터도 허용하지 않지만, JUnit5 에서는 부분적으로 파라미터를 사용 가능합니다.
JUnit 프레임워크에 걸맞은 모양으로 메서드 시그니처를 변경하였으니, 이제 검증 방식도 JUnit 이 제공하는 방식으로 우아하게 바꿔 봅시다.
검증 코드 전환
JUnit 은 다양한 검증 방법을 제공합니다. 검증 메서드는 org.junit.Assert 클래스에 static 으로 정의되어 있습니다.
아래 코드에서는 저장한 user 와 조회한 user2 의 이름과 패스워드가 일치하는지 확인하기 위해서 Hamcrest.MatcherAssert 의 검증 메서드를 사용하였습니다. 책에서 사용한 junit.Assert 의 assertThat() 메서드는 현재는 deprecate 된 관계로 다른 검증 방법을 사용하였습니다.
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("whiteship");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
User user2 = dao.get(user.getId());
MatcherAssert.assertThat(user2.getName(), is(user.getName()));
MatcherAssert.assertThat(user2.getPassword(), is(user.getPassword()));
}
}
이렇게 수정한 UserDaoTest 를 실행시키면 아래와 같은 결과화면을 보여줍니다.
JUnit 을 사용하여 진행한 테스트 결과입니다. 이러한 검증 방식은 프로그래머의 추가적인 검증이 필요하지 않기 때문에 테스트 비용이 절감되며, 실수로 인한 오류가 발생하는 것을막아줍니다.
또한 한 번의 실행으로 동시에 여러 테스트를 실행시킬 수 있으며, 테스트가 성공했을 경우 BUILD SUCCESSFUL 메세지와 함께 몇 개의 테스트가 성공했는지 알기 쉽게 결과를 출력해 줍니다.
테스트가 실패한 경우 어떤 결과가 나올까요?
다음은 User 의 name 을 다른 값으로 바꾼 후 실행한 테스트의 결과입니다.
테스트가 실패한 경우 BUILD FAILED 메세지와 함께 에러 로그가 출력되어 어떤 이유로 테스트에 실패했는지 확인할 수 있습니다. 또 어떤 클래스에서 어떤 테스트가 실패했는지 알기 쉽게 나타나므로, 여러 테스트를 실행했더라도 문제 지점을 어렵지 않게 찾을 수 있습니다.
이처럼 JUnit 을 사용하면 여러 테스트를 간단하게 실행하고 정확하게 검증하며, 그 결과를 한 눈에 알 수 있습니다. 지금처럼 하나의 클래스와 하나의 테스트만 있는 경우 그 장점이 크게 느껴지지 않지만, 어플리케이션이 커지고 수십, 수백개의 테스트가 생기게 된다면 자동화된 실행과 검증은 테스트의 필수 요소가 될 것입니다. JUnit 은 그것을 가능하게 해 주는, Java 테스트의 실직적 표준 위치에 있는 프레임워크입니다.
이번 글에서는 테스트의 필요성과 적절한 범위와 빈도의 단위테스트를 알아보았습니다. 테스트는 본질적으로 지금까지 개발한 코드에 안정감과 확신을 주며, 그 위에서 지속적으로 개발할 수 있도록 적절한 범위를 대상으로 하는 잦은 빈도의 단위테스트가 필요합니다.
또한 테스트가 정말 테스트로서의 기능을 하기 위해서는 실행과 검증이 사용자의 개입 없이 자동으로 이루어져야 합니다. 그렇기 때문에 지금까지 사용했던, main() 메서드를 사용하고 직접 검증이 필요한 UserDaoTest의 단점을 개선하기 위해 Java 테스트 프레임워크인 JUnit 을 사용해 보고 그 이점을 확인했습니다.
다음 글에서는JUnit 이 제공하는 편의성을 조금 더 알아보고, 책이 쓰여진 때와 조금은 달라진 JUnit5(Jupiter)를 사용하는 방법도 학습 해 보겠습니다. 또 스프링에서 테스트를 적용할 때 주의할 점도 몇 가지 알아보겠습니다. 긴 글 읽어주셔서 감사합니다.
'Study' 카테고리의 다른 글
초간단 Jmeter 사용법 (0) | 2025.06.06 |
---|---|
토비의 스프링 - 2. 테스트 (2) (0) | 2025.06.02 |
잘못된 yml 설정 값으로 인한 Could not resolve placeholder "" Exception 해결 방법 (0) | 2025.05.16 |
토비의 스프링 - 1. 오브젝트와 의존관계(2) (0) | 2025.05.16 |
Oauth2 를 사용한 소셜로그인 구현 과정 (0) | 2025.05.13 |