1. 스프링 프레임워크와 친해지기

스프링은 인기있는 엔터프라이즈 애플리케이션 개발 프레임워크이다.

이 장에서는 다음 주제를 다룬다.

  • 스프링 프레임워크 기본
  • 스프링 프로젝트
  • 스프링 아키텍처 와 모듈
  • 제어의 역전(IoC) 과 의존성 주입(DI)
  • 스프링 개발 환경 설정, Hello World 프로그램, 오토와이어링(autowiring)
  • 관점 지향 프로그래밍(AOP)
  • 스프링 JDBC
  • 트랜잭션 관리
  • 스프링 MVC

스프링 시작하기

스프링은 자바를 위한 오픈소스 엔터프라이즈 애플리케이션 개발 프레임워크이다. 스프링은 Rod Johnson(로드 존슨)에 의해 작성되었으며, 2003년 6월 아파치 2.0 라이센스로 출시되었다. 스프링 프레임워크는 자바애플리케이션 개발을 위해 종합적인 인프라를 제공한다. 스프링은 인프라를 다루며, 우리의 애플리케이션 로직에 집중하도록 도와준다. 스프링은 환경과 규약에 의존적이지 않은 POJOs(Plain Old Java Objects)를 애플리케이션에 빌드할 수 있고, 비침투적으로(Non-invasive: 기술의 적용 사실이 코드에 직접 반영되지 않는 특징) POJOs를 애플리케이션 서비스로 지원한다.

다음은 POJO기반 애플리케이션 개발의 예이다.

  • 자바메소드는 서블릿과 서블릿API에 관련된 것을 작성하지 않아도 HTTP POST/GET 요청을 처리할 수 있다.
  • 자바메소드는 웹서비스API처리 없이 Restful웹서비스로서 행동할 수 있다.
  • 자바메소드는 트랜잭션API처리 없이 데이터베이스 트랜잭션을 실행할 수 있다.
  • 로컬자바메소드는 리모트API처리 없이 리모트프로시저콜(RPC)에 참여할 수 있다.
  • 자바메소드는 JMS API처리 없이 메시지를 처리하거나 소비할 수 있다.
  • 자바메소드는 JMX API처리 없이 애플리케이션 관리를 위한 다양한 기능을 제공할 목적으로서 (Management eXtension) 작업할 수 있다.

간단히 말하면 스프링은 다음과 같이 설명된다.

  • 오픈소스 애플리케이션 프레임워크이다.
  • 경량급 엔터프라이즈 애플리케이션 프레임워크이다.
  • 비침투적인(POJO기반)이다.
  • 모듈화되어 있다.
  • 다른 프레임워크와 확장이 가능하다.
  • 자바엔터프라이즈 애플리케이션의 사실상의 표준이다.

스프링의 장점은 다음과 같다.

  • 경량급이며 POJOs를 사용한 최소한의 비침투적인 개발
  • 의존성주입과 인터페이스지향을 통한 낮은 결합도
  • 관점(Aspects) 과 공통 규약을 통한 평서한 프로그래밍
  • 관점(Aspects) 과 템플릿을 통한 반복적인 코드 감소

스프링 프로젝트는 보안설정, 웹 애플리케이션, 빅데이터, LDAP 등을 구성하는 인프라를 제공한다. 스프링 프레임워크는 스프링프로젝트의 중 일부이다.

사용할 수 있는 다양한 스프링 프로젝트가 있다. 이 책에서는 우리는 스프링4를 사용할 것이다.

다음은 스프링의 일부 프로젝트의 아이콘이다.

다음은 2014년 9월 기준 스프링 프로젝트이다.

  • 스프링 I/O 플랫폼 : 스프링 IO는 현대적인 애플리케이션을 위해서 응집력 있고 버전관리가 된 기본적인 플랫폼안에 핵심(core) 스프링 API를 함께 제공한다. 스프링 IO는 스프링 IO Foundation과 스프링 IO Execution 레이어로 구성되어 있다.
  • 스프링 부트 : 스프링 부트는 작은 스프링 구성과 함께 언제든지 실행할 수 있는 스프링 애플리케이션을 만들수 있게 도와준다.
  • 스프링 프레임워크 : 스프링 프레임워크는 자바 엔터프라이즈 애플리케이션을 위한 오픈 소스 프레임워크다. 자바진을 위한 제어의 역전(inversion of control)을 제공한다. 프레임워크는 개발자들을 위해 많은 템플릿을 제공한다. 템플릿은 인프라 코드는 숨기고 비지니스 로직에 집중할 수 있게 해준다.
  • 스프링 XD : 스프링 XD는 데이터 통합, 실시간분석, 배치프로세싱과 데이터를 내보내기 위한 통합, 분산, 확장가능한 시스템이다. 목적은 빅데이터 애플리케이션의 개발을 단순화하는 것이다.
  • 스프링 클라우드 : 스프링 클라우드는 클래스패스(classpath)를 추가할 때 애플리케이션 동작을 향상하기 위한 라이브러리의 묶음으로 제공함으로써 스프링 부트를 기반으로 한다. 당신은 매우 빨리 시작하기 위한 기본적인 장점을 취할 수 이고, 그리고나서 당신이 필요로 할 때 당신은 사용자 솔루션을 생성하기 위해 라이브러리를 구성하거나 확장할 수 있다.
  • 스프링 데이터 : 스프링 데이터는 데이터 액세스를 단순화거나 관계형 데이터베이스, NoSQL, 비관계형 데이트베이스, 빅테이터, 맵감소(map-reduce)알고리즘과 작업하기 위한 API를 제공한다.
  • 스프링 통합 : 스프링 통합은 외부시스템과 통합하기 위한 스프링 애플리케이션을 위해서 POJO기반 메시징과 경량화 할 수 있게 엔터프라이즈 통합 패턴(Enterprise Integration Patterns EIP)를 따른다.
  • 스프링 배치 : 스프링 배치는 엔터프라이즈 시스템의 일상적인 운영을 위해서 강력한 배치 애플리케이션의 개발이 가능하도록 설계된 종합적이고 경량화 된 배치 프레임워크이다.

다음 그림은 다음 과 같은 스프링 프로젝트의 아이콘을 보여준다: 시큐리티(security), HATEOAS, 소셜(social), AMQP, 웹서비스(web services), 모바일(Mobile), 안드로이드(android), 웹플로우(web flow), 스프링 LDAP(Spring LDAP), Grails

  • 스프링 시큐리티 : 스프링 시큐리티는 강력하고 매우 사용자화 인증 및 액세스 제어 프레임워크이다. 스프링 기반 애플리케이션의 보호하기 위한 사실상의 표준이다.
  • 스프링 HATEOAS : 스프링 HATEOAS는 당신의 스프링 기반 애플리케이션에서 HATEOAS 원리를 따라 REST표현을 만들 수 있게 해준다.
  • 스프링 소셜 : 스프링 소셜은 페이스북, 트위터, 링크드인과 같은 Software as a Service(SaaS) API와 당신의 스프링 애플리케이션을 연결할 수 있게 해준다.
  • 스프링 AMQP : AMQP(Advanced Message Queuing Protocol)은 메시징을 위한 오픈 표준이다.스프링 AMQP는 AMQP 기반 메세징을 위한 솔루션을 제공한다. 예를 들면, AMQP 브로커 RabbitMQ와 함께 사용될 수 있다.
  • 스프링 모바일 : 스프링 모바일은 모바일 웹 애플리케이션의 개발의 단순화를 목적으로 하는 스프링 MVC의 확장이다.
  • 안드로이드를 위한 스프링 : 네이티브 안드로이드 애플리케이션의 개발의 단순화를 목적을 하는 스프링 MVC의 확장이다.
  • 스프링 웹플로우 : 스프링 웹플로우는 페이지 네비게이션, 네비게이션 트리거, 애플리케이션 상태, 호출(invoke)하는 서비스와 같은 웹 기반 스프링 애플리케이션을 위해서 프로세스 작업흐름을 구축하는 인프라를 제공한다. 또한 상태지향(stateful)적이며, 수명이 짧거나 긴 프로세스 흐름이 될 수도 있다.
  • 스프링 웹서비스 : 스프링 웹서비스는 contract-first SOAP 서비스 개발을 촉진하는 목적이 있으며, 또한 XML의 페이로드를 다루기 위한 많은 방법중 하나로 사용되는 유연한 웹 서비스를의 생성을 허락한다.
  • 스프링 LDAP : 경량화 디렉토리 액세스 프로토콜(LDAP)를 사용하는 스프링 기반 애플리케이션을 구축하는데 좀 더 쉽게 만들게 해준다.

스프링 아키텍처 탐험하기

스프링 프레임워크는 모듈화되어 있으며, 각각의 특정은 다른 모듈안에 조직화 되어 있다. 이 섹션에서는 코어 스프링모듈에 관해서 얘기해 볼 것이다. 다음은 스프링 4가지 모듈이다.

코어 컨테이너

