자바, 스프링 개발자들의 종착역이자, 주기적으로 다시 돌아오는 그곳, <토비의 스프링 3.1>을 드디어 읽는다.
한창 개발 걸음마를 막 떼고 스프링으로 아장아장 기어다니며 CRUD를 할 때즈음, 토비의 스프링을 찬양하고 있는 많은 개발자들을 보았고, 역시 지식이 깊어지려면, 책을 통해서 지식을 정리하고 깊이를 다지는 시간이 필요하겠구나, 생각했다. 하지만, 개발을 이제 막 배운 그때의 나에게 토비 책은 너무나도 무시무시했고, 토비책을 읽다가 개발을 포기한 사람들의 증언도 여럿 읽고 나니, 이 책은 개발자로서 레베루업이 필요할 때즈음, 다시 꺼내봐야겠다고 생각했다.
그리고 지금이다. 퇴사 후, 이직을 준비하는 동안, 지식의 깊이를 좀 더 다져놓고 싶다는 생각이 들었고, 집앞 도서관에서 (아무도 빌려가지 않는) 토비님의 책을 빌려서 읽기 시작했다. 처음에는 가볍게 읽기 시작했지만, 읽을 수록 인사이트가 쌓여, 기록하면서 제대로 읽고 싶어졌다. 그래서 나의 세컨 브레인인 이곳 블로그에 짧은 글들로 그 내용을 적어보며, 토비님으로부터 얻은 인사이트를 내것으로 만들어보고자 한다.
스프링이 주는 핵심 가치 2가지
객체지향설계
테스트
테스트 없이는 스프링도 의미없다.
테스트의 필요성
일반적으로 기능하나를 개발한다고 하면, DAO 를 테스트하기 위해서는 데이터를 표시해줄 화면, 컨트롤러, 서비스 단까지 실제 테스트하고자 하는 것보다 더 많은 양의 코드를 미리 작성해두어야 한다.
이 경우, 핵심 기능 자체만 테스트하기가 어렵다. 테스트의 과정이 길기 때문에 어디서에 에러가 났는지 디버깅하기가 쉽지 않기 떄문이다.
작은 단위의 테스트가 필요하다. 단위테스트라고 한다. 테스트 코드 역시 한번에 하나의 관심사만 갖고 있을 수 있도록 해야한다.
단위테스트를 하면 각 기능당 발생할 수 있는 버그는 웬만큼 잡고 어플리케이션이 출시되기 때문에 실제 QA 에서 보고되는 오류의 수가 월등히 줄어들 수 있다.
자동수행되는 테스트코드가 바람직하다. 매 테스트 때마다 DB 에 조작을 가하거나, 추가적인 작업이 필요하다면, 테스트를 반복해서 하기도 어렵고 쉽게 넘어가버리게 된다.
UerDaoTest 개선하기
현재 UserDaoTest 는 두 가지 문제점이 있다.
데이터의 입력 등은 자동화했지만, 여전히 테스트 결과는 화면에 출력되고, 사람의 눈으로 직접 확인해야한다.
테스트를 위해서 main() 매소드를 계속 직접 실행시켜줘야한다.
필요한 것
테스트 결과 검증의 자동화
테스트를 위한 데이터 베이스 상태 원복
개발자를 위한 테스팅 프레임워크, JUnit
JUnit 테스트로 전환하기
테스트 지원 도구로써 자바에서 단위테스트 하기에 유리하다.
JUnit 테스트로의 전환
JUnit 테스트 프레임워크를 이용함으로써, IoC 가 발생하게 된다. 어플리케이션 실행을 위한 오브젝트를 생성, 실행하는 일은 모두 프레임워크에게도 제어권이 넘어온다. 따라서 main() 매소드는 더이상 필요하지 않다.
테스트 메소드의 전환
JUnit 프레임워크가 요구하는 조건 2가지
메소드는 public 으로 선언한다.
테스트를 위한 메소드에 @Test 어노테이션을 붙여준다.
검증코드의 전환
if, else 로 구성된 결과 검증 코드를 assertThat() 스태틱 메소드를 이용하여 변경한다. assertThat() 은 첫번째 파라미터를 두번째 파라미터로 오는 매처라고 불리는 조건과 비교하여 일치하면 넘어가고, 아니면 테스트가 실패하도록 해준다.
JUnit 테스트 실행하기
스프링 컨테이너와 마찬가지로 JUnit 프레임워크도 자바코드로 만들어진 프로그램이니까, 어디서든 한번은 실행을 시켜줘야한다.
아무 곳에서 JUnitCore 클래스의 main 매소드를 호출해주는 간단한 코드를 넣어준다.
publicclassUserDaoTest {publicUserDaoTest() { } @TestpublicvoidandAndGet() throwsSQLException {ApplicationContext context =newGenericXmlApplicationContext(newString[]{"applicationContext.xml"});UserDao dao = (UserDao)context.getBean("userDao",UserDao.class);User user =newUser();user.setId("gyumee");user.setName("박성철");user.setPassword("springno1");dao.add(user);User user2 =dao.get(user.getId());Assert.assertThat(user2.getName(),CoreMatchers.is(user.getName()));Assert.assertThat(user2.getPassword(),CoreMatchers.is(user.getPassword())); }publicstaticvoidmain(String[] args) {JUnitCore.main(newString[]{"springbook.user.dao.UserDaoTest"}); }}
테스트 결과의 일관성 유지하기 - 데이터베이스 선처리
현재의 UserDaoTest 라면, 이전 테스트의 결과로 남아있는 DB 에 중복된 데이터가 있을 수 있다. 이를 방지하기 위해서는 UserDaoTest 의 addAndGet() 테스트가 끝나면 자동으로 추가된 사용자 정보를 삭제해서 테스트 이전의 상태로 데이터베이스를 만들어주는 것이 필요하다.
이를 위해서는 기능을 추가해야한다.
deleteAll() : 모든 사용자를 삭제하는 기능
getCount() : 현재 데이터베이스에 존재하는 모든 사용자의 수를 카운트하는 기능
publicclassUserDao {privateDataSource dataSource;publicUserDao() { }publicvoidsetDataSource(DataSource dataSource) {this.dataSource= dataSource; }publicvoidadd(User user) throwsSQLException {... }publicUserget(String id) throwsSQLException {... }publicvoiddeleteAll() throwsSQLException {Connection c =this.dataSource.getConnection();PreparedStatement ps =c.prepareStatement("delete from users");ps.executeUpdate();ps.close();c.close(); }publicintgetCount() throwsSQLException {Connection c =this.dataSource.getConnection();PreparedStatement ps =c.prepareStatement("select count(*) from users");ResultSet rs =ps.executeQuery();rs.next();int count =rs.getInt(1);rs.close();ps.close();c.close();return count; }}publicclassUserDaoTest {publicUserDaoTest() { } @TestpublicvoidandAndGet() throwsSQLException {ApplicationContext context =newGenericXmlApplicationContext(newString[]{"applicationContext.xml"});UserDao dao = (UserDao)context.getBean("userDao",UserDao.class);dao.deleteAll();Assert.assertThat(dao.getCount(),CoreMatchers.is(0));User user =newUser();user.setId("gyumee");user.setName("박성철");user.setPassword("springno1");dao.add(user);Assert.assertThat(dao.getCount(),CoreMatchers.is(1));User user2 =dao.get(user.getId());Assert.assertThat(user2.getName(),CoreMatchers.is(user.getName()));Assert.assertThat(user2.getPassword(),CoreMatchers.is(user.getPassword())); }}
deleteAll() 을 하는 시점은 테스트를 마치기 직전이나 테스트를 시작하기 전일 수 있다. 이때, 테스트를 시작하기 전에 호출해주는 것이 좋은데, 이유는 테스트 직전에 외부 요소에 의해 데이터베이스에 추가된 임의의 데이터들도 모두 날려주고 데이터베이스를 테스트를 하기 위해 딱 좋은 상태로 만들어주기 때문이다.
JUnit 프레임워크 덕분에 테스트가 간편해졌지만, @Before 내에서 어플리케이션 컨텍스트를 생성하는 방식은 테스트 개수가 늘어나는 만큼 반복되어 실행되므로 좋지 않다. 어차피 싱글톤으로 만들어진 UserDao 와 데이터베이스는 모두 상태를 갖고 있지 않으므로, 한번만 만들어지고 계속 반복되어 사용되어도 괜찮다.
@RunWith : JUnit 프레임워크의 테스트 실행방법을 확장할 때 사용하는 어노테이션
SpringJUnit4ClassRunner : JUnit 용 테스트 컨텍스트 프레임워크 확장 클래스. 테스트 진행하는 중에 사용할 어플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration : 자동으로 만들어줄 어플리케이션 컨텍스트의 설정파일 위치를 지정한 것.
하나의 테스트 클래스는 물론 여러개의 다른 테스트 클래스에서도 설정파일이 같다면 하나의 어플리케이션 콘텍스트가 만들어진다. 즉, 설정파일이 같다면 어플리케이션 콘텍스트를 공유한다. 이때문에 테스트의 속도 및 성능이 엄청나게 향상되는 것이다.
하나의 클래스 혹은 설정파일을 공유하는 클래스들 간에서 아래와 같이 출력해서 확인해보면 됨
소프트웨어 개발에서 절대 변하지 않는 것이란 없다. 추후 변경을 위해 작은 작업은 무리가 되지 않는다.
클래스 구현방식은 변하지 않더라도 다른 기능이 추가될 여지는 있다. 그런 기능의 추가와 제거를 손쉽게 하는데 인터페이스를 두고 설계하는 것이 깔끔하다.
테스트가 용이하기 때문이다. 테스트는 최소한의 단위로 진행할 수 있어야 하는데, DI 는 그것을 가능하게 해준다.
테스트에 DI 를 이용하는 방법 3가지
테스트 코드에 의한 DI
테스트 코드 setUp 시에 테스트용으로 오브젝트를 직접 만든다.
어플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 않는 것이 원칙이다. 왜냐하면 테스트 클래스들 간에 모두 공유하여 사용 되기 떄문이다.
@DirtiesContext 어노테이션을 통해 그 문제를 해결할 수 있지만, 이 경우, 어플리케이션 컨텍스트가 매번 만들어지게 된다.
별도의 설정파일을 이용한 DI 설정
테스트 환경을 위한 별도의 설정파일을 생성하여 Configuration 에서 설정해주는 것이다.
컨텍스트 없이 DI 테스트
사실 DI 는 컨테이너 없이도 가능한 작업이다. 아래와 같이 컨테이너를 사용하지 않고 직접 오브젝트를 생성하고 테스트용 DataSource 오브젝트를 만들어 직접 DI 를 해주는 방법도 있다.
하지만 이 경우, JUnit 에서 매번 새로운 테스트 오브젝트를 만들기 때문에 UserDao 가 계속 중복되어 생성된다.
//1. 테스트 코드에 의한 DI @DirtiesContext//이 클래스의 테스트에서만 어플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 이 어노테이션이 붙으면 테스트 컨텍스트는 어플리케이션 컨텍스트 공유를 허용하지 않는다. publicclassUserDaoTest { @AutowiredprivateUserDao dao;... @BeforepublicvoidsetUp() { DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource); }}//2. 별도의 설정파일을 이용한 DI 설정@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = {"/test-applicationContext.xml"})publicclassUserDaoTest { @AutowiredprivateUserDao dao;...}//3. 컨텍스트 없이 DI 테스트 publicclassUserDaoTest {privateUserDao dao;... @BeforepublicvoidsetUp() { dao =newUserDao(); DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource); }}