[토비의 스프링] Week2(1.6~2.3)

28 분 소요

토비의 스프링 3.1 1장~2장 1.6~2.3

1.6 싱글톤 레지스트리와 오브젝트 스코프

오브젝트의 동일성(identical)과 동등성(equivalent)

동일성(Identical)

String s1 = new String("고길동");
String s2 = new String("고길동");

s1 == s2; // false; 두 스트링 객체는 담고있는 값이 같을 뿐 동일한 객체는 아니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01bfe5b2-a1ff-4ace-8056-f692a6a0dab2/Untitled.png

두 객체가 완전히 동일한 객체라면 동일한(identical) 객체라고 할 수 있다. s1==s1은 참이지만, s1==s2는 거짓이다.

동등성

String s1 = new String("고길동");
String s2 = new String("고길동");

s1.equals(s2); // true;
// Java String 객체의 동등성 비교는 각 스트링 객체가 실질적으로 담고있는
// 문자열이 같다면 동등하다고 하며 우리의 직관에도 배치되지 않는다.

두 객체의 동등성 조건 - 각 객체에 구현된 equals메서드에는 무엇이 같아야 두 객체가 동등한지를 구현해놓았다. 두 객체는 동일하지 않더라도 동등할 수 있으며, 두 객체가 동일하면 동등하다.

자세히 살펴보기 : Effective Java ITEM 10. equals는 일반 규약을 지켜 재정의 하라

싱글톤(Singleton) 패턴

싱글톤 패턴은 어떤 클래스를 어플리케이션 내에서 제한된 갯수의 인스턴스 갯수를 갖도록 하는 패턴이며, 이름에서 유추할 수 있듯이 일반적으로 단 하나의 오브젝트만 존재한다.

전역에서 하나의 인스턴스를 공유해야 하는 경우에 이 패턴을 사용한다.

싱글턴 패턴을 Java에서 구현하려면

  • 생성자를 private로 지정하여 외부에서 새 인스턴스를 만들지 못하도록 한다.
  • static 필드에 인스턴스를 담을 수 있는 변수를 하나 정의한다.
  • static factory method인 getInstance()를 만들고 메서드가 최초로 호출되는 시점에 오브젝트를 만들어 static 필드의 변수에 저장한다.
  • getInstance() 메서드는 static 필드에 저장된 인스턴스를 반환한다. 호출될 때 마다 static 필드에 저장된 단 하나의 인스턴스를 반환하므로 싱글톤 패턴이다.

그러나 싱글턴 패턴은

  • private 생성자는 상속을 제한한다.
  • 테스트하기 힘들다.

    직접 인스턴스를 만들 수 없기 때문에 테스트가 힘들다.

  • 서버환경에서는 싱글톤이 단 하나임을 보장하지 못한다.

    서버에서 클래스 로더를 어떻게 구성하냐에 따라 static 변수는 단 하나임이 보장되지 않는다. 예를 들면 다수의 JVM에서 분산되어 돌아가는 경우가 있다.

  • 전역 상태의 특성은 바람직하지 않다.

    객체지향 프로그래밍에서 전역 상태는 권장되지 않는 모델이다.

스프링에서의 싱글톤

지금까지 작성한 DaoFactory 코드는 userDao() 메서드를 호출할 때 마다 새로운 객체를 반환한다.

그러나 스프링 Application Context에 설정정보를 등록하고 getBean() 메서드로 가져오는 userDao() 메서드는 항상 동일한 인스턴스를 반환한다. 그래서 Application Context에 이르러 싱글톤 레지스트리(Singleton Registry)라고 하기도 한다.

스프링은 주로 서버환경에서 사용된다. 서버에는 다양한 계층의 오브젝트가 있다. 비즈니스 로직, 서비스 로직, DAO 등등… 그러나 이를 담당하는 객체들을 매 요청마다 생성한다면? 한 번의 요청에 10개의 객체를 생성한다면, 500번의 요청에는 5000개의 객체가 생성될 것인데 GC가 바쁘게 돌아가고 성능 저하는 피할 수 없을 것이다. 그래서 자바 엔터프라이즈에서는 서비스 오브젝트라는 개념이 일찍부터 고안되었다. 엔터프라이즈의 핵심이자 기반인 서블릿에서는 싱글톤으로 멀티쓰레드 환경에서 동작한다.