코어 컨테이너는 스프링 프레임워크의 중심이다. 다음은 코어 컨테이너의 서브모듈들이다.

  • Core and Beans : IoC와 의존성 주입에 특징을 포함한 프레임워크의 기초적인 부분을 제공한다.
  • Context : JNDI 등록과 비슷한 프레임워크 스타일의 방법안에서의 객체 접근을 의미한다.
  • Expression Language : SpEL(문자열을 코드처럼 인식시켜주는 고급언어)으로 알려져 있다.- 수학적인 표현들을 평가하거나 객체 그래프(특정 시점에 객체들의 참조관계를 나타낸 모습) 수정 및 쿼리를 사용하는 표현언어(Expression Language)이다.

AOP 모듈

AOP는 스프링의 관점지향프로그램(aspect-oriented programming) 구현체를 말한다. AOP는 로깅과 보안과 같은 횡단(cross-cutting) 인프라 코드에서 비지니스 로직을 분리하는걸 말한다.

Instrumentation 모듈

Instrumentation 모듈은 스프링 애프리케이션을 위해서 클래스 인스트루멘테이션(instrumentation) 지원을 제공한다. 인스트루멘테이션은 MBean과 JMX 관리안에 도움을 통해 컨테이너를 자원을 노출시키다.

메세징 모듈

메시징 모듈은 메시징 기반 애플리케이션에 기본적으로 제공되기 위한 Message, MessageChannel, MssageHandler 처럼 스프링 통합(Spring Integration) 프로젝트로 부터 키 추상화으로 왔다.

데이터 액세스 모듈

다음은 데이터 액세스 모듈의 서브모듈이다.

  • JDBC : JDBC 추상레이어를 제공한다.
  • ORM : JPA, JDO, Hibernate, iBATIS를포함한 대중적인 ORM(object-relational mapping) API를 위한 통합레이어를 제공한다.
  • OXM : JAXB, Castor, XMLBeans, JiBX, Xstream를 위한 object/XML 매핑 구현을 지원하기 위해서 추상레이어를 제공한다.
  • JMS : 메세지를 생산하고 소비하는 기능들을 포함한다.
  • Transactions : 프로그램에 입각한 선언적인 트랜잭션 관리를 지원한다.

웹 레이어

웹 레이어는 웹, 웹MVC/서블릿, 웹소켓, 웹MVC-포틀릿(webmvc-portlet) 모률로 구성된다.

  • Web : 이 모듈은 멀티파트 파일 업로드 기능, 서블릿리스너와 웹기반 애플리케이션 컨텍스트를 사용하는 IoC 컨테이너의 초기화와 같은 웹 기반 통합 기능을 제공한다. 또한 스프링의 원격지원의 웹기반 부분을 포함한다.
  • Webmvc : 이 모듈(웹서블릿 모듈이라고도 함)은 웹 애플리케이션의 위한 스프링의 모듈-뷰-컨트롤러 구현을 포함한다. 스프링의 MVC 프레임워크는 스프링 프레임워크의 다른 특징들과 함께 도메인모델, 웹폼, 통합하는 코드들 사이에 깨끗한 분리를 제공한다.
  • Portlet :이 모듈(웹-포틀릿 모듈이라고도 함)은 웹MVC 모듈의 기능을 반영하거나 포틀릿 환경에 사용되는 MVC 구현을 제공한다.
  • WebSocket : 이 모듈은 클라이언트와 서버사이에 2가지 방식 통신을 위한 API를 제공한다. 클라이언트와 서버가 낮은 대기시간과 높은 빈도 수로 이벤트를 교환할 때 매우 유용하다. 금융, 게임, 협력하는 애플리케이션 등에 주요하게 사용한다.

테스트 모듈

테스트모듈은 JUnit 또는 TestNG와 스프링 컴포넌트의 통합 테스트와 단위 테스트를 지원한다.

다음그림은 스프링 4 모듈을 보여준다.

Learining the Inversion of Control

제어의 역전(Inversion of Control - IoC)의존관계 주입(Dependency Injection - DI)은 번갈아 가며(interchangebly) 사용된다. IoC는 DI를 통해 이루어진다. DI는 의존관계를 제공하는 과정이며 IoC는 DI의 결과이다. 스프링의 IoC 컨테이너는 컴포넌트 작성 시 DI 패턴을 따르도록 강제하여 결합도를 낮추고 코드를 추상화 하도록 한다.

의존관계 주입은 객체를 생성하는 스타일 중의 하나로 객체의 참조객체를 외부 엔티티에서 생성하는 방식이다. 다시 말하면, 객체들이 외부 엔티티에 의해 설정되는 것이다. 의존관계 주입은 객체를 스스로 생성하는 방식에 대한 대안이다. 모호하게 들릴 수 있으므로 간단한 예제를 통해 확인해보자.

Packt Publishing 웹사이트를 방문하면 저자명이나 다른 키워드 들(criteria)로 도서를 조회 할 수 있는데 저자명으로 조회된 도서목록들을 출력하는 서비스를 확인해 보자 .

아래 인터페이스는 도서조회 메서드를 정의한다.

public interface BookService {
    List<Book> findAll();
}

아래 클래스는 저자명으로 조회된 도서목록들을 출력한다.

public class BookLister {

    private BookService bookFinder = new BookServiceImpl();

    public List<Book> findByAuthor(String author) {
        List<Book> books = new ArrayList<>();

        for(Book aBook:bookFinder.findAll()) {
            for(String anAuthor:aBook.getAuthors()) {
                if(anAuthor.equals(author)){
                    books.add(aBook);
                    break;
                }
            }
        }

       return books;
    }
}

BookLister 클래스는 BookService 를 구현해야 한다. 즉, BookLister 클래스는 BookService 에 의존적이며 이것의 구현(implementation)없이는 실행될 수 없다. 그러므로, BookListerBookService와 그것의 구현부에 의존성을 가지고 있다고 말할 수 있다. BookLister 클래스 자신이 BookService의 구현체인 BookServiceImpl을 객체화(instantiates)하였으므로 BookLister 클래스는 자신의 의존관계를 만족시켰다고 말할 수 있다. 하나의 클래스가 자신의 의존관계들을 만족시킨다는 것은 자동적으로 그 의존관계들이 가지고 있는 클래스들과도 의존관계에 있다는 것이다. 위 샘플코드의 경우, BookListerBookServiceImpl뿐 아니라 BookServiceImpl 생성자에 전달되는 파라미터가 존재한다면 그 대상들에게도 의존성을 갖게 된다. BookService 인터페이스는 Spring JDBC나 JPA등을 사용하여 데이터 처리하는 여러 구현체들을 가질 수 있는데 이 코드들을 수정하지 않고서는 다른 구현체를 사용 할 수가 없게된다.

이처럼 높은 결합도를 낮추기 위해 BookService 구현체를 다음과 같이 클래스의 생성자로 전달받도록 옮길 수 있다.

public class BookLister {

    private final BookService bookFinder;

    public BookLister(BookService bookFinder) {
        this.bookFinder = bookFinder;
    }

    public List<Book> findByAuthor(String author) {
        List<Book> books = new ArrayList<>();

        for(Book aBook:bookFinder.findAll()) {
            for(String anAuthor:aBook.getAuthors()) {
                if(anAuthor.equals(author)) {
                    books.add(aBook);
                    break;
                }
            }
        }

        return books;
    }
}

BookService의 의존관계가 BookLister의 생성자의 인자(argument)로 전달되는 것을 확인하자. BookLister는 오직 BookService에게만 의존적이므로 BookLister 생성자를 사용해 객체화하는 모든 클래스는 이 의존관계를 만족하게 되며 의존관계 주입(Dependency Injection) 이라는 용어를 이용, BookService 의존관계는 BookLister 생성자에 의해 주입되었다 라고 말할 수 있다. 이제 BookService를 사용하는 BookLister 클래스의 코드변경 없이도 BookService의 구현체를 수정할 수 있게 되었다.

의존관계 주입방법은 2가지 이다.

  • 생성자를 이용한 주입
  • SET 메서드(Setter)를 이용한 주입

스프링 설정파일은 빈들을 생성/정의 하거나 설정하여 의존관계를 해결한다. 생성자를 이용하여 주입하는 방법은 아래와 같다.

<bean id="bookLister" class="com.packt.di.BookLister">
     <constructor-arg ref="bookService"/>
</bean>

<bean id="bookService" class="com.packt.di.BookServiceImpl" />

위 설정은 아래 코드와 동일하다.

BookService service = new BookServiceImpl();
BookLister bookLister = new BookLister(service);

SET 메서드를 이용한 주입(setter주입)은 프로퍼티를 설정하여 실행하며 bookService를 생성자의 인자로 전달하는 대신 SET메서드의 인자로 전달한다. 스프링 설정은 아래와 같다.

<bean id="bookListerSetterInjection" class="com.packt.di.BookLister">
    <property name="bookService" ref="bookService" />
</bean>

<bean id="bookService" class="com.packt.di.BookServiceImpl" />

위 설정은 아래 코드와 동일하다.

