https://to-the-goal.tistory.com/20
[Spring] 토비의 스프링 Vol.1 2장 - 테스트 (1)
2장을 시작하며 필자는 스프링의 가장 중요한 가치를 객체지향과 테스트라고 말하고 있다.앞서 1장에서 IoC/DI는 오브젝트의 설계, 생성, 관계, 사용에 관한 기술이며이를 쉽게 사용할 수 있게 해
to-the-goal.tistory.com
이어서 작성합니다.
2.3.5 테스트 코드 개선
애플리케이션 코드만이 리팩토링 대상이 아니고, 테스트 코드 또한
언제든지 내부 구조와 설계를 개선해 더 깔끔하고 이해하기 쉽고, 변경이 용이한 코드를 만들 필요가 있다.
@Before
(JUnit5에서 @BeforeEach로 변경되었다.)
@BeforeEach
public void setup() {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
userDao = context.getBean("userDao", IndependentUserDao.class);
}
기존 메소드의 로컬변수였던 AC를 테스트 메소드에서 접근할 수 있도록 인스턴스 변수로 변경하고,
setUp()에서 getBean()이 돌려주는 오브젝트를 userDao에 저장한다.
당연히 테스트는 잘 동작한다.
우리는 프레임워크에 대해서 다시 한 번 생각해보자.
프레임워크는 스스로 제어권을 가지며, 주도적으로 동작하고, 개발자가 만든 코드는 프레임워크에 의해 수동적으로 실행된다.
코드만으로는 실행 흐름이 잘 보이지 않기 때문에 프레임워크가 어떻게 사용하는지 잘 이해하고 있어야한다.
JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @BeforeEach가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
- @AfterEach가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트 결과를 종합해서 돌려준다.
실제로는 정말 복잡하지만 이러한 7단계를 거쳐 테스트가 진행된다.
꼭 기억해야할 것은 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다!
한 번 만들어진 테스트 클래스 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다.
매번 새로 만들면 비효율적인 것이 아닌가?
이러한 비효율로 인해 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장할 수 있다.
덕분에 인스턴스 변수도 부담 없이 사용할 수 있다.
그렇다면 테스트 메소드 일부에서 공통적으로 사용되는 코드는 어떻게 분리해야할까?
- 일반적인 메소드 추출 방법을 사용해 메소드를 분리, 테스트 메소드에서 직접 호출
- 공통적인 특징을 지닌 테스트 메소드를 모아 별도 클래스로 분리
픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처라고 한다.
일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에
@BeforeEach 메소드를 이용해 생성해두면 편리하다.
private IndependentUserDao userDao;
private User user1;
private User user2;
private User user3;
@BeforeEach
public void setup() {
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
userDao = context.getBean("userDao", IndependentUserDao.class);
this.user1 = new User("aa", "김김", "spring1");
this.user2 = new User("bb", "이이", "spring2");
this.user3 = new User("cc", "박박", "spring3");
}
기존 테스트 코드에서는 User user1 = new User.. 메소드 로컬 변수를 반복적으로 선언했는데
이런 식으로 활용하면 로컬 변수를 따로 선언하지 않고 따로 깔끔하게 테스트 코드 작성이 가능하다.
2.4 스프링 테스트 적용
현재 코드는 테스트 클래스 안에서 테스트 메소드가 n개 존재하면 ApplicationContext가 n번 만들어진다.
지금이야 단순히 학습을 위해 Bean의 가짓수가 몇 없지만,
Bean이 많아지고 복잡해진다면 AC 생성에 적지않은 시간이 걸릴 것이다.
AC가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다!
AC가 초기화될 때 어떤 빈 오브젝트는 자체적인 초기화 작업,
어떤 빈은 독자적으로 많은 리소스 할당, 독립적 스레드를 띄우기도 한다.
테스트를 가능한 독립적으로 새로운 오브젝트를 만들어 사용하는 것이 원칙이나
AC처럼 생성에 많은 시간과 자원이 소모되는 경우 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
AC는 초기화 이후 내부 상태가 바뀌는 일은 거의 없기 때문에 여러 테스트 공유가 가능하다.
근데 문제는 JUnit이 테스트 클래스 오브젝트를 매번 새로 만든다.
따라서 오브젝트 레벨에서 AC를 유지한다면 의미가 없어진다.
-> 스태틱 필드에 저장
이 메소드에서 AC를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용할 수 있으나,
스프링이 직접 제공하는 AC 테스트 지원 기능을 사용하는 것이 더 편리하다.
2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리
스프링 테스트 컨텍스트 프레임워크 적용
// 책 예제와 어노테이션은 다르다.
@SpringJUnitConfig(DaoFactory.class)
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private IndependentUserDao userDao;
private User user1;
private User user2;
private User user3;
@BeforeEach
public void setup() {
this.userDao = this.context.getBean("userDao", IndependentUserDao.class);
...
AC를 @Autowired를 이용해 아주 단순하게 가져올 수 있다.
지금 이렇게 리팩토링한 상태에서, 아래 코드를 실행하면
@BeforeEach
public void setup() {
System.out.println(this.context);
System.out.println(this);
...
}

콘솔을 확인하면 context의 참조값은 매번 동일하고,
this, 즉 테스트 오브젝트의 참조값은 달라 매 실행마다 새롭게 생성된다.
스프링의 JUnit 확장 기능은 테스트가 실행되기 전 한 번만 애플리케이션 컨텍스트를 만들어두고,
테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해
애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해준다.
테스트 클래스의 컨텍스트 공유
여러 개의 테스트 클래스가 모두 같은 설정 파일을 가진 애플리케이션 컨텍스트를 사용한다면,
스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.
따라서 수백 개의 테스트 클래스가 있어도 같은 설정 파일을 사용한다면,
테스트 전체에 걸쳐 한 개의 애플리케이션 컨텍스트만 만들어 사용ㅎ나다.
-> 테스트 성능 대폭 향상 가능
@Autowired
스프링 DI에 사용되는 특별한 애노테이션이다.
@Autowired가 붙은 인스턴스 변수가 있으면 테스트 컨텍스트 프레임워크는
변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입과 일치하는 빈이 있으면 인스턴스 변수에 주입해준다.
별도 DI 설정 필드 타입 정보를 이용해 빈을 자동으로 가져올 수 있는데,
이런 방법을 타입에 의한 자동 와이어링이라고 한다.
앞서 본 코드에서는 빈 오브젝트가 아니라 ApplicationContext에 DI를 사용했다.
스프링 ApplicationContext는 초기화할 때 자기 자신도 빈으로 등록하여
애플리케이션 컨텍스트 내에 AC 타입의 빈이 존재하고, 그러므로 DI도 가능하다.
@SpringJUnitConfig(DaoFactory.class)
public class UserDaoTest {
@Autowired
private IndependentUserDao userDao;
private User user1;
private User user2;
private User user3;
@BeforeEach
public void setup() {
this.user1 = new User("aa", "김김", "spring1");
this.user2 = new User("bb", "이이", "spring2");
this.user3 = new User("cc", "박박", "spring3");
}
...
이제 AC를 DI 받아서 DL 방식으로 UserDao를 가져올 때보다 테스트 코드를 더 깔끔하게 만들었다.
@Autowired
1. 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다.
- 같은 타입의 빈이 두 개 이상 있는 경우 타입만으로는 어떤 빈을 가져오는지 결정할 수 없다.
2. 타입으로 가져올 빈 하나를 선택할 수 없는 경우에는 변수의 이름과 같은 이름의 빈을 가져온다.
둘 다 안되면 예외가 발생한다.
2.4.2 DI와 테스트
'구체적인 구현 방식이 절대 바뀌지 않을 것인데 DI를 활용해야 하나?'
- 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다
- 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하면 다른 차원의 서비스 기능을 도입 가능
- 효율적인 테스트를 손쉽게 만들기 위해서 DI 적용 필요 -> 테스트 코드는 가능한 작은 단위로 독립적으로 실행 필요
테스트 코드에 의한 DI
운영 서버에서 작동하고 있는 DB에 deleteAll()을 실행한다면?
어떤 테스트 클래스에서 스프링 테스트 컨텍스트 프레임워크를 적용했다면,
애플리케이션 컨텍스트는 테스트 과정에서 딱 1개만 만들어지고 모든 테스트에서 공유해야한다.
하지만 어떤 테스트에서는 빈 의존관계 변경이 필요할 수도 있는데, 나머지 테스트에서도 오염이 된다.
-> @DirtiesContext 애노테이션으로 해결 가능
스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스 테스트에서 애플리케이션 컨텍스트의 상태를 변경함을 알리고,
테스트 메소드(클래스)를 실행한 이후에는 새로운 AC를 만들어 다음 테스트에서 활용할 수 있도록 한다.
테스트를 위한 별도 DI 설정
2장 초반부에서 살펴보았듯 테스트 코드에서 직접 수동 DI를 하면 단점이 많아진다.
DI를 잘 적용해놓았기 때문에 테스트 환경에 적합한 구성을 가진 설정 파일을 활용해 테스트를 진행할 수 있다.
DI를 이용한 테스트 방법 선택
DI를 테스트에 이용하는 세 가지 방법이 있다.
먼저, 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 속도가 빠르고 가장 간결하다.
때때로 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야한다면 스프링 DI를 사용하자.
개발 환경에 따른 차이가 있기 때문에 다른 설정 파일을 활용하자.
예외적으로 의존관계를 강제로 구성해서 테스트해야할 경우가 있다.
이때는 컨텍스트에서 DI 받은 오브젝트에 테스트 코드에서 수동 DI를 진행하고,
@DirtiesContext 애노테이션을 통해 새로 컨텍스트를 생성해주자.
2.5 학습 테스트로 배우는 스프링
개발자는 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어 제공한 라이브러리 등에 대해서도 테스트를 작성해야한다.
이를 학습 테스트라고 한다. 학습 테스트의 목표는 다음과 같다.
- 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익힌다.
- 기능의 검증보다 자신이 사용할 기술이나 기능에 대해 이해도를 검증한다.
- 정확하고 빠르게 사용법을 익힌다.
따라서 학습 테스트는 테스트 대상보다 테스트 코드 자체에 관심을 갖고 만들어야 한다.
2.5.1 학습 테스트의 장점
- 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
- 테스트 작성에 대한 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
스프링 테스트를 직접 살펴본다면 래퍼런스 문서에는 미처 설명되어있지 않았던 정보가 많이 있으며,
테스트 작성 방법에 대한 좋은 팁을 얻을 수 있을 것이다.
2.5.2 학습 테스트 예제
1. JUnit 테스트 오브젝트 예제
JUnit은 테스트 메소드를 수행할 때마다 새로운 오브젝트를 만든다고 했다. 이를 테스트해보자.
static JUnitTest testObject;
@Test
void test1() {
Assertions.assertThat(this).isNotSameAs(testObject);
testObject = this;
}
@Test
void test2() {
Assertions.assertThat(this).isNotSameAs(testObject);
testObject = this;
}
@Test
void test3() {
Assertions.assertThat(this).isNotSameAs(testObject);
testObject = this;
}
static 필드에 만들어진 오브젝트와 자신을 비교해서 같지 않다는 사실을 확인할 수 있다.
그런데 여기서 1 -> 2 -> 3번 순서로 실행되었다고 할때, 1번 3번이 같을 수도 있음을 테스트해보자.
static Set<JUnitTest2> testObjects = new HashSet<>();
@Test
void test1() {
Assertions.assertThat(testObjects).doesNotContain(this);
testObjects.add(this);
}
@Test
void test2() {
Assertions.assertThat(testObjects).doesNotContain(this);
testObjects.add(this);
}
@Test
void test3() {
Assertions.assertThat(testObjects).doesNotContain(this);
testObjects.add(this);
}
static 변수로 테스트 오브젝트를 저장할 수 있는 컬렉션을 만들어 아이템이 셋에 들어있는지 확인할 수 있다.
2. 스프링 테스트 컨텍스트 테스트
JUnit과 반대로 테스트용 애플리케이션 컨텍스트는 1개만 만들어지며 모든 테스트에서 공유된다.
@Autowired
ApplicationContext context;
static Set<ApplicationContextTest> testObjects = new HashSet<>();
static ApplicationContext contextObject = null;
@Test
public void test1() {
Assertions.assertThat(testObjects).doesNotContain(this);
testObjects.add(this);
Assertions.assertThat(contextObject == null || this.context == contextObject).isTrue();
contextObject = this.context;
}
@Test
public void test2() {
Assertions.assertThat(testObjects).doesNotContain(this);
testObjects.add(this);
Assertions.assertThat(this.context == contextObject).isTrue();
}
(책 예제와는 조금 다르다)
현재 코드에서는 test1()이 먼저 실행되면 두 개의 테스트가 모두 성공하고,
test2() 메소드가 먼저 실행되면 실패가 발생한다.
test1()이 먼저 실행되면, contextObject가 null이기 때문에 테스트가 통과한다.
테스트 통과 이후, contextObject에 현재 만들어져있는 ApplicationContext 참조값이 대입된다.
그 이후에 test2()가 실행되면 같다는게 판별이 나서 성공한다. 하지만 반대 순서면 null이라서 테스트가 실패한다.
테스트는 순서에 종속되지 않고 일관되어야 하지만 이해를 위해 코드를 변경했다!
2.5.3 버그 테스트
버그 테스트란 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트다.
버그 테스트는 일단 실패하도록 만들어야하며 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.
버그 테스트의 필요성
- 테스트의 완성도를 높여준다
- 버그의 내용을 명확하게 분석하게 해준다
- 테스트코드를 만들며 오류를 발생시키는 값의 범위가 어떤 것인지 분석해 기회가 생긴다.
- 기술적인 문제를 해결하는 데 도움이 된다
예제 코드 PR
'spring' 카테고리의 다른 글
| 밴드 플랫폼 서버 개발 일지 - 스프링 시큐리티 세팅 (0) | 2026.06.23 |
|---|---|
| [Spring] 토비의 스프링 Vol.1 2장 - 테스트 (1) (0) | 2026.06.17 |
| [Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (2) (0) | 2026.06.11 |
| [Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (1) (0) | 2026.06.09 |
| [Spring] 토비의 스프링 강의 섹션 3 - 스프링 도입 (0) | 2026.05.04 |