싱글톤 레지스트리

위에서 언급했던 것 처럼 Java의 싱글톤 구현의 정석은 여러 문제가 있기 때문에, 스프링에서 싱글톤 오브젝트를 생성하고 관리하는 기능을 제공하는 요소를 싱글톤 레지스트리라고 한다. 싱글톤 레지스트리는 평범한 자바 객체도 싱글톤으로 활용할 수 있도록 해준다. 어노테이션을 붙여 스프링 컨테이너에게 제어권을 넘기면 알아서 다 해준다.

싱글톤과 오브젝트의 상태

싱글톤은 멀티쓰레드 환경에서 여러 쓰레드가 접근할 수 있으므로, stateless 방식으로 만들어져야 한다. 만약 인스턴스 필드를 두고 값을 유지하며 동작에 관여하도록 stateful 하게 만들면 race condition에 의해 서버가 망가질 것이다.

Code #10 : stateful한 UserDao

public class UserDao {
	private ConnectionMaker connectionMaker;
	// 생성자가 한 번 설정하면 후에 바뀔 일이 없는 읽기전용 인스턴스 변수.
  // connectionMaker는 스프링이 관리하는 빈이고, 싱글톤이기 때문에
	// 다른 ConnectionMaker 타입의 인스턴스가 할당될 일이 없다.
  // 물론 final 키워드로 지정해 주는 편이 좋을 것이다.
  // 요약 : 싱글톤 빈을 저장하는 인스턴스 변수는 문제가 되지 않는다.

	private Connection c;
	private User user;
	// 각 요청마다 고유한 객체를 담고 있어야 할 필드가 인스턴스 변수로 선언이되면
  // race condition이 발생한다.
	public User get(String id) throws ClassNotFoundException, SQLException {
		this.c = connectionMaker.makeConnection();

		this.user = new User();
		this.user.setId(rs.getString("id"));
		/*
		 * this.user 변수로 접근하여 조작하는 와중에
		 * 다른 쓰레드가 this.user를 덮어 씌우게 된다면?
		 */

		//...

		return this.user;
	}
}

stateless하게 만들면 정보는 어떻게 다뤄야 할까? 파라미터와 리턴 값, 로컬 변수 등을 이용하면 thread-safe함을 보장할 수 있다.

스프링 빈의 스코프

스프링 빈이 생성되고 존재하고 적용되는 범위를 스코프라고 한다. 스프링 빈의 default 스코프는 싱글톤이며, 컨테이너 내에 한 개의 오브젝트만 만들어진다. 만약 Bean을 요청할 때 마다 새로은 인스턴스를 만들도록 하고 싶다면 prototype 스코프를 고려할 수 있다. 그 외에도 매 요청마다 생성되는 request 스코프, 웹의 세션에 대응하는 session 스코프도 있다.

1.7 의존관계 주입(DI)

IoC는 객체지향 패러다임에서 중요하게 여겨지는 개념으로, DaoFactory처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화한 것이 스프링의 IoC 컨테이너다.

스프링을 IoC 컨테이너로 부르기에는 단순한 템플릿 메서드 패턴을 이용해 만들어진 프레임워크,인지, 서블릿처럼 동작하는 서비스 컨테이너 뜻인지 이해하기 어렵기 때문에 스프링의 IoC 방식을 의존관계 주입(Dependency Injection)이라 부른다. 런타임에 의존관계가 정해진다는 의도를 담고 있다.

의존관계