BookService service = new BookServiceImpl();
BookLister bookLister = new BookLister();
bookLister.setBookService(service);

스프링 IoC컨테이너는 어플리케이션 컨텍스트로 알려져있다. 어플리케이션에서 사용되는 객체들 중 어플리케이션 컨텍스트에 정의되고 스프링 IoC컨테이너가 관리하는 객체들을 빈 이라 부른다. 위 예제에서 빈은 bookService다.

빈은 스프링 IoC컨테이너가 관리하는 객체이다. 빈객체는 XML의 <bean/> 태그나 자바 어노테이션처럼 컨테이너에게 제공되는 메타데이터 설정으로 생성된다.

빈 정의는 빈 객체에 대해 기술한다. 빈 정의는 빈의 생성방법, 생명주기 그리고 의존관계등 컨테이너가 필요로 하는 정보를 포함하며 이 정보들을 메타데이터 설정이라 한다.

빈 정의에 사용되는 프로퍼티들은 다음과 같다.

  • class: 강제사항이며 컨테이너가 빈 객체를 생성하기 위해 클래스의 풀네임(fully qualified)으로 기술해야 한다.
  • name: id라고도 하며 유일한 값 이어야 한다.
  • scope: 빈 정의에 의해 생성된 객체의 범위를 제공(prototype, singleton 등)한다. 세부설명은 이후 다시 언급예정.
  • constructor-arg: 생성자의 인자를 통해 의존관계를 주입한다.
  • properties: setter의 인자를 통해 의존관계를 주입한다.
  • lazy-init: 이 값이 true이면 IoC 컨테이너는 시작 시점이 아닌 객체가 처음 요청되는 시점에 해당 객체를 생성하게 된다. 객체가 스프링 컨텍스트내에 생성될 때 까지는 어떠한 설정에러도 발견되지 않는다는 뜻이다.
  • init-method: IoC컨테이너가 모든 필요한 프로퍼티들을 빈에 설정한 직후 호출하는 빈의 메서드 이름을 지정한다.
  • destroy-method: 빈이 소멸될 때 컨테이너가 이 메서드를 호출한다.빈이 소멸되기 전 정리해야 할 작업들이 있다면 유용하다.

빈의 범위(scope)들은 아래와 같다.

  • singleton: IoC컨테이너 당 하나의 빈 객체를 의미하며 디자인 패턴의 싱글톤(클래스로더당 하나의 객체)과는 조금 다르다.
  • prototype: 여러 객체를 생성하기 위한 단일 빈정의. 객체 요청 시마다 새로운 객체가 생성된다.
  • request: 하나의 HTTP 요청에 해당하는 빈 객체. 웹기반(web-aware)의 어플리케이션 컨텐스트내에서만 유효하다.
  • session: 하나의 HTTP 세션에 해당하는 빈 객체. 웹기반(web-aware)의 어플리케이션 컨텐스트내에서만 유효하다.
  • global-session: 전역 HTTP 세션에 해당하는 빈객체. 웹기반(web-aware)의 어플리케이션 컨텐스트내에서만 유효하다.

다음은 빈의 생명주기에 대한 단계별 설명이다.

  1. 첫번째 단계는 빈을 찾아 객체화 하는 것이다. 스프링 IoC컨테이너는 XML로 부터 빈 정의를 읽어 객체화 한다.
  2. 다음 단계는 빈 프로퍼티들을 설정하고 의존관계들을 만족시키는 것이다. IoC컨테이너는 의존관계 주입을 통해 프로퍼티들을 설정한다.
  3. 의존관계들을 설정하면 각 빈들의 SET메서드(setBeanName)가 호출된다. 만약 빈들이 BeanNameAware 인터페이스를 구현했다면 setBeanName()메서드는 빈의 ID를 전달하여 호출된다.
  4. 만약 빈이 BeanFactoryAware 인터페이스를 구현했다면 setBeanFactory()메서드가 호출된다.
  5. 빈과 연관된 하나 이상의 BeanPostProcessor 가 존재하면 각각의 processBeforeInititialization()을 호출한다.
  6. XML 빈 정의에 init-method 프로퍼티가 지정되어 있다면 해당 메서드를 호출한다.
  7. 마지막으로, 빈과 연관된 하나 이상의 BeanPostProcessor 가 존재하면 각각의 postProcessAfterInititialization()을 호출한다.

POJO는 스프링의 어떤 기능과도 의존관계가 없다는 것을 기억하자. 특수한 경우에 한해 스프링이 인터페이스 형태로 연결고리를 제공할 뿐이다. 그것들을 사용한다는 것은 스프링에서 의존관계를 시작한다는 뜻이다. 다음 그림은 빈의 생명주기를 도식화 한 것이다.

DI와 IoC에 대해 더 알고 싶다면 아래 마틴 파울러의 블로그를 방문해보자. http://martinfowler.com/articles/injection.html

Printing Hello World

이번 장에서는 Hello World 예제를 만들어 보고 스프링을 사용하기 위한 이클립스 환경설정에 대해 알아본다. 이클립스 최신버전은 http://www.eclipse. org/downloads/ 에서 다운로드 할 수 있다.

참고로, 스프링에서는 STS(Spring Tool Suite)라는 스프링 전용 이클립스 배포판을 제공한다. STS는 스프링 어플리케이션 개발에 최적화 되어있다. STS는 http://spring.io/tools/sts 에서 다운로드 할 수 있다.

메이븐 레파지토리인 http://search. maven.org/ 또는 http://mvnrepository.com/artifact/org.springframework 에서 Spring 4.1.0 JAR를 다운로드 한다.

  1. 이클립스를 실행하여 SpringOverview 라는 이름의 자바 프로젝트를 생성한다.
  2. 의존관계가 있는다음 라이브러리들을 추가한다.
  3. src 디렉토리 하위에 com.packt.lifecycle 패키지를 생성한다.
  4. HelloWorld 클래스를 생성한다.

    public class HelloWorld {
     private String message;
    
     public String getMessage() {
         return message;
     }
    
     public void setMessage(String message) {
        this.message = message;
     }
    }
    
  5. applicationContext.xml 이름의 XML 파일을 src 디렉토리 바로 하위에 추가한다. 빈정의는 아래와 같다.

    <?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="helloWorld" class="com.packt.lifecycle.HelloWorld">
           <property name="message" value="Welcome to the Spring world">
           </property>
          </bean>
        </beans>
    
  6. HelloWorldExample 이름의 자바 클래스를 생성한 후 빈 설정을 체크하는 로직을 추가한다.

    public class HelloWorldExample {
     public static void main(String[] args) {
         ApplicationContext context = new
         ClassPathXmlApplicationContext(
             "applicationContext.xml");
    
         HelloWorld world = (HelloWorld)
             context.getBean("helloWorld");
    
         System.out.println(world.getMessage());
     }
    }
    

    HelloWorldExample의 main()은 XML에 설정된 스프링 빈을 로딩한다. 현재 클래스패스 상에 존재하는 applicationContext.xml 파일을 찾아 컨텍스트에게 helloWorld라는 이름 또는 ID의 빈을 요청한 후 해당 빈의 getMessage()를 호출하여 applicationContext.xml에 설정한 프로퍼티가 정상출력되는지 확인한다.

  7. HelloWorldExample 프로그램을 실행하면 다음 결과가 출력된다.

라이프 사이클 직접 확인하기

우리는 빈의 라이프 사이클에 대해서 읽었는데, 이제 라이프 사이클을 직접 확인해 보자.

HelloWord 클래스에서 Spring Framework의 다음 인터페이스들을 구현하자.

  • ApplicationContextAware : setApplicationContext 메서드를 구현해야 한다.
  • BeanNameAware : setBeanName 메서드를 구현해야 한다.
  • InitializingBean : afterPropertiesSet 메서드를 구현해야 한다.
  • BeanFactoryAware : setBeanFactory 메서드를 구현해야 한다.
  • BeanPostProcessor : postPorcessBeforeInitialization과 postProcessAfterInitialization 메서드를 구현해야 한다.
  • DisposableBean : destroy 메서드를 구현해야 한다.

모든 메서드에 System.out.println을 추가하자. 일단 다음 두 메서드를 추가하자.

public void myInit() {
       System.out.println("custom myInit is called ");
}
     public void myDestroy() {
       System.out.println("custom myDestroy is called ");
}

빈 정의에 init-method와 destroy-method를 다음과 같이 수정한다.

<bean id="helloWorld" class="com.packt.lifecycle.HelloWorld"
       init-method="myInit" destroy-method="myDestroy">
       <property name="message" value="Welcome to the Spring world">
       </property>
</bean>

HelloWorldExample을 다음처럼 수정하면 셧다운 후크가 등록되서 어플리케이션 컨텍스트가 destory 되도록 할 수 있다.

AbstractApplicationContext context = new  ClassPathXmlApplicationConte
   xt("applicationContext.xml");
     HelloWorld world = (HelloWorld) context.getBean("helloWorld");
     System.out.println(world.getMessage());
     context.registerShutdownHook();

어플리케이션을 실행하면 다음 내용이 출력된다.

setBeanName is called with helloWorld
setBeanFactory is called
setApplicationContext is called
afterPropertiesSet is called
custom myInit is called
Welcome to the Spring world
destroy is called
custom myDestroy is called

setBeanName, setBeanFactory, setApplicationContext, afterPropertiesSet 순서대로 호출되고 커스텀 init 메서드가 호출되는 것을 보여준다. 프로그램 종료 처리중에는 destory 메서드가 처음 호출되고 다음에 커스텀 destory-method가 호출된다.

자동연결(autowring)과 특수주석(annotation) 사용하기

스프링 컨테이너는 엘레먼트를 사용하지 않고 협업하는 빈들 간에 의존성을 자동연결하는 기능을 가지고 있다.

다음과 같은 자동연결 모드를 통해서 스프링 컨테이너가 의존성 주입을 자동적으로 처리하는 방법을 결정할 수 있다.

  • no : 기본값, 자동연결을 하지 않는다.
  • byName : 속성의 이름과 동일한 이름으로 정의된 빈을 연결한다.
  • byType : 속성의 타입과 동일한 타입으로 정의된 빈을 연결한다. 동일한 타입으로 여러개의 빈이 정의되어 있는 경우에는 예외가 발생한다.
  • constructor : 이것은 byType과 비슷하지만 생성자 타입에서 찾는다는 차이가 있다. 여러개의 빈이 정의되어 있는 경우에는 예외가 발생한다.
  • default : constructor로 자동연결을 시도하는데 처리가 되지 않으면 byType으로 자동연결을 한다.

HelloWorld를 다음과 같이 수정하면, byName으로 자동연결된다.

<bean name="message" class="java.lang.String">
    <constructor-arg value="auto wired" />
</bean>

<bean id="helloWorld" class="com.packt.lifecycle.HelloWorld"
   autowire="byName">
</bean>

실행하면 auto wired가 출력될 것이다.

스프링은 특수주석을 통한 설정도 제공하고 있다.

  • @Required : 빈 속성 설정 메서드(setter)에 적용된다.
  • @Autowired : 빈 속성 설정 메서드(setter), 생성자, 속성들에 적용된다.
  • @Qualifier : @Autowired와 함께 쓰인다. qualifier 이름을 지정할 수 있다.

어노테이션을 통해서 자동연결을 하기 위해서는 어플리케이션 컨텍스트에 다음 내용이 추가되어야 한다.

<context:annotation-config/>

특수주석을 사용할 있게 수정한 어플리케이션 컨텍스트 설정은 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
   <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-
   3.0.xsd">
   <context:annotation-config/>
      <bean name="message" id="message" class="java.lang.String">
          <constructor-arg value="auto wired" />
       </bean>
       <bean id="helloWorld" class="com.packt.lifecycle.HelloWorld">
       </bean>
</beans>

HelloWorld 클래스에 특수주석을 적용해 보자. 여기서는 설정 메서드(setMessage)에 특수주석을 붙였지만, message 속성에 @Autowired를 붙여도 된다.

public class HelloWorld implements ApplicationContextAware,BeanNameAware, 
       InitializingBean,BeanFactoryAware,BeanPostProcessor,  DisposableBean {
     private String message;
     public String getMessage() {
       return message;
}
     @Autowired
     public void setMessage(String message) {
       this.message = message;
     }
        //code omitted for brevity
   }

프로그램을 실행하면 auto wired 메시지가 출력된다.


--------------------- 편집점

스프링 MVC 애플리케이션 빌드하기

모델 뷰 컨트롤러(MVC) 는 널리 사용되는 웹 개발 패턴이다. MVC패턴은 모델, 뷰, 컨트롤러로 3개의 연결된 컴포넌트를 정의한다.

모델은 애플리케이션 데이터, 로직 혹은 비지니스 룰을 나타낸다.

뷰는 정보나 모델의 표현이다. 하나의 모델은 여러개의 뷰를 가질 수 있다. 예를 들면 학생의 성적들은 표형식이나 시각적인 차트로 나타낼 수 있다.

컨트롤러는 애플리케이션의 흐름을 제어한다. JEE 애플리케이션안에서 컨트롤러는 보통 서블릿으로 구현된다. 하나의 컨트롤러 서블릿은 요청들을 가로막은 다음 각 요청을 적절하게 처리하는 자원(handler resource)에 맵핑한다. 이 절에서 우리는 요청을 뷰에게 다시 전달하는 전형적인 MVC 프론트 컨트롤러 서브릿을 빌드할 것이다.

스프링 MVC는 스프링 디자인 원리들의 장점을 가지는 웹 애플리케이션 프레임워크다.

  • 의존성 주입
  • 인터페이스 주도 디자인
  • 프레임워크에 묶이지 않는 POJO

스프링 MVC는 다음과 같은 장점이 사용된다.

  • 의존성 주입을 통한 테스팅
  • 도메인 객체에 요청데이터를 바인딩
  • 폼 유효성
  • 에러 핸들링
  • 다양한 뷰 기술들
  • 다양한 포맷 지원 (JSP, 벨로시티, 엑셀, PDF)
  • 페이지 작업흐름

스프링 MVC 안에서 다음과 같은 간단한 요청 처리 매카니즘이 있다.

  1. DispatcherSevlet은 요청을 수신하고, 그 요청을 처리할 수 있는 컨트롤러를 알아내기 위해서 핸들러 맵핑(handler mapping)과 협의한다. 그리고 요청을 처리할 수 컨트롤러에게 요청을 전달한다.
  2. 컨트롤러는 비즈니스 로직(서비스나 비지니스 처리기에 요청을 위임할 수있는)을 수행한다. 그리고 사용자 디스플레이나 응답을 위해서 DistpatcherServlet에 정보를 다시 돌려준다. 사용자에게 직접 정보를(모델)을 보내주는 대신에 컨트롤러는 모델을 보여줄 수 있는 뷰 이름을 돌려준다.
  3. DispatcherServlet 뷰 이름으로 부터 물리적으로 보여지는 것을 해결한다. 그리고 뷰에게 모델 객체를 전달한다. 이 방식의 DispatcherServlet은 뷰 구현으로 부터 분리되게 된다.
  4. 뷰는 모델을 보여준다. 뷰는 JSP 페이지, 서블릿, PDF 파일, 엑셀 리포트이거나 어떠한 보여질 수는 컴포넌트가 될 수 있다.

다음 시퀀스(순서)다이어그램은 스프링 MVC의 흐름과 상호작용을 보여준다.