두 개의 클래스가 있다. A가 B를 의존한다면 B의 변화가 A에 영향을 미친다는 의미이다. A가 B의 메서드를 사용하는 경우가 ㄷ대표적이다. 사용에 대한 의존관계가 있다고 말할 수 있다. B의 메서드 시그니쳐가 바뀌거나 내부 동작이 바뀌면 A의 코드를 수정해야하거나 결과에 영향이 미칠 수 있다. UML 모델에선 점선으로 표기하며, 아래 그림을 참조하면 된다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/522c9e12-8416-4963-8ea3-c01e00dfb724/Untitled.png

UserDao의 경우에는, ConnectionMaker 인터페이스에 의존하고 있다. 그러나 인터페이스를 구현한 클래스가 다른 것으로 바뀌거나 인터페이스에 정의된 메서드가 아닌 내부에서 사용하는 메서드에 변화가 생겨도 UserDao는 동작할 것이다. 인터페이스에 의존하면 그 구현 클래스와의 관계는 느슨해지며 변화에 영향을 많이 받지 않는다. UserDao 입장에선 구현 클래스의 존재를 알지 못한다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bb56b8de-e88f-49d5-8ed5-32239ecf9785/Untitled.png

의존관계 주입

UML에서 의존관계는 설계 모델의 관점에서 이야기하는 것이다. 모델이나 코드에서 드러나는 의존관계 이외에도 런타임에 오브젝트 사이에서 만들어ㅏ지는 의존 관계를 런타임 의존관계라 부르며, 위에서 다뤘던 설계 모델의 관점에서 의존 관계가 구체화된다고 볼 수 있다. 런타임에 의존관계를 맺는 오브젝트를 의존 오브젝트라고 한다.

구체적인 의존 오브젝트와 클라이언트 오브젝트를 연결해주는 작업을 의존관계 주입이라고 부른다. UserDao가 DConnectionMaker 인스턴스를 인터페이스를 통해 받아들이는 경우이다.

의존관계 주입은 다음 세 가지 조건을 따른다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 즉 어떤 구현 클래스를 쓰는지 UserDao 코드에선 알 수 없으며 인터페이스에만 의존하고 있는 상태이다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리와 같은 제3의 존재가 결정한다.
  • 이 외부 주체가 실질적으로 사용할 오브젝트를 주입/제공하는 시점에 의존관계가 만들어진다.

이 제 3의 존재는 Application Context, Bean Factory, IoC 컨테이너 등이다.

Code #11 : 의존관계 주입을 적용한 생성자

public UserDao(ConnectionMaker connectionMaker){
		this.connectionMaker = connectionMaker;
		// 자신이 사용할 오브젝트는 생성자 파라미터를 통해서 전달받는다.
}

주의해야 할 점은 외부에서 오브젝트를 파라미터로 전달해주었다고 해서 의존관계 주입은 아니다. 반드시 인터페이스 타입으로 전달해주어야 한다.

의존관계 검색

외부로 주입을 받지 않고 스스로 검색하여 의존관계를 맺는 의존관계 검색(Dependency Lookup)도 있다. 자신이 사용할 구체적인 클래스를 지목하지는 않지만, 컨테이너에게 요청한다.

Code #12 : 의존관계 검색을 이용하는 생성자

public UserDao(){
		AnnotationConfigApplicationContext context =
				new AnnotationConfigApplicationContext(DaoFactory.class);
		this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);

}

ApplicationContext에 “connectionMaker” 이름을 가진 Bean을 요청한다. 해당 이름을 가진 Bean을 직접 생성하지는 않기 때문에 여전히 IoC 원칙을 준수한다.

의존관계 주입 vs 검색

일반적으로는 의존관계 주입을 사용하는 것이 깔끔하다. 의존관계 검색은 스프링 API나 팩토리 클래스에 의존하게 된다.

그러나 의존관계 검색을 사용해야 할 때가 있다. 스프링 IoC와 DI 컨테이너가 적용되어 있지만 한 번은 반드시 의존관계 검색을 수행해야 한다. 엔트리 포인트인 main()에서는 DI로 주입받을 방법이 없기 때문이다. 또한 의존관계 검색을 구현한 클래스는 자기 자신이 스프링의 Bean일 필요가 전혀 없다. ConnectionMaker는 스프링의 Bean이여야 하지만, UserDao는 그렇지 않아도 된다는 소리이다. 그 반대로 의존관계 주입에서는 UserDao도 반드시 Spring이 관리하는 Bean이 되어야 한다.