우리는 다음과 같은 순서로 스프링 웹 애플리케이션과 JUnit을 사용하는 단위 테스트 코드를 빌드할 것이다.

  1. 이클립스를 실행하고 SpringMVCTest라는 dynamic web project를 생성한다.
  2. web.xml 파일을 열고 아래의 라인은 추가한다.

     <display-name>SpringMVCTest</display-name>
            <servlet>
                <servlet-name>dispatcher</servlet-name>
                <servlet-class>
                        org.springframework.web.servlet.DispatcherServlet
                    </servlet-class>
                <load-on-startup>1</load-on-startup>
              </servlet>
              <servlet-mapping>
                <servlet-name>dispatcher</servlet-name>
                <url-pattern>/</url-pattern>
              </servlet-mapping>
              <context-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>
                  /WEB-INF/dispatcher-servlet.xml
                </param-value>
       </context-param>
     </web-app>
    

    위의 서블릿 이름 dispatcher 가 DispatcherServlet이며 모든 요청을 맵핑한다. contextConfigLocation 파라미터를 주목하자. 이것은 스프링 빈들이 /WEB-INF/dispatcher-servlet.xml 파일 안에 정의된다는 것을 가르킨다.

  3. dispatcher-servlet.xml 파일을 WEB-INF 안에 생성하고 아래의 라인을 추가한다.

     <?xml version="1.0" encoding="UTF-8"?>
     <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:context="http://www.springframework.org/schema/context"
         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-
            3.0.xsd
               http://www.springframework.org/schema/context
                    http://www.springframework.org/schema/context/spring-
            context-3.0.xsd">
         <context:component-scan base-package="com.packt" />
         <bean
             class="org.springframework.web.servlet.view.
                                  InternalResourceViewResolver">
             <property name="prefix">
                 <value>/WEB-INF/pages/</value>
             </property>
             <property name="suffix">
                 <value>.jsp</value>
             </property>
         </bean>
     </beans>
    

    이 XML은 서블릿 뷰 리졸버(view resolver)를 정의한다. 모든 뷰는 /WEB-INF/pages 위치 아래에 .jsp로 끝나는 이름으로 찾아낼 수 있다. 그리고 모든 빈들은 스프링 애노테이션으로 com.packt 패키지 아래에 구성된다.

  4. LoginInfo 클래스 파일을 com.packt.model 패키지안에 생성한다. 이 클래스는 로그인 정보를 보여준다. 두개의 private 문자열 필드 userId 와 password 를 추가하고, getter 와 setter를생성한다.

  5. login.jsp 파일을 /WEB-INF/pages 아래에 생성하고 스프링 태그 라이브러리를 사용하는 폼을 생성하는 아래의 라인을 추가한다. 폼을 수정하고 username 과 password 위한 기본적인 HTML input을 추가해라.

     <%@ taglib prefix="sf" uri="http://www.springframework.org/tags/
            form"%>
            <sf:form method="POST" modelAttribute="loginInfo" action="/
            onLogin">
      </sf:form>
    
  6. 로그인 요청을 처리할 수 있는 com.packt.controller.LoginController 컨트롤러 클래스 파일을 생성한다. 아래의 라인을 추가한다.

     @Controller
     @Scope("session")
     public class LoginController implements Serializable {
    
         @RequestMapping({ "/", "/login" })
         public String onStartUp(ModelMap model) {
             model.addAttribute("loginInfo", new LoginInfo());
             return "login";
         }
     }
    

    @Controller 애노테이션은 이 클래스가 스프링 MVC 컨트롤러 클래스라는 것을 나타낸다. sample-servlet.xml 파일 안에서 우리는 <context:component-scan base-package="com.packt" /> 을 정의했다. 그래서 스프링은 @Controller 애노테이션을 스캔하고 빈을 생성할 수 있다. @RequestMapping 애노테이션은 기본적인 패스 /SpringMvcTest/ 혹은 /SpringMvcTest/login 으로 요청된 모든 요청에 대해서 onStartUp 메소드를 맵핑해준다. XML 파일에 정의된 뷰 리졸버는 로그인 요청을 /WEB-INF/pages/login.jsp 페이지에 맵핑한다.

  7. Login 클래스에 로그인 제출 요청를 처리할 다른 메소드를 생성한다.

     @RequestMapping({ "/onLogin" })
     public String onLogin(@ModelAttribute("loginInfo") LoginInfo loginInfo,
             ModelMap model) {
         if (!"junit".equals(loginInfo.getUserId())) {
             model.addAttribute("error", "invalid login name");
             return "login";
         }
         if (!"password".equals(loginInfo.getPassword())) {
             model.addAttribute("error", "invalid password");
             return "login";
         }
         model.addAttribute("name", "junit reader!");
         return "greetings";
     }
    

    이 메소드는 /onLogin 요청에 맵핑된다. @ModelAttribute("loginInfo") 는 login.jsp 폼으로 부터 제출된 모델이다. 이 메소드는 사용자 이름 junit 과 패스워드 password 인지 아닌지를 체크한다. 만약 사용자 아이디와 패스워드가 맞지 않으면 에러메시지를 login 페이지에 보여준다. 입력한 정보가 일치하면 greetings 뷰가 열린다.

  8. /SpringMvcTest/onLogin으로 폼을 제출하기 위해서 login.jsp 파일을 수정하자. modelattribute의 이름은 loginInfo 이다.

     <sf:form method="POST" modelAttribute="loginInfo" action="/
            SpringMvcTest/onLogin">
    
  9. greetings.jsp 파일을 만들고 아래 라인을 추가한다.

     <h1>Hello :${name}</h1>
    
  10. 브라우저 주소 창에 http://localhost:8080/SpringMvcTest/ 를 입력한다. login 페이지가 열릴것이다. login 페이지 안에서 어떠한 값 입력없이 제출하면 Invalid login name 에러메세지가 보이게 될 것이다. 지금 user 안에 junit을 입력하고 패스워드 안에 pasword를 입력하고 엔터키를 치면 애플리케이션은 아래의 메세지와 함께 너에게 인사할 것이다.

    출처(Resources) : 스프링 프레임워크 레퍼런스 문서(Spring Framework Reference Documentation)

요약

이 장에서는 스프링 기초를 다루었다. 스프링 프로젝트 특히 스프링 프레임워크에 대해서 논의했다. 스프링 컨테이너, 스프링 빈 라이프 사이, 의존성 주입, AOP, 스프링 MVC 그리고 스프링 트랜잭션 관리에 대해서 살펴보았다.

다음 장에서는 JUnit4와 Mocking 프레임워크를 빨리 시작하길 원하는 독자에게 초첨을 맞출 것이다. JUnit 테스팅의 개요와 Mockito API를 알아보자.

2. JUnit 과 Mockito 와 작업하기

이 장에서는 단위테스팅, JUnit4 프레임워크, 이클립스 설치, 테스트 더블, Mockito를 사용한 mocking 의 개념을 다룬다.

이 장에서 다루는 주제는 아래와 같다.

  • JUnit4 애노테이션
  • Assertion 메소드 와 assertThat
  • @RunWith 애노테이션
  • JUnit 예외처리
  • JUnit 테스트 묶음(suite)
  • Mockito 와 Mockito API 개요
  • 고급 Mockito 예제들

단위테스트 배우기

테스트는 무언가의 성능을 측정하거나 혹은 데이터의 검토를 말한다. 예를 들면 클래스 테스트는 다음 단계로 갈 수 있는지 없는지를 결정하기 위해서 우리가 이해한 것을 평가한다. 우리는 우리의 고객에게 소프트웨어를 전달한다. 그래서 테스트는 소프트웨어가 고객에게 전달되기 전에 소프트웨어 문맥안에서 요구사항을 검증하는 것을 말한다. 예를 들면 유효한 사용자가 시스템에 로그인 할 수 있는지 혹은 동시에 천 명의 사용자가 시스템에 접속할 수 있는지를 확인할 필요가 있다.

단위테스트는 계산된 결과가 아마도 실수할 수 있는지 혹은 아닌지를 빠르게 평가하기 위한 기본적인 테스트를 말한다. 즉, 계산 결과의 근거를 검증하는 간단한 확인 작업이다.

일반적으로 자바코드는 출력문(print statements)을 사용하거나 애플리케이션을 디버깅하는 것으로 단위테스트된다. 이런 접근법은 올바르지 않을뿐더러 테스팅로직과 생산코드(production code)가 결합된 코드를 작성하는 것은 좋은 습관이 아니다. 비록 생산코드가 잘 작동하더라도 코드의 복잡성을 증가시키거나 코드를 읽기 어렵게 만든다. 또한 심감한 유지보수 상에 문제를 만들어 내거나 만약 코드 상 구성이 잘못되어 생산코드가 오작동을 일으킬 수도 있다. 단위테스팅을 위해서 생산코드 안에서 출력문과 과도한 로깅문(logging statements)을 추가할 때 출력문과 로깅문은 생산코드와 함께 실행이되고 또한 불필요한 정보가 출력된다. 결국 로깅은 진정한 문제를 야기시킬 수 있다. 예를 들면 우리는 과도한 로깅때문에 심각하게 발생한 스레드 메세지를 알리는데 실패할 지도 모른다.

단위테스팅은 테스트주도개발(Test-Driven Development 이하 TDD)의 기준이 된다. TDD에서는 실패할 테스트를 먼저 작성한다. 그러고 나서 테스트에 만족할 수 있는 코드를 작성한다. 그런 다음 코드를 리팩토링하고 패턴을 적용시켜서 코드의 품질을 향상시킨다. 그래서 단위테스트는 설계를 주도한다. 실패하는 테스트를 만족하기 위해서 코드가 작성되기 때문에 단위테스트는 복잡하게 설계하는 것을 줄여준다. 자동화된 테스트들은 리팩토링과 새로운 기능을 위해서 빠른 회귀 안정망을 제공한다.

켄트백은 익스트림 프로그래밍(Extreme Programming 이하 XP) 개념과 TDD를 고안했다. 그는 많은 책과 문서를 저술했다.

일반적으로 생산코드와 테스트코드를 혼합해서 작성하지 않는다. 단위테스트들은 같은 프로젝트에 유지시킨다. 하지만 다른 디렉토리 혹은 소스폴더 아래의 이런 org.packt.Bat.java 자바클래스파일을 위한 단위테스트들은 org.packt.BatTest.java 테스트클래스파일 안에 작성되어야 한다. 파일이름의 명명규칙(convention)은 테스트클래스이름의 마지막에 Test를 붙인다. Bar클래스와 BarTest클래스 파일이 같은 패키지(org.packt)에 있다고 한다면 각각의 소스파일들은 src(/org/for/Bar.java), test(/org/foo/BarTest.java) 소스 폴더에 구성해야 된다. 소스 코드와 단위테스트코드를 같은 패키지 내에서 유지하는 것은 소스코드의 protected와 default의 메소드와 멤버에 접근하는 것을 허용한다. 이 접근법은 레거시코드와 작업하는 동안 유용하다.

일반적으로 고객들은 단위테스트를 실행하지 못하기 때문에 단위테스트을 필요로 하지 않는다. 그래서 소프트웨어를 패키징하는 동안에 테스트폴더는 생산코드에 포함하지 않는다.

코드주도 단위테스팅 프레임워크는 자바코드 단위테스트에 익숙하다. 다음은 자바단위테스팅 프레임워크들이다.

  • SpryTest
  • Jtest
  • JUnit
  • TestNG

가장 인기있고 널리 사용되는 프레임워크는 JUnit 프레임워크이다. Junit4는 다음 장에서 살펴볼 것이다.

JUnit 프레임워크와 작업하기

JUnit은 자바를 위한 가장 인기있는 단위테스팅 프레임워크다. JUnit은 자바커뮤니티를 위해서 메타데이터기반이며 비침투적이고 멋진 단위테스팅 프레임워크를 제공한다. TestNG는 JUnit 보다 깔끔한 문법과 사용법을 가지고 있다. 하지만 JUnit은 TestNG보다 더 유명하다. JUnit은 개발자가 만들어 사용할 수 있는 JUnit4 runner를 제공하는 Mockito와 같이 목킹을 더 잘 지원해준다.

버전 4.12는 아래의 사이트에서 다운로드할 수 있는 가장 최신의 버전이다.
https://github.com/junit-team/junit/wiki/Download-and-Install

JUnit4는 메타데이터 기반(애노테이션), 비침투적인(JUnit 테스트는 프레임워크 클래스로 부터 상속을 받을 필요가 없다.) 프레임워크이다. JUnit프레임워크는 개별적이고 기능적인 흐름및 요구사항 혹은 코드의 단위들을 검증하기 위한 테스트케이스들을 작성하기 위해서 API를 제공한다. JUnit은 침투적인 프레임워크에서 비침투적인 프레임워크까지 발전했다. 그래서 우리는 JUnit의 이점을 이해하기 위해서 이전 버전의 Junit프레임워크를 살펴봐야 된다. 다음 장은 JUnit4프레임워크와 이전버전의 프레임워크와 비교한다. JUnit3는 많은 단점을 가지고 있다. JUnit3에서 개발자가 만든 JUnit테스트는 TestCase클래스를 상속(extend)하고 몇개의 메소드들을 오버라이드하는 것을 강제하였다. 테스트메소드의 이름은 test로 이름을 시작해야 했다. 아래는 JUnit4의 장점이다.

  • 테스트케이스는 더이상 junit.framework.Testcase를 상속하지 않아도 된다. 모든 POJO클래스는 테스트클래스가 될 수 있다.
  • JUnit3에서 테스트데이터를 준비하거나 지우기 위해서 setUp과 tearDown메소드를 사용했었다. setup고 tearDown메소드를 명시적으로 오버라이드해야 했다. 하지만 JUnit4에서는 모든 테스트메소드 바로 이전 이후에 실행할 수 있게 모든 메소드에 @before 혹은 @after애노테이션 주석을 달기만 하면 된다.
  • JUnit3에서는 테스트메소드 이름을 test로 시작해야 했다. 하지만 JUnit4에서는 테스트메소드로서 실행하기 위해서는 public메소드에 @Test애노테이션 주석을 달기만 하면 된다.

    자바 통합개발환경(IDEs)은 단계별 디버깅, 문법 하이라이팅, 자동완성기능, 리팩토링과 같이 좀 더 쉽게 코드를 작성하거나 디버깅 할 수 있는 기능들을 제공한다. 인기있는 자바IDE는 Eclipse, NetBeans, JCreator, BlueJ, JBuilder, MyEclipse, IntelliJ IDEA, JDeveloper 등이 있다.

이 책에서 우르는 자바 코딩과 JUnit테스팅을 하기 위해서 이크립스(Eclipse)를 사용할 것이다. 이클립스는 아래의 사이트에서 다운로드 받을 수 있다.
http://www.eclipse.org/downloads

가장 최근 이클립스IDE 버전은 Mars이다(v4.5) 책이 쓰여진 시점에는 Luna(v4.4) 기준이였음

이클립스 매년마다 프로젝트가 릴리즈된다. Callisto(C로 시작된) 이름으로 프로젝트가 시작했다. 집필상 이클립스 프로젝트 이름들은 C,E,G,H,I,J,K,L,M으로 진행된다. 2006년 이후로 이클립스는 다음과 같이 출시되었다. Europa(E), Ganymede(G), Galileo(G), Helios(H), Indigo(I), Juno(J), Kepler(K), Luna(L) Mars(M)

이클립스 설정하기

만약 이클립스와 자바프로젝트의 클래스패스(classpath)의 환경설정 구성하는 방법을 이미 알고 있고 있다면 이 장을 건너뛰어도 된다. 아래는 이클립스를 설정하는 단계이다.

  1. 이클립스 다운로드사이트http:/www.eclipse.org/downloads/를 간다. 바이너리(binary) 파일을 다운로드 하기 위해서 사용자의 운영체제(Windows, Mar, Linux)를 맞게 선택하고 하드웨어 아키텍처에 맞게 32bit 혹은 64bit를 클릭한다. 아래의 화면사진은 이클립스 Mars를 보여준다. 가장 최근의 이클립스 버전은 Mars다. 스프링(Spring)사용자는 Eclipse IDE for Java EE Developers를 설치하는 것이 좋다. Eclipse IDE for Java EE Developers는 스프링 지원과 마지막 장에서 사용될 웹 개발을 포함하고 있다.

  2. 이클립스 바이너리파일을 압축을 풀고 이클립스를 실행하기 위해서 eclipse.exe(윈도우 기준) 파일을 클릭하거나 ./Eclipse 쉘 스크립트(리눅스 혹은 맥기준)을 실행한다.

  3. 이클립스는 프로젝트 파일들을 관리하기위해서 워크스페이스(workspace)가 필요하다. 새로운 워크스페이스를 만들기 위해서 워크스페이스 이름을 입력한다. 예를 들면 윈도우 기준 C:\myworkspace\junit, 리눅스나 맥 기준 $HOME /workspace/junit. 만약 디렉토리나 폴더가 존재하지 않는다면 이클립스는 워크스페이스 이름대로 디렉토리 계층을 생성하고 새로운 워크스페이스를 오픈한다.

  4. Ctrl + N 누르거나 혹은 File에서 New 메뉴옵션 클릭한다. 새로운 마법사(wizard) 창이 열릴 것이다. 마법사창에서 Java Project를 선택하고 Next버튼을 클릭한다. JUnitTests 라고 자바프로젝트 이름을 입력하고 Finish버튼을 클릭한다. 이클립스는 JUnitTest프로젝트를 생성한다.

  5. 이 장에서는 우리는 JUnit테스트를 작성하고 JUnit테스트하기 위해서 JUnit프레임워크 JAR파일이 필요하다. JUnit JAR파일을 다운로드하기 위해서 다음 사이트로 이동한다.https://github.com/ junit-team/junit/wiki/Download-and-Install 사이트에서 junit.jar파일과 hamcreat-core.jar 파일을 다운로드 받는다. 다운로드한 JAR파일을 JUnitTests 프로젝트 디렉토리에 복사한다.

  6. 다운로드 한 JUnit JAR파일을 프로젝트 라이브러리 혹은 클래스패스에 추가하는 두 가지 방법이 있다. 다운로드된 JAR파일을 우클릭한 후 Build Path메뉴를 선택한다. 그리고 나서 Add to build path 메뉴항목을 클릭한다. 또는 프로젝트를 우클릭한다. 팝업메뉴가 나타나면 Properties메뉴항목을 선택한다. 좌측편 상에 Java build path를 클릭하고 Libraries 탭을 연다. Libraries탭에서 Add JARs...버튼을 클릭한다. 프로젝트창이 팝업창으로 열리게 된다. 메뉴에서 JUnitTests프로젝트을 확장하고 두 개의 JAR파일(junit.jar와 hamcrest-core.jar)을 선택하고 Libraries에 추가한다. 현재 JUnitTests프로젝트는 JUnit테스팅위한 준비가 되었다.

우리는 JUnit4가 비침투적이고 애노테이션기반 프레임워크이며 어떤 프레임워크 클래스를 확장하도록 요청하지 않는다는 걸 알았다. 다음 장에서는 JUnit4 애노테이션, 주장(assertions), 예외처리에 대해서 살펴볼 것이다.

우리는 첫번째 테스트를 작성하기 전에 애노테이션을 살펴볼 것이다.

애노테이션 살펴보기

@Test애노테이션은 테스트를 나타낸다. 모든 public메소드에 JUnit테스트메소드를 만들기 위해서 @Test애노테이션을 붙있 수 있다. test로 시작하는 테스트메소드는 더이상 필요하지 않는다.

때로는 어떤 메소드가 학생의 리스트를 받고 메소드 내부적으로 학생이 받은 종합점수를 근거로 학생리스트를 정렬한 결과를 출력하는 코드로직있다고 하자. 이 메소드를 검증하기위해서는 사전에 검증할 데이터를 구축할 필요가 있다. 정렬로직을 단위테스트하기 위해서 학생리스트를 미리 구축하고 그 학생들의 개별적 종합점수를 셋팅할 필요가 있다. 점수와 함께 학생리스트를 구축하는 이 활동을 데이터셋업(data setup)이라고 한다. JUnit3 API는 데이터셋업을 위해서 TestCase클래스에 setUp()메소드를 제공한다. 이 테스트클래스는 setUp()메소드를 오버라이드할 수 있고 데이터가 거주하는 로직을 작서할 수도 있다. 아래의 setUp()메소드의 표시다.

  protected void setUp() throws Exception