DI의 장점은 무엇일까?

앞에서 설명한 원칙과 객체지향 설계를 따랐을 때 얻는 이점들을 그대로 얻을 수 있다. 코드에는 의존하는 클래스가 구체적으로 나타나지 않아 결합도가 낮기 때문에 변경을 통한 확장에 자유롭다.

시나리오 1. 데이터베이스 교체하기

배포 시에는 실제 데이터베이스 서버를 이용해야겠지만, 개발할 때는 개발자 로컬의 데이터베이스를 이용하는 것이 일반적이다. DI를 적용한 코드에서는 이 변경이 빠르고 자유롭다. 예를 들면, ConnectionMaker.connectionMaker()에서 반환하는 오브젝트만 수정하면 끝이다.

시나리오 2. 부가기능 추가

DAO가 DB에 얼마나 자주 연결하는지 파악하고 싶다고 해보자. 개발하다보면 DAO의 종류가 많아질 텐데 그 모든 코드에 커넥션을 호출할 때 마다 카운트를 증가시키는 로직을 추가한다면 손이 많이 가겠지만, 실제 ConnectionMaker 구체 클래스와 인터페이스 사이에 하나의 오브젝트를 더 끼워넣고, 카운트하는 로직을 추가하는게 다일 것이다.

Code #13 : 연결 횟수를 Counting하는 ConnectionMaker 구체 클래스

import java.sql.Connection;
import java.sql.SQLException;

public class CountingConnectionMaker implements ConnectionMaker {

	int counter = 0;
	private ConnectionMaker realConnectionMaker;

	public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
		this.realConnectionMaker = realConnectionMaker;
		// 실제 사용될 ConnectionMaker 구체클래스를 받는다.
	}

	@Override
	public Connection makeConnection() throws ClassNotFoundException, SQLException {
		this.counter++;
		return realConnectionMaker.makeConnection();
		// 실제 오브젝트를 반환하기 전에 counter를 한 번 증가시켜준다.
	}

	public int getCounter() {
		return this.counter;
	}
}

스프링에서 사용하는 메서드 이용 의존관계 주입

지금까지 예제에서는 의존관계 주입에 생성자 파라미터를 이용했지만, 일반적으로 스프링에선 setter 메서드를 통한 DI를 사용했다. 자바 빈 규격을 따르기 때문에 나중에 사용하기 편하다.

메서드 이름은 의미있고 단순한 이름이 제일 좋지만 특별한 아이디어가 없다면 메서드를 통해 주입받을 오브젝트 타입이름을 붙이는 게 제일 좋다. setConnectionMaker()가 그 예다.

1.8 XML을 이용한 설정

스프링 DI 의존관계 설정하는 방법은 XML과 Annotation 크게 2가지가 있다. 그 중 XML은

  1. 단순한 텍스트 파일로 수정이 용이
  2. 쉽게 이해 가능
  3. 변경사항 발생 시 재컴파일하지 않아도 됨
  4. 빠르게 변경사항 반영 가능
  5. 스키마나 DTD를 이용해 정해진 포맷을 따라 작성되었는지 검사 가능 (문법 검사)

과 같은 장점을 가지고 있다.

XML 설정하기

스프링의 Application Context는 XML에 담긴 DI정보를 활용할 수 있다. 이 XML을 작성할 때 루트 엘리먼트를 로 두고 작성해 나가면 된다.

어노테이션으로 작성한 하나의 @Bean 메소드에서 얻을 수 있는 빈의 DI 정보는

  • 빈의 이름 : @Bean 메소드 이름으로 지정이 되며, getBean()에서 사용하는 이름
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메소드를 통해 의존 오브젝트를 주입하는데, 주입하는 오브젝트 또한 빈일 것이고 이름으로 가져온다. 여러 개에 의존할 수도 있음