JUnit4에서는 데이터설정을 위한 어떠한 메소드를 정의하지 않아도 된다. 더 정확히 말하면 JUnit4는 @Before애노테이션을 제공한다. @Before애노테이션이 있는 public메소드가 있을때 그 메소드는 테스트가 실행되기 전에 실행이 된다.

비슷하게 @After애노테이션이 있는 public메소드는 테스트메소드가 실행되고 난 다음에 실행이 된다. JUnit3는 이 목적으로 tearDown()메소드를 정의했었다.

JUnit4는 두 개의 메소드레벨의 애노테이션을 정의한다. 바로 public정적메소드를위한 @BeforeClass와 @AfterClass이다. 정적일때, 테스트클래스마다 오직 한번씩만 실행이 된다. @BeforeClass가 있는 public정적메소드는 처음 테스트 전에 실행이 되고 @AfterClass애노테이션이 있는 public정적메소드는 마지막테스트 다음에 실행이 된다.

다음 예제는 JUnit4의 애노테이션들과 애노테이션이 붙은 메소드들의 실행순서를 설명한다.

  1. 이클립스를 실행하고 JUnitTests프로젝트를 연다. test소스폴더를 생성하고 com.packtpub.junit.recap패키지 아래에 SanityTest.java이름의 자바파일을 생성한다. 아래 그림은 위의 내용을 설명한 것이다.
    일반적으로 테스트클래스의 이름의 코딩규약은 테스트클래임 끝에 Test를 붙여준다. 예를들어 SomeClass의 테스트클래스는 SomeClassTest가 될 것이다. 특정 코드를 보장 툴들은 테스트클래스의 이름끝에 Test가 없으면 테스트를 무시한다.

  2. 테스트클래스를 작성했다. SanityTest클래스에 다음 코드를 추가하자

    package com.packtpub.junit.recap;
    
    import org.junit.After;
    import org.junit.AfterClass;
    import org.junit.Before;
    import org.junit.BeforeClass;
    import org.junit.Test;
    
    public class SanityTest {
    
       @BeforeClass
       public static void beforeClass() {
           System.out.println("***Before Class is invoked");
       }
    
       @Before
       public void before() {
           System.out.println("____________________");
           System.out.println("\t Before is invoked");
       }
    
       @After
       public void after() {
           System.out.println("\t\t After is invoked");
           System.out.println("===================");
       }
    
       @Test
       public void someTest() {
           System.out.println("\t\t someTest is invoked");
       }
    
       @Test
       public void someTest2() {
           System.out.println("\t\t someTest2 is invoked");
       }
    
       @AfterClass
       public static void afterClass() {
           System.out.println("***After Class is invoked");
       }
    }
    

    SanityTest클래스는 여섯개의 메소드를 정의하고 있다. 두 개의 메소는 @Test애노테이션이 있고 두개의 public정적메소드에는 @BeforeClass와 @AfterClass애노테이션이 있다. 그리고 나머지 2개의 정적메소드가 아닌 메소드에는 @Before와 @After애노테이션이 있다.

    @BeforeClass가 있는 정적메소드는 SanityTest클래스가 인스턴스가 만들어질 때(다른 말로 처음 테스트메소드가 실행되기 전에) 오직 한번 실행된다. 그리고 @AfterClass가 있는 정적메소드는 두 개의 테스트메소드의 실행이 모두 끝난 후에 실행된다.

  3. 메소드 실행 순서를 이해하기 위해서 테스트를 실행해보자. 테스트를 실행하기 위해서 Alt+Shift+X+T를 누르거나 Run | Run As | JUnitTest 순으로 메뉴에서 찾아서 실행한다. 테스트가 실행되는 동안 아래의 콘솔(System.out.println) 출력이 화면에 보일 것이다.

  4. 각각의 테스트메소드 실행 전후에 before와 after메소드가 실행되었는지 보장한다. 그러나 테스트 메소드의 실행순서는 실행환경에 따라서 다르다. 그래서 someTest는 someTest2전에 혹은 반대로 실행될 수도 있다. afterClass와 beforeClass메소드는 오직 한번 실행된다.

축하한다!. 당신은 당신의 첫 JUnit4테스트를 실행했고 애노테이션에 대해서 배웠다.

@Before와 @After애노테이션은 어떠한 public void 메소드에 적용시킬 수 있다. @AfterClass와 @BeforeClass애노테이션은 오직 public 정적 void 메소드에서만 적용시킬 수 있다.

단언(assertion)으로 예상값 검증하기

단언은 코드실행의 실제 결과와 프로그래밍 상의 추정 혹은 추측한 값과 검증하는걸 말한다. 예를 들어 양수들 중에 덧셈을 할 때 그 결과가 양수라는 걸 예상할 수 있다. 그래서 예상되는 값과 실제 결과값을 단언하고 숫자들을 더하는 add 메소드를 작성할 수 있다. 예를 들면 1, 2, 3을 add 메소드에 통과시키면 결과가 6이 될 것이라는 걸 예상할 수 있다. 그래서 프로그램의 실제 결과를 6으로 단언할 수 있다. 만약 결과가 예상한 값과 맞지 않는다고하면 이 단언(assertion)은 코딩 로직안에 문제가 있다라는 암시와 함께 실패한다. 그러므로 로직을 다시 확인할 필요가 있다. org.junit.Assert 클래스는 모든 원시타입(primitive type), 객체, 배열을 위한 예상값과 실제값을 단언하는 오버로드된 정적메소드의 셋을 제공한다.

모든 단언(assert) 메소들은 첫번째 아규먼트로 문자열 메세지를 가지는 버전을 가지고 있고 만약 단언이 실패했을 때 이 문자열메시지가 보여지게 된다. 다음은 유용한 단언메소드들이다.

  • assertTrue(assert condition) 또는 assertTrue(failure message, assert condition): 만약 단언 조건이 false가 된다면 단언은 실패하고 assertTrue메소드는 AssertionError를 발생시킨다. 실패메세지가 통과될 때 실패메세지는 발생(throw)되게 된다.

  • assertFalse(boolean condition) 또는 assertFalse(failure message, boolean condition): 이 단언메소드들은 false가 될 Boolean 조건이 통과하기를 기대한다. 예를 들면 만약 유저로그인이 실패할 것이라고 예상한다면 isValidUser()를 호출하거나 객체가 null이 될 것을 예상하고 obj == null 체크한다. 그러나 만약 조건이 참이 될 경우 (isValidUser() 메소드가 true를 반환하거나 obj가 null이 아닌 경우) 단언은 실패하고 assertFalse메소드는 AssertionError를 통과된 에러메시지와 함께 던진다.

  • assertNull: 이 메소드는 통과된 아규먼트가 null이 될 것을 예상한다. 만약 아규먼트가 null이 되지 않으면 단언은 실패하고 이 메소드는 AssertionError를 던진다. 이 메소드는 유효한 입력값을 메소드에 통과하고 결과가 null이 되는 것을 예상할 때 유용하다.

  • assertNotNull: 이 메소드는 통과된 아규먼트가 null이 되지 않을 것을 예상한다. 만약 아규먼트가 null이 되면 단언은 실패하고 이 메소드는 AssertionError를 던진다. 메소드를 호출하고 응답객체를 가져온다고 가정하자. null이 아님을 응답을 단언할 수 있다 그리고 나서 응답의 다른 속성을 확인할 수 있다.

  • assertEquals(string message, object expected, object actual) 또는 assertEquals(object expected, object actual) 또는 assertEquals(primitive expected, primitive actual): 이 메소드는 두개의 아규먼트를 받는다. 예상값과 실제값이다. 그리고 값을 비교한다. 만약 아규먼트가 서로 맞지 않으면 AssertionError가 발생한다. 윈시값들이 이 메소드를 통과될 때 값들이 비교된다. 만약 객체들이 통과되면 equals() 메소드가 expedted 비교(equals) actual 이런식으로 호출된다.

  • assertSame(object expected, object actual): 이 메소드는 두 개의 같은 객체 참조가 이 메소드에 통과될 것이라는 것을 예상한다. 객체 참조는 == 연산자를 사용하여 확인하고 만약 두 개의 다른 객체가 통과되면 AssertionError를 던진다.

  • assertNotSame: 이 메소드는 두개의 다른 객체 참조가 이 메소드에 통과 될것을 예상한다. 만약 같은 객체 참조가 통과되면 이 단언은 실패한다.

    가끔 double값 계산은 자바가 double값을 저장하기 위해 사용하는 표현때문에 예기치 않은 결과를 초래한다. 다음은 double 값 계산의 불확실성을 설명한다. double변수 result = .999 + .98을 선언한다. 그 result변수는 1.98값을 가져야 한다. 그러나 result변수값을 콘솔에 출력하면 결과가 1.9889999999999999가 보여진다. 그래서 만약 double값 1.98을 단언하면 단언은 실패한다. double 계산의 불확실성때문에 Assert클래스는 double비교에 의존하지 않는다. 이런 이유로 assertEquals(double expected, double actual) 메소드는 더 이상 사용되지 않는다. 그 대신에 Assert는 double값 단언을 위해서 assertEquals(double expected, double actual, double delta) 형태의 오버로드 된 assertEquals메소드를 제공한다. 세번째 아규먼트 delta는 기대값이 실제값과 맞지 않을 때 매우 중요하다. 예상값과 실제값이 맞지 않을 때 두 값이 차이가 delta값과 같거나 작은 경우 단언은 통과되는쪽으로 고려한다. 통화계산을 위해서 절대로 double값을 사용하지 말야아한다. 대신에 BigDecimal을 사용하자.