클래스 - XML 설정 1:1 대응

기존 클래스에 작성된 정보를 XML로 1:1 대응하면 된다. 주의할 점은, 빈의 클래스 항목에 작성하는 내용은 반환하고자 하는 실제 구체 클래스이며, 인터페이스를 작성하지 않도록 하자. 또한 패키지 경로를 모두 작성하여야 한다.

Code #14 : 메서드 → XML 전환 예시

@Bean //----------------------------------->  <bean
public ConnectionMaker{
    connectionMaker(){ //------------------> id="connectionMaker
        return new DConnectionMaker(); // -> class="springbook...DConnectionMaker" />
    }
}

<bean id="connectionMaker class="springbook...DConnectionMaker"/>

의존 관계를 주입받는 Bean 설정하기

앞에서 작성한 userDao는 connectionMaker 의존을 주입받는다. 스프링은 자바빈을 관례를 따라서 setter 메서드를 프로퍼티로 사용한다. 즉 setConnectionMaker()라는 메서드를 정의해 놓으면 set을 제외한 ConnectionMaker라는 프로퍼티 이름으로 사용한다.

XML에서는 태그를 이용해 의존 관계를 정의한다. name과 ref라는 두 개의 애트리뷰트를 갖는다. name은 프로퍼티의 이름이고, ref는 주입해줄 오브젝트 빈의 이름이다.

Code #15 : 메서드 → XML 의존주입 전환

userDao.setConnectionMaker(connectionMaker());
        ___|______________ _______________
				 /     \                    \
 _______/       \______________      \_______________
<property name="connectionMaker" ref="connectionMaker" />

주의해야 할 점은, setter 메서드는 Java의 컨벤션인 Camel Case를 따라 C가 대문자이지만, xml에 작성할 때도 set을 제외한 부분에서 첫 글자는 소문자로 작성해 주어야 한다.

Code #16 : userDao 빈 설정 마치기

<bean id ="userDao" class="{classpath....}.UserDao">
	<property name="connectionMaker" ref="connectionMaker" />
</bean>

Code #17 : XML 완성

<beans>
	<bean id="connectionMaker class="springbook...DConnectionMaker"/>
	<bean id ="userDao" class="{classpath....}.UserDao">
		<property name="connectionMaker" ref="connectionMaker" />
	</bean>
</beans>

ApplicationContext 설정하기

XML 빈의 의존관계 정보를 이용하는 DI에는 GenericXmlApplicationContext를 사용한다. ApplicationContext가 사용하는 파일은 applicationContext.xml로 만드는게 컨벤션이다.

Code #18 : applicationContext.xml 예시

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<bean id="connectionMaker class="springbook...DConnectionMaker"/>
	<bean id ="userDao" class="{classpath....}.UserDao">
		<property name="connectionMaker" ref="connectionMaker" />
	</bean>
</beans>

Code #19 : ApplicationContext 불러오기

ApplicationContext context = new GenericXmlApplicationContext("{path}/applicationContext.xml");
ApplicationContext context = new ClassPathApplicationContext("daoContext.xml", UserDao.class);
//UserDao 패키지가 위치한 클래스패스를 가져와 상대경로를 지정할 수 있다.

DataSource 인터페이스 적용

ConnectionMaker는 DB 커넥션 생성 딱 하나만 당당하는 인터페이스다. Java에서는 DB 커넥션을 가져오는 오브젝트의 기능을 담은 DataSource라는 인터페이스가 존재한다. 이에 맞춰 구현된 클래스들을 사용하는 것으로 충분하다.

jdbc 라이브러리를 추가하자.

Code #20 : DataSource

package javax.sql

public interface DataSource extends CommonDataSource, Wrapper{
    Connection getConnction() throws SQLException;
		// 커넥션을 가져오는 메서드인 getConnection() 사용
		//...
}

import javax.sql.DataSource;

public class UserDao{
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource){
        this.dataSource = dataSource;
    }

    public void add(User user) throws SQLException{
        Connection c = dataSource.getConnction();
        //...
    }
    //...
}

Code #21 : 자바 코드로 url, id, pw 설정하기

@Bean
public DataSource dataSource(){
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();

    dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
    dataSource.setUrl("jdbc:mysql://localhost/springbook");
    dataSource.setUsername("myid");
    dataSource.setPassword("mypw");

    return dataSource;
}

@Bean
public UserDao userDao(){
    UserDao userDao = new UserDao();
    userDao.setDataSource(dataSource()); // DataSource를 주입받는다.
    return userDao;
}

Code #21 : XML로 로그인 정보, url 설정하기

<bean id="dataSource"
    class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />
	<property name="driverClass" value="com.mysql.jdbc.Driver" />
	<!--
		driver class는 java.lang.Class를 받도록 되어있지만 value는 String 값이다.
		스프링이 파라미터 타입을 참고해서 적당히 추론하여 변환해준다.
		내부적으론 아래와 같은 작업을 해준다고 생각하면 된다.
		Class driverClass = Class.forName("com.mysql.jdbc.Driver");
		dataSource.setDriverClass(driverClass);
	-->
	<property name="url" value="jdbc://localhost/springbook" />
	<property name="username" value="myid" />
	<property name="password" value="mypw" />
</bean>
import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
    JUnitCore.main("spring.user.dao.UserDaoTest");
}

출처:

https://haviyj.tistory.com/15

[What do you want?]

추가해야 할 라이브러리

junit

Code : 1장에서 소개된 UserDao가 잘 동작하는지 확인하는 테스트

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.setld("user");
		user.setName("백기선");
		user.setPassword("married");

		dao .add(user);

		System.out.println(user.getId() + " 등록 성공“);
		User user2 =dao.get(user.getld());
		System.out.println(user2.getName());
		System.out.println(user2.getPassword());
		System.out.println(user2.getld() + " 조회 성공");
	}
}

애플리케이션이 복잡하면 할 수록, 앞으로 나아가기 위해 뒤돌아보지 않게 되는 확신을 가지게 하는 것이 테스트다. 테스트를 잘 작성한다면 리팩토링과 확장 단계에서 문제가 없는지 코드가 의도대로 동작하는지를 확인하는 중요한 수단이다.

테스트 코드를 사용하지 않는다면? 브라우저로 DAO 테스트를 한다고 가정하자.

  1. 웹 어플리케이션에서 DAO를 손으로 한다고 생각하면, 서비스 레이어, MVC 레이어까지 모두 완성해서 직접 접속할 수 있는 페이지로 접속한 뒤, 일일이 값을 입력하고 확인하는 작업을 손으로 해야한다.
  2. 문제가 생기면 DAO뿐 아니라 모든 레이어의 검증을 거쳐야 한다. 즉 DAO 외적인 문제 해결에 많은 시간을 소모할 것이다.

테스트하고자 하는 대상이 명확하면 그 대상에만 집중해서 테스트하는 것이 바람직하다. IoC를 다루면서 지겹도록 보았을 관심사의 분리 또한 테스트에 적용된다. 이를 단위 테스트(unit test)라고 한다.

관심사는 작게는 DAO의 메서드 하나부터, 크게는 유저 관리기능 전체로 볼 수도 있다. 정하기 나름이지만 집중해서 효율적으로 테스트할 수 있도록 범위를 확정한다. 책에서 소개된 UserDaoTest는 그 DAO를 테스트하는데만 집중했다. 그러나 각 기능이 잘 동작하더라도 나중에는 로그인→기능사용→로그아웃까지 여러 기능들이 한 곳에 참여할 때도 잘 동작하는지, 한 번의 호흡으로 테스트해야 할 필요도 있다.

테스트를 자동으로 수행할 수 있다면, 자주 반복하여도 큰 부담이 없어 코드를 수정할 때마다 수행할 수 있을 것이다.

UserDaoTest 개선하기