다음 예제에서 assert메소드들을 살펴볼 것이다.

  1. com.paktpub.junit.recap 패키지에 AssertTest라는 이름의 JUnit테스트 클래스를 추가한다. 그리고 클래스안에 다음 코드를 추가한다.

     package com.packtpub.junit.recap
    
     import org.junit.Assert;
     import org.junit.Test;
    
     public class AssertTest {
    
         @Test
         public void assert_boolean_condition() throws Exception {
             Assert.assertTrue(true);
             Assert.assertFalse(false);
         }
    
         @Test
         public void assert_null_and_not_noll_object_values() throws Exception {
             Object object = null;
             Assert.assertNull(object);
    
             object = new String("String value");
             Assert.assertNotNull(object);
           }
       }
    

    assert_boolean_conditions테스트는 assertTrue에 true를 assertFalse에 false를 보낸다. 만약 assertTrue에 false가 또는 assertFalse가 true가 통과하면 테스트는 실패한다. assert_null_and_not_null_object_values테스트는 null객체를 생성하고 assertNull메소드에 null객체를 통과시킨다. 객체에 문자열값을 재할당하고나서 assertNotNull에 문자열을 통과시킨다.
    테스트를 실행시카자. 테스트들은 녹색이 될 것이다.

    1. assertEquals의 행동을 조사할 것이다. 클래스안에 아래의 테스트 코드를 포함시키자. 이전 예제에서는 정적방법으로 단언메소드를 사용하였다. 정적(static) assertEquals메소드를 임포트하고 로컬메소드와 같은 단언메소드를 호출하자.

      import static org.junit.Assert.assertEquals;
      
      @Test
      public void assert_equals_test() throws Exception {
        Integer anInteger = 5;
        Integer anotherInteger = 5;
        assertEquals(anInteger, anotherInteger);
      }
      

      이 테스트는 두 개의 정수객체 anInteger와 anotherInteger에 5로 초기화시키고, assertEquals메소드에 통과시킨다. 결국, assertEquals메소드는 anInteger.equals(anotherInteger)를 호출한다. 값들이 같다면 equals메소드는 true를 반환하고 단언은 통과한다. assertEquals메소드는 값을 비교하고 assertSame은 참조를 비교하는것에 주목하자 만약 double값을 비교하기를 원한다면 assertEquals(actual, expected, delta)의 delta버전을 사용하던지 double값 대신에 BigDecimal을 사용하자.

  2. assertNotSame의 행동을 검증할 것이다. 테스트클래스에 아래의 테스트를 추가하고 정적 단언메소드를 임포트하자.

     import static org.junit.Assert.assertNotSame;
    
     @Test
     public void assert_not_same_test() throws Exception {
         Integer anInt = new Integer("5");
         Integer anotherInt = new Integer("5");
         assertNotSame(anInt, anotherInt);
     }
    

    assertNotSame메소드는 예상 객체의 참조와 실제 객첵의 참조가 같은 메모리 주소를 가르키고 있다면 AssertionError를 발생시킨다. anInt와 anotherInt는 같은 값을 가진다. 하지만 두 객체는 서로 다른 메모리 주소를 가르키고 있다. 따라서 assertNotSame메소드는 통과한다.

  3. assertSame의 행동을 조사할 것이다. 테스트클래스에 아래의 테스트를 추가하고 정적 단언메소드를 임포트하자.

     import static org.junit.Assert.assertSame;
    
     @Test
     public void assert_same_test() throws Exception {
         Integer anInt = new Integer("5");
         Integer anotherInt = anInt;
         assertSame(anInt, anotherInt);
     }
    

    테스트는 anInt와 anotherInt가 같은 메모리를 참조하기 때문에 통과한다.

예외처리 살펴보기

이 장에서는 JUnit테스트에서의 예외를 다룬다. JUnit테스트에서는 테스트메소드가 예외를 던질(throws)때 테스트는 실패하고 테스트메소드는 틀렸다라는 것을 테스트에 표시한다. 단위테스트에 예외적인 조건을 허락해보자. 예를 들면 어떤 API는 두 개의 객체를 받아들이면서 아규먼트로 null이 통과된다면 예외를 던진다고 하자. 만약 API에 null값이 통과시키면 테스트는 에러와 함께 실패한다. 그러나 사실은 에외는 에러가 아니다. 오히려 이것이 바람직하다 또한 만약 테스트는 API가 예외를 던지지 않았다면 실패하게 된다.

JUnit4는 이전상황을 다루는 메카니즘을 제공한다.

@Test애노테이션은 expected=<<Exception class name>>.class 야규먼트를 받는다.

테스트메소드에 @Test애노테이션 및 @Test애노테이션에 예상 예외가 전달하지만 실행시간동안 테스트메소드로부터 던저진 실제 예외가 예상되는 예외와 맞지않는다. 또한 테스트메소드는 예외를 던지지 않는다. 테스트는 실패한다.

아래의 테스트 코드는 예외 처리를 검사한다.

    @Test(expected=RuntimeException.class)
    public void test_excetion_condition() {
        throws new RuntimeException();
    }

이 예외 처리 메카니즘은 에러메시지를 검증하는 것을 허락하지 않는다. JUnit4는 더 좋은 해결책을 고려하는 몇개의 다른 메카니즘을 제공한다. 예를 들면 타입과 마찬가지로 메세지를 검사하게 해주는 ExpectedException 규칙인 @Rule애노테이션이 있다.

@RunWith애노테이션과 작업하기

테스트런너(Test runner)는 JUnit테스트를 실행을 수행한다. JUnit테스트가 이클립스에서 실행할 때 녹색막대 혹은 빨강막대와 같은 시각적인 결과를 얻는다. 이클립스는 JUnit테스팅을 위한 시각적인 러너를 자체에 포함하고 있다.

@RunWith애노테이션은 클래스이름을 받는다. 그 클래스는 org.junit.runner.Runner.class를 상속해야 된다. 러너의 한 예가 JUnit4.class 있다. 이 클래스는 기본 Junit4클래스러너로 알려져 있다.

테스트실행동안 테스트클래스에 @RunWith애노테이션을 달던지 또는 @RunWith 클래스를 상속받던지 내장된 JUnit4러너는 무시된다. 대신에 JUnit은 @RunWith아규먼트를 참조하는 러너를 사용한다.

러너는 테스트클래스의 성질을 변경할 수 있다. 예를 들어 스프링러너는 스프링문맥(Spring context)을 초기화 할 수 있게 하고, Mockito러너는 @Mock애노테이션으로 주석이 붙은 프락시객체를 초기화한다.

Suite는 많은 패키지있는 테스트들이 포함한 묶음(suite)을 빌드하게 해주는 기본 러너이다. 아래는 @RunWith의 예이다.

    @RunWith(Suite.class)
    public class MySuite {

    }

@테스트묶음(test suites)과 작업하기

테스트묶음은 여러개의 테스트를 묶고 실행한다. 이클립스에서 개별적인 테스트클래스들을 실행했다. 하지만 여러개의 테스트들을 함께 실행하기 위해서 테스트묶음이 필요하다. 여러개의 테스트들을 함께 실행하기 위해서 JUnit4는 Suite.class와 @Suite.SuiteClasses애노테이션을 제공한다. 이 애노테이션은 콤마로 분리된 테스트클래스들의 배열을 받는다.

TestSuite 이름의 자바클래스를 추가하고 @RunWith(Suite.class)애노테이션을 달자. 결과적으로 묶음(suite)러너는 테스트클래스를 실행하기 위한 책임을 진다.

TestSuite클래스에 @Suite.SuiteClasses와 아래와 같은 콤마로 분리된 다른 테스트클래스들을 통과하는 애노테이션을 붙이자. ({ AssertTest.class, TestExcutionOrder.class, Assumption.class })

아래는 테스트묶음을 위한 코드이다.

    import org.junit.runner.RunWith;
    import org.junit.runner.Suite;

    @RunWith(Suite.class)
    @Suite.SuiteClasses({ AssertTest.class, TestExecutionOrder.class, Assumption.class})
    public class TestSuite {

    }

TestSuite클래스를 실행하면 결과적으로 @Suite.SuiteClasses애노테이션에 통과하는 모든 테스트클래스들이 실행된다. 아래의 그림은 테스트묶음의 실행결과를 보여준다.