맨 위의 코드는 몇 가지 문제점이 있다.

  1. 테스트 수행은 자동적으로 해주지만, 결과는 여전히 눈으로 검증하는 과정이 필요하다.
  2. 테스트가 많아진다면 각각의 모든 테스트 main() 메서드를 실행하는건 보통 일이 아니다.

테스트 검증의 자동화

테스트는 성공과 실패 두 가지의 결과를 가질 수 있다. 실패는 또 에러가 발생해서 테스트를 마치지 못한 경우와 테스트 수행은 문제없이 되었지만 예상했던 결과와 다르게 나오는 경우로 구분할 수 있다. 전자를 테스트 에러, 후자를 테스트 실패로 책에서는 구분한다.

UserDaoTest에서는, 콘솔에 결과를 출력해 눈으로 비교하는 과정이 있었지만, 그 대신 아래 코드로 대체하면 결과를 자동으로 확인할 수 있다.

if(!user.getName().equals(user2.getName())) {
		//테스트 성공!
}
else{
		//테스트 실패!
}

자동화 테스트 xUnit을 개발한 켄트 백은 “테스트란 개발자가 맘 편히 잠자리에 들 수 있도록 하는 것”이라고 정의했다. 작성한 코드의 기능을 모두 점검할 수 있는 포괄적인 테스트(comprehensive test)를 잘 작성하면, 수정에 코드가 망가지지 않았는지 하는 걱정은 하지 않아도 된다.

테스트의 효율적인 수행과 결과관리

UserDaoTest는 테스트의 기능을 모두 갖추었지만, 다수의 테스트를 편리하게 수행하는 데에는 main() 메서드에 구현된 내용으로는 부담이 크다. 그래서 JUnit이라는 테스트 프레임워크를 사용하기로 한다.

JUnit은 프레임워크로, IoC가 핵심이기 때문에 JUnit 프레임워크에 개발자의 코드 흐름에 대한 제어권을 넘겨주어야 한다. 그러기 위해서는 main() 메서드를 테스트 메서드로 전환하여야 하는데, 2가지 조건을 따라야 한다.

  1. public으로 선언할 것
  2. @Test 어노테이션 붙여줄 것

Code : JUnit 테스트 메서드로 전환

import org.junit.Test;

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicaionContext.xml");
        UserDao dao = context.getBean("userDao", UserDao.class);
				//...
    }
}

테스트 메서드로 전환을 마쳤다면 테스트 결과를 검증하는 if/else를 JUnit에 맞게 바꾸어본다.

if(!user.getName().equals(user2.getName())) {
		//테스트 성공!
}
//--------------------------------------
assertThat(user2.getName(), is(user.getName()));

assertThat() 메서드는 첫 번째 파라미터의 값을 뒤에 나오는 매처(matcher)라는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 한다. is()는 equals()로 비교해주는 매처이다. 만약 결과가 일치하지 않는다면 AssertionError를 던진다.

JUnit 테스트 실행

임의의 클래스에 main() 메서드를 추가하고(개인적으론 별도의 클래스로 분리하는 것이 좋다고 생각), JUnitCore 클래스의 main 정적 메서드를 호출하는 간단한 코드를 넣는다. 파라미터에는 @Test 어노테이션이 붙은 테스트 메서드를 가진 클래스의 이름을 넣는다.

Code : JUnit 테스트의 시작점이 되는 main() 메서드 작성

import org.junit.runner.JUnitCore;
//...
public static void main(String[] args) {
    JUnitCore.main("spring.user.dao.UserDaoTest");
}

JUnit

테스트 결과의 일관성

테스트는 코드에 변경사항이 없다면 항상 동일한 결과를 내야한다.

예를 들어 DAO를 테스트할 때, 이전 테스트에서 등록했던 유저정보를 지우지 않고 다음 테스트에서 똑같은 유저정보를 등록한다면, PRIMARY KEY 중복으로 인하여 등록이 되지 않을 것이고 테스트는 실패할 것이다., 그렇기에 테스트를 진행하기 전과 후의 상태가 동일하도록 테스트 중 발생한 변경을 정리해주어야 한다.

포괄적인 테스트

개발자는 성공하는 테스트만 골라서 만드는 경향이 있다. 코드를 작성하고 앞으로 나아가고 싶어 하기에 무의식중에 성공할만한 테스트만 작성할 수 있다는 것이다. 존재하는 id의 회원정보를 잘 가져오는 것도 중요하지만, 존재하지 않는 id를 요청했을 때에는 어떻게 반응할지 결정하고 꼼꼼하게 테스트를 작성하는 것이 중요하다. 항상 네거티브 테스트를 만들라는 스프링의 아버지 로드 존슨의 조언을 가슴에 새기도록 하자.

요청이 실패하는 경우에는 결과값을 null로 반환하던가, 예외를 던질 수 있다. JUnit에서는 중간에 발생하는 예외 또한 테스트 결과로 활용할 수 있다.@Test(expeced={발생을 기대하는 예외 클래스}.class)

JUnit의 테스트 흐름

  1. @Test 어노테이션이 붙으면서 public void 이며 파라미터가 없는 테스트메서드를 모두 찾는다.
  2. 테스트를 대상이 되는 클래스의 Object를 하나 만든다. ***
  3. @Before 메소드가 있다면 실행
  4. @Test 메소드를 하나 실행하고 결과를 저장
  5. @After 메소드가 있다면 실행
  6. 모든 @Test 메소드에 대해 2~5 반복
  7. 테스트 결과 반환

위에서 테스트의 공통적인 사전준비나 테스트 결과 정리는 @Before와 @After가 붙은 메서드에 잘 작성하면 되며, 만약 테스트 메서드와 공유해야 하는 데이터가 있다면 인스턴스 변수를 이용하자.

주의해야 할 점은 각 테스트메서드 마다 테스트 클래스의 인스턴스를 하나씩 만든다는 점이다. @Before→@Test→@After 사이클은 테스트 메서드의 갯수만큼 수행된다. 이는 서로의 테스트가 서로에게 영향을 주지 않도록 한 JUnit의 디자인이다. 다수의 테스트 수행시 순서는 매번 바뀌며, 예측할 수 없다.

픽스쳐

테스트를 수행하는데 필요한 정보나 오브젝트를 픽스쳐라고 한다. 이 오브젝트 만드는 코드가 중복된다면 @Before에 작성하자.

TDD : 테스트가 이끄는 개발(Test Driven Development)

테스트를 먼저 만들고 테스트가 실패하는 것을 확인한 뒤에 코드를 작성하거나 수정하는 것을 TDD라고 한다. 코드를 어떻게 테스트할까 생각하는 대신, 테스트를 만들며 추가하고 싶은 기능을 표현할 수 있다.

테스트 코드에는

  1. 어떤 조건을 가지고
  2. 어떤 행위를 할 때
  3. 어떤 결과가 나온다

라는 것이 표현되어 있다. 기능설계, 구현, 테스트라는 일반적인 개발 흐름에서 기능설계 부분을 테스트를 작성함으로써 대신하는 것이다. 테스트가 하나의 기능 정의서라고 봐도 좋지 않을까?

“실패한 테스트를 성공하도록 만드는 목적이 아닌 코드는 작성하지 않는다”는 원칙이 TDD의 핵심이다. 테스트를 먼저 만들고 그 테스트가 통과할 수 있도록 작성하는 TDD의 장점은

  1. 테스트부터 작성하기 때문에 빼먹지 않고 테스트 작성을 할 수 있다.
  2. 코드에 대한 피드백을 빠르게 받아볼 수 있다.
  3. 기능이 잘 작동하는지 확신을 가질 수 있다.

TDD에서는 테스트를 작성하고 코드를 만드는 주기를 가능한 짧게 가져가도록 권장한다.

Reference

토비의 스프링 예제코드 by icednut

토비의 스프링 - 1장 오브젝트와 의존관계 by wan-blog (다이어그램 스캔본 출처)

댓글남기기