잊지 않겠습니다.

'Di'에 해당되는 글 4건

  1. 2016.09.28 Dependency Injection
  2. 2014.08.05 Google Guice를 이용한 DI
  3. 2013.09.06 6-2. Spring을 이용한 Simple Application의 제작(2) 1
  4. 2013.09.06 5. Spring 소개

Dependency Injection

angularjs2 2016. 9. 28. 15:17

Dependency Injection은 angular, angular2의 핵심 기능입니다.

이미 Server side 부분에서 자주 언급되고 있는 것으로, 특정 객체가 사용하는 객체군들을 만들어서 객체를 사용하는 쪽에서 골라 사용하는 개념이지요.

angularjs2는 자체 Dependency Injection Framework를 가지고 있습니다. 모든 ComponentServicePipe,Delegate는 Dependency Injection Framework를 통해 사용되어야지 됩니다.

Root Dependency Injection Tree

angularjs2 applicaiton은 한개 이상의 NgModule을 갖습니다. bootstrap에 연결된 이 NgModule에서 선언된providers import의 경우에는 전체 Global Root Dependency Injection Tree에 등록되게 됩니다.

NgModule에 등록된 providers와 import는 모든 Component에서 사용가능합니다. 각 Component에서 한정적으로만 사용될 Service나 PipeDelegate의 경우에는 각 Component별 provider에 등록해서 사용하면 됩니다.

@Injectable

@Injectable annotation은 DI 대상을 지정합니다. service에서만 @Injectable annotation이 사용되게 됩니다. 다른@Component@Pipe@Directive annotation 모두 @Injectable의 subset이기 때문에 따로 지정할 필요는 없습니다.

Inject Provider

기본적으로 providers에는 그 객체가 지정됩니다. 다음 코드는 일반적인 providers code입니다.

providers: [ Logger ]

이는 축약된 표현으로 원 표현은 다음과 같습니다.

providers: [ { provide: Logger, useClass: Logger } ]

Logger라는 객체를 얻어낼려고 하면 Logger를 넘겨준다는 표현식이 됩니다. 이를 좀 응용하면 다른 일들을 할 수 있습니다. Logger로 지정해두고, AnotherLogger를 inject 시킬 수 있다는 뜻이 됩니다. 다음과 같이요.

providers: [ AnotherLogger, { provide: Logger, useClass: AnotherLogger } ]

위 표현식은 Logger와 AnotherLogger 2개의 instance가 생성되게 됩니다. 모든 Logger를 AnotherLogger로 바꾸어 사용하고 싶은 경우에는 다음과 같이 선언합니다.

providers: [ AnotherLogger, { provide: Logger, useExisting: AnotherLogger } ]

지정된 객체는 singleton instance로 동작하는 것이 기본값입니다. 사용될 때마다 새로운 객체를 얻어내기 위해서는Factory를 생성해야지 됩니다. Factory는 function 형태로 다음과 같이 선언될 수 있습니다.

const anotherLoggerFactory = (logger: Logger) => { return new AnotherLogger(logger); };

이렇게 선언된 Factory를 다음과 같이 providers에 등록하면 이제 AnotherLogger는 사용될 때마다 신규 객체를 생성해서 처리가 되게 됩니다.

providers : [ { provide: AnotherLogger, useFactory: anotherLoggerFactory, deps: [ Logger ] } ]

Value Provider

직접적인 을 제공하고 싶을 때 사용할 수 있습니다. applicaiton의 여러 설정들이나 상수값들을 지정할 때 사용 가능합니다. 다음과 같은 값을 전달할 때 사용할 수 있습니다.

const appConfig = { apiUrl: 'http://testserver:8080/api', userToken: 'jkfla;sjdifpqiowjerkjskadj;fla' };

그런데, 이와 같은 값을 어떻게 하면 provider에 지정이 가능할지 의문이 듭니다. 위 은 객체가 아닙니다. 객체가 아니기 때문에 DI에서 이용되는 Class가 무엇인지 알 수 없습니다. 그래서 angular2에서는 OpaqueToken이라는 것을 제공하고 있습니다. OpaqueToken을 이용해서 다음과 같이 선언후, @Inject에서 OpaqueToken을 지정해주면 됩니다.

import { OpaqueToken } from '@angular/core'; export const APP_CONFIG = new OpaqueToken('app.config'); .... import { APP_CONFIG } from './config'; providers: [ { provide: APP_CONFIG, useValue: appConfig } ] .... constructor(@Inject(APP_CONFIG) config: any) { console.log(config.apiUrl); }

Manual Injector

직접적으로 객체를 DI tree에서 가지고 와서 처리하고 싶을 때도 있을 수 있습니다. sping에서 ApplicationContext에서 바로 객체를 들고오고 싶을 그럴 때와 같이요. 이러한 경우에는 다음과 같이 처리하면 됩니다.

export class InjectorComponent { car: Car = this.injector.get(Car); heroService: HeroService = this.injector.get(HeroService); hero: Hero = this.heroService.getHeroes()[0]; constructor(private injector: Injector) { } }

Root Provider에 이미 Injector 서비스가 등록이 되어 있습니다. 이 등록된 서비스를 가지고 와서 사용하면 됩니다. 개인적으로는 정말 추천하지 않는 방법이긴 합니다.;

SUMMARY

angular2는 다양한 방법의 DI를 제공하고 있습니다. Spring이나 CastleNinject등을 사용해보신 분들이라면 매우 익숙하겠지만, 지금까지 FrontEnd만 해본 사람들에게는 복잡한 내용임은 사실입니다. 그렇지만, DI는 angular2에서 가장 중요한 기능중 하나입니다. 객체를 어떻게 다루고, 관리할지에 대한 pattern을 결정하는 설정이니까요. 개인적으로는OpaqueToken이 매우 인상적인것 같습니다.

Posted by Y2K
,

Dependency Injection

DI란 사용될 Object들이 의존성을 갖는 여러 객체들을 직접 자신이 얻어내는 것을 의미한다. DI는 여러분의 code를 느슨한 결합상태로 만들고, 테스트 하기 쉽게 만들며, 읽기 쉽게 만들어준다.

Java의 Official standard인 JSR-330에서 정의된 DI에 대해서도 역시 알아볼 것이다.

Inject some knowledge - understanding IoC and DI

IoC와 DI의 개념은 매우 헛갈리며, 이 둘을 섞어서 이야기하는 경우가 매우 많다. 이 둘에 대한 내용을 좀더 알아보기로 한다.

IoC

만약에 IoC를 사용하고 있지 않다면, program의 logic은 함수의 조합에 의하여 조절될 것이다. 매우 정밀한 design에 의해서 꾸며진 이 함수들은 여러 Object들에 의해서 재사용이 되어가며 사용되고 있을것입니다.
IoC를 사용한다면, 이에 대한 “central control” 이라는 개념자체를 뒤집게 됩니다. 호출자의 code에 의해 다뤄지는 program의 실행으로 구성이 되며 모든 Program의 Logic은 호출되는 subroutine에 의하여 encapsulated 되게 됩니다.
이는 Design Pattern 중 Hollywood Principal과 동일합니다. 여러분의 code가 호출하는 것이 아닌, 어떤 곳에서 여러분의 code를 호출하는 방식으로의 변경을 의미하게 됩니다.

Text 기반의 mud game이 있고, 이를 GUI Framework로 감싼 Version이 있다고 가정해보도록 합시다. GUI Framework는 어떠한 Logic도 가지고 있지 않습니다. “LEFT Click” 이라는 event가 호출이 되면, 이에 따른 GO LEFT 라는 command가 실행이 되는 것 뿐입니다. 이에 대한 실질적인 로직은 모두 Text 기반의 mud game안에 들어가 있습니다.

IoC에 대해서 이야기한다면, 다른 개념으로 생성자에 대한 접근을 볼 수 있습니다. 우리가 일반적인 개발방식으로 Program Logic을 생성한다면, 많은 생성자를 Call 하는 Code내에서 생성하게 되는 것을 볼 수 있습니다. 그런데, 이와 같은 방식을 뒤집에서 이미 생성되어 있는, 또는 생성할 방법이 결정나 있는 Factory Pattern을 통해 구성되는 객체들로 만들어진다면 이는 객체에 대한 Control을 객체에게 넘겨준 IoC가 적용된 상태라고 할 수 있습니다.

DI (Dependency Injection)

DI는 IoC의 일부입니다. 이는 여러분의 code안에 있는 객체의 dependency를 code 바깥에서 code가 생성되거나 실행될 때, 주입(Inject)하는 것에 촛점이 맞춰져있습니다.

IoC Container를 Spring에서 이야기한다면, ApplicationContext가 그 역활을 합니다.
DI의 경우, @Autowired가 그 일을 담당하게 됩니다.

DI를 구성하는 경우 다음과 같은 장점을 갖습니다.

Loose coupling

많은 코드들 내부에서 가지고 있는 new를 통한 객체의 생성을 하지 않기 때문에, 객체에 대한 coupling이 작아지게 됩니다. interface를 통한 결합을 하게 된다면, 의존되는 객체에 대한 의존성을 제거하고, 객체의 행위에만 집중할 수 있게 됩니다.

Testability

Loose coupling을 통해, 단순화된 객체들은 Test를 행하기 쉽습니다. Loose coupling이 되지 않은 객체들은 각각의 생성자 및 모든 method의 호출 방법의 차이에 따른 Test가 매우 힘듭니다.

Greater cohesion (높은 응집성)

생성된 code들은 객체에 대한 생성방법이나 여러 부가적 initialize를 할 필요가 없기 때문에, Logic에 대한 높은 응집성을 갖게 됩니다. 이는, code에 대한 가독성을 높여주는 장점을 가지고 있습니다.

Reusable components

loose coupling에 의한 결과로서, 다른 객체에서 사용하기 쉬운 상태로 만들어주고, 이는 객체에 대한 재 사용성을 높여주게 됩니다.

Lighter code

이는 높은 응집성에 의하여 나온 결과입니다. dependency 된 객체에 대한 추가 코드는 더이상 존재하지 않고, 사용자가 작성한 code가 직접적으로 호출하는 부분만이 남겨져 있는 상태입니다. 가독성이 높아지고, 버그가 생길 구석이 좀 더 줄어들수 있습니다.

DI의 표준화

2004년 google Guice의 Bob Lee와 SpringSource의 Rod Johnson은 JSR-330(javax.inject)에 대하여 합의하게 됩니다. 이 합의로 인하여, 요구되는 Framework내에서 표준화된 DI를 사용할 수 있게 되었습니다. 가벼운 Guice Framework를 사용하다가, 보다 많은 기능을 가진 Spring Framework로의 전환이 자유롭게 된 것을 의미합니다.

실질적으로 위의 말은 거의 불가능에 가깝습니다. Guice와 Spring Framework의 성격이 IoC와 DI를 제공하는 것이 사실이지만, Project가 진행되어가는데 있어서, 기본적인 javax.inject의 기능만이 아닌 다른 기능을 이용해서 처리하게 되는 것이 일반적입니다. 왜냐면 javax.inject는 Guice와 Spring의 가장 기본적인 기능중 공통점만을 가지고 있기 때문입니다.
지금, javax를 이용하는 경우는, JNDI를 이용한 Resource의 관리 이외에는 크게 사용되고 있지 않습니다. Spring, Guice 만을 이용하게 되는 것이 일반적이지요.

@Inject annotation

@Inject annotation은 삽입(Inject)될 dependency를 지정합니다. @Inject는 다음과 같은 곳에서 사용가능합니다.

  • Constructor
  • Methods
  • Fields

@Qualifier annotation

@Qualifier annotation은 객체들의 identify를 지정합니다. 이는 뒤에 나올 @Named와 밀접한 관계를 갖습니다.

@Named annotation

@Inject와 같이 사용됩니다. @Qualifier에 의하여 지정된 이름을 가진 객체를 @Named에 의해서 지정하여 삽입(Inject)하게 됩니다.

@Scope annotation

삽입될 객체의 생명주기를 나타냅니다. 이는 다음과 같습니다.

  • @Scope가 선언되지 않는 경우, 삽입(Inject)가 발생할 때마다, 객체를 새로 생성하게 됩니다.
  • @Scope가 선언되는 경우, 삽입(Inject)가 발생될 때, 기존에 생성된 객체가 있는 경우에는 그 객체를 사용하게 됩니다. 이는 thread-safe해야지 될 필요성을 갖게 됩니다.

@Scope annotation은 각 IoC Framework에서 새롭게 정의되어서 사용되는 것이 일반적이니다. 또한 @Singleton annotation이 생긴후, JSR-330에서는 @Singleton을 이용한 객체 선언을 주로 해주는 경우가 더 많습니다.

@Singleton annotation

@Scope가 선언된것과 동일하게 동작합니다. 이는 거의 모든 DI Framework의 기본 동작입니다.

Provide< T > interface

T 객체에 대한 구현이 아닌 Provide<T>에 대한 구현을 요구하는 경우가 있습니다. 이런 경우, 다음과 같은 목적으로 사용되게 됩니다.

  • 여러 instance를 생성해서 사용하는 경우.
  • 객체를 사용할 때, Lazy Loading을 이용해서 객체를 얻어낼 필요가 있을 때.
  • circular dependency를 회피할 목적으로.

Google Guice를 이용한 DI sample code

/**
 * Created by ykyoon on 14. 8. 5.
 * code에 @Inject가 될 대상
 */
public class AgentFinder {
    public void doSomeThing() {
        System.out.println("Print SomeThing");
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * @Inject를 이용해서 서비스 받을 대상
 */
public class HollyWoodService {
    private AgentFinder agentFinder;

    @Inject
    public HollyWoodService(AgentFinder agentFinder) {
        this.agentFinder = agentFinder;
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * AgentFinder에 대한 dependency될 객체 설정 - Spring의 @Configuration과 거의 동일
 */
public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class);
    }
}

위 코드에서 보면 알수 있듯이, AbstractModule을 이용한 Configuration이 행해진다는 것에 유의할 필요가 있다. DI를 사용하는 code를 JUnit test code로 작성하면 다음과 같다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho = injector.getInstance(HollyWoodService.class);
        assertThat(ho, is(not(nullValue())));
    }
}

Google Guice의 AbstractModule을 보면 Spring의 @Configuration과 매우 유사합니다. 그리고 문법의 경우에는 .NET 진영의 ninject와 거의 유사합니다. (서로간에 영향을 받은건지, 아니면 ninject가 영향을 받은 건지는 잘 모르겠습니다.)

이번에는 객체의 생명주기를 결정하는 Scope입니다.

Scope를 이용한 객체 생명주기의 설정

AppModule내에서 객체의 생명주기를 결정 가능합니다.

public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class)
                .annotatedWith(Names.named("primary"))
                .to(AgentFinderImpl.class)
                .in(Singleton.class);
        bind(HollyWoodService.class);
    }
}

위 구성으로 다음 테스트 코드를 실해하면 결과는 다음과 같습니다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho1 = injector.getInstance(HollyWoodService.class);
        assertThat(ho1, is(not(nullValue())));
        HollyWoodService ho2 = injector.getInstance(HollyWoodService.class);
        System.out.println(ho1);
        System.out.println(ho2);
        assertThat(ho1 != ho2, is(true));

        System.out.println(ho1.getAgentFinder());
        System.out.println(ho2.getAgentFinder());
        assertThat(ho1.getAgentFinder() == ho2.getAgentFinder(), is(true));
    }
}
me.xyzlast.gg.hollywood.HollyWoodService@2b9627bc
me.xyzlast.gg.hollywood.HollyWoodService@65e2dbf3
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0

Singleton으로 설정된 객체는 호출될 때, 기존의 객체가 있는 경우에는 그 객체를 계속해서 사용하게 됩니다. 그렇지만, 아무것도 설정되지 않은 객체의 경우에는 마치 new를 통해서 생성되는 객체와 동일한 패턴을 따르게 됩니다.


Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


지난 장에서는 JDBC를 이용한 db의 접근. 그리고 간단한 table의 CRUD를 하는 방법에 대해서 조금 깊게 들어가봤습니다. 또한 저번 시간의 최대 포인트는 테스트입니다. 테스트를 어떻게 작성을 하는지에 대한 논의와 테스트 코드를 직접 사용해보는 시간을 가져봤습니다. 이번 시간에는 드디어 Spring을 이용한 코드에 대해서 논의해보도록 하겠습니다.기존 코드의 가장 큰 문제는 무엇인가요?

1.  DB connection이 변경되는 경우, compile을 다시 해줘야지 된다.
2.  DB connection의 open/close가 모든 method에서 반복된다.

크게 보면 이 두가지의 문제가 나오게 됩니다. 

Connection은 외부에서 설정할 수 있는 영역입니다. 특히 이런 부분은 config file로 관리가 되는 것이 일반적이고, Enterprise 구성환경에서는 JNDI를 이용한 DB Connection이 제공되는 경우도 많습니다. 따라서, 외부에서 설정이 가능한 것이라고 생각해도 좋습니다. 또한, BookApp은 books table에 CRUD를 하는 것을 목적으로 하는 객체입니다. 객체에 대한 가장 큰 원칙인 단일 책임의 원칙에 의해서, BookApp에서 Connection까지 관리가 되는 것은 영역을 넘어가게 됩니다. 또한, books 이외의 table이 존재할 때, Connection에 대한 코드는 중복될 수 밖에 없는 코드가 됩니다. 따라서 Connection을 제공하는 객체로 따로 분리를 해줍시다. 

Connection을 관리, 생성하는 객체이기 때문에 ConnectionFactory라고 명명하고, 객체를 구성합니다. 객체의 코드는 다음과 같습니다.

public class ConnectionFactory {
     private String connectionString;
     private String driverName;
     private String username;
     private String password;
    
     public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Class.forName(this.driverName).newInstance();
          Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
          return conn;
     }
     public String getConnectionString() {
          return connectionString;
     }
     public void setConnectionString(String connectionString) {
          this.connectionString = connectionString;
     }
     public String getDriverName() {
          return driverName;
     }
     public void setDriverName(String driverName) {
          this.driverName = driverName;
     }
     public String getUsername() {
          return username;
     }
     public void setUsername(String username) {
          this.username = username;
     }
     public String getPassword() {
          return password;
     }
     public void setPassword(String password) {
          this.password = password;
     }
}

그리고, ConnectionFactory를 이용한 코드로 TestCode를 다시 구성해보도록 하겟습니다. 

public class BookAppTest {
     private BookApp bookApp = new BookApp();
     private ConnectionFactory connectionFactory = new ConnectionFactory("com.mysql.jdbc.Driver", "jdbc:mysql://localhost/bookstore", "root", "qwer12#$");
    
     private List<Book> getBooks() {
          Book book1 = new Book();
          book1.setId(1);
          book1.setName("book name01");
          book1.setAuthor("autor name 01");
          book1.setComment("comment01");
          book1.setPublishDate(new Date());
         
          Book book2 = new Book();
          book2.setId(2);
          book2.setName("book name02");
          book2.setAuthor("autor name 02");
          book2.setComment("comment02");
          book2.setPublishDate(new Date());
          Book book3 = new Book();
          book3.setId(3);
          book3.setName("book name03");
          book3.setAuthor("autor name 03");
          book3.setComment("comment03");
          book3.setPublishDate(new Date());
         
          List<Book> books = Arrays.asList(book1, book2, book3);
          return books;
     }
    
     private void compareBook(Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Book dbBook = bookApp.get(book.getId());
          assertThat(dbBook.getName(), is(book.getName()));
          assertThat(dbBook.getAuthor(), is(book.getAuthor()));
          assertThat(dbBook.getComment(), is(book.getComment()));
          assertThat(dbBook.getPublishDate().toString(), is(book.getPublishDate().toString()));
     }
    
     @Before
     public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          bookApp.setConnectionFactory(connectionFactory);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }
    
     @Test
     public void addAndCount() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
     }
    
     @Test
     public void update() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
              
               book.setName("changed name");
               book.setPublishDate(new Date());
               book.setAuthor("changed author");
               bookApp.update(book);
              
               compareBook(book);
          }
     }
    
     @Test
     public void getAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
         
          List<Book> books2 = bookApp.getAll();
          assertThat(books2.size(), is(books.size()));
     }
    
     @Test
     public void search() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          List<Book> books = getBooks();
          int count = 0;
          for(Book book : books) {
               bookApp.add(book);
               count++;
               assertThat(bookApp.countAll(), is(count));
          }
         
          List<Book> searchedBooks = bookApp.search("01");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("02");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("03");
          assertThat(searchedBooks.size(), is(1));
         
          searchedBooks = bookApp.search("name");
          assertThat(searchedBooks.size(), is(3));
     }
}

지금까지 구성된 BookApp을 지금 사용하는 객체는 test 객체만이 유일한 client입니다. 사용하는 객체(client)에서 자신이 사용할 객체의 구성을 결정하는 행위를 IoC(inverse of control)이라고 합니다. 

여기서, Spring의 가장 주요한 기능을 사용할 때가 왔습니다. 

Spring은 IoC를 지원하는 경량 container입니다. 

그런데, IoC를 (어떻게) 지원하는 지에 대한 설명을 한다면 어려운 말로 DI를 통해서 지원한다고 할 수 있습니다. DI란 Dependency Injection의 약자입니다. 사용할 객체의 Dependency를 inject 한다는 뜻입니다. 여기서 말이 좀더 어려워서 예시를 이용해서 바꿔주도록 하겠습니다. 

BookApp은 ConnectionFactory에 의존한다.
BookAppTest는 BookApp에 의존한다.
BookAppTest는 BookApp의 ConnectionFactory를 변경시켜서 사용한다.

최종적으로, BookAppTest는 BookApp을 사용하지만, BookApp이 종속되어 있는 ConnectionFactory를 변경시켜서 사용하게 됩니다. 이때, 의존되어 있는 ConnectionFactory를 변경하는 행위를 DI(dependency Injection)이라고 합니다. 따라서, 위 코드를 좀 유식한 말로 풀어서 한다면, BookApp을 DI를 통해 사용하는 코드 라고 할 수 있습니다. 이를 그림으로 도식화 해보면 다음과 같습니다. 


지금 이야기드린 IoC, DI는 Spring에서 굉장히 중요한 개념입니다. Spring은 light DI container라고 할 정도로 Spring의 핵심 기능중 하나입니다. 



지금까지 작성된 코드를 보면 처음과는 다른 특징을 가지고 있습니다.
1. 테스트를 통해서 검증이 가능합니다.
2. connectionString 및 jdbc Driver를 사용하는 code내에서 찾아서 사용합니다.
3.중복 코드가 존재하지 않습니다.

위의 특징은 잘 짜진 코드라면 당연히 가져야지 되는 특징입니다.


Spring을 이용한 IoC/DI

spring을 이용하면 지금까지 구성되었던 코드를 보다 더 깔끔하게 만들어줄 수 있습니다. 먼저 spring에 대한 dependency를 추가시켜줍니다.

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.0.RELEASE</version>
      <scope>test</scope>
    </dependency>

spring-test는 scope를 이용해서, test code에서만 사용하도록 지정한것을 제외하고는 mysql dependency를 추가하는 방법과 동일하게 설정해줍니다.
applicationContext.xml 파일을 src/test/resource에 추가하고 다음과 같이 적어줍니다.
<?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="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
        <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
        <property name="driverName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="qwer12#$"/>
    </bean>
    <bean id="bookApp" class="com.xyzlast.bookstore01.domain.BookApp">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean></beans>

solution의 directory 구조는 다음과 같습니다. 





테스트 코드를 다음과 같이 수정합니다.


    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        ApplicationContext context = new GenericXmlApplicationContext("/appContext.xml");
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }


테스트 코드가 모두 통과됨을 알 수 있습니다. 어떻게 해서 이렇게 된 것인지 서술해보도록 하겠습니다.
applicationContext.xml 파일은 사용할 객체에 대한 정의 파일입니다. spring에서는 ApplicationContext로 불리우는 객체에 대한 Hashtable 이라고 생각하시면 됩니다.
appContext에 정의된 ConnectionFactory 객체와 BookApp 이라는 객체에 대한 관계를 봐보시면 좀 더 생각이 쉬울 수 있습니다. 먼저 connectionFactory 라는 이름으로 ConnectionFactory를 정의합니다. 저희가 property로 뽑은 connectionString, driverName, username, password에 대한 값들을 모두 설정하는 것을 볼 수 있습니다. 그리고, bookApp이라는 이름으로 설정된 BookApp 객체를 보시면 좀더 재미잇는 코드를 볼 수 있습니다. ref 라는 키워드로 값을 설정하고 있는데. 이는 먼저 설정된 connectionFactory 객체를 bookApp에 주입(Inject) 시키고 있는 것을 알 수 있습니다.
그리고, 테스트 코드에 대해서 알아보도록 하겠습니다. 테스트 코드에서는 먼저 GenericXmlApplicationContext 객체를 이용해서 applicationContext를 전체 로드합니다. 로드된 xml을 기초로 객체들을 생성하고, 그 객체를 Hashtable 형태로 저장하는 일을 합니다.
Hashtable 형태로 저장된 객체들은 각각의 id를 통해서 로드가 가능하며, bookApp = (BookApp) context.getBean("bookApp"); 코드를 통해서 BookApp을 로드할 수 있습니다. 이렇게 spring을 통해서 객체를 사용하는 것이 어떤 장점을 갖게 될까요?

Spring을 사용한 객체의 사용의 장점은 크게 3가지로 볼 수 있습니다.

1. 정해진 규칙에 따른 객체의 선언
2. xml로 정의된 객체의 dependency 파악이 가능
3. 테스트의 편의성

먼저 정해진 규칙에 따른 객체의 선언은 팀단위의 개발자들에게 일정한 개발 패턴을 만들어줍니다. 정해진 개발 패턴은 정형화된 코드를 만들고, 서로간에 코드의 공유가 원활하게 할 수 있습니다. 그리고, xml을 통한 객체의 의존성 관리는 객체들이 어떠한 구조를 가지고 있는지를 파악하는데 도움을 줍니다. 마지막으로 테스트의 편의성을 들 수 있습니다. spring은 테스트 코드를 작성하는데, 지금까지 보던 코드보다 더욱 깔끔하고 쉬운 테스트 패턴을 제공하고 있습니다. Spring을 사용한 테스트 코드를 다시 한번 알아보도록 하겠습니다. 

지금까지 작성된 코드를 기반으로, Spring을 이용한 테스트 코드는 다음과 같이 구성될 수 있습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/applicationContext.xml")
public class BookAppTest {
    @Autowired
    private ApplicationContext context;
    private BookApp bookApp;

    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }

Test class에 @RunWith와 @ContextConfiguration annotation이 붙어 있는 것을 알 수 있습니다. @RunWith는 Spring을 이용한 JUnit4 test class임을 명시하는 선언입니다. 중요한것은 @ContextConfiguration인데, 이는 방금 작성한 applicationContext.xml를 지정하는 영역입니다. @ContextConfiguration이 설정된 경우, Test class가 로드 되면서, xml 파일안에 위치한 객체를 생성해서 spring application context에 저장하고 있습니다. 4 line에 위치한 @Autowired는 spring application context 중에 type이 동일한 객체를 자동으로 할당하는 annotation입니다. spring test는 1개의 applicationContext를 로드하기 때문에, 그 객체를 context 변수에 자동으로 할당하게 됩니다. 자신이 구성한 applicationContext.xml은 반드시 이러한 과정을 거쳐서 테스트를 통과시켜야지 됩니다. 


Spring에서의 객체의 생명 주기


spring에서의 객체의 생명 주기는 기본적으로 한번 사용된 객체를 재사용합니다. 테스트 코드를 통해서 이를 확인해보도록 하겠습니다.
구성된 코드에 System.out.println 을 이용해서 bookApp 객체의 instance id를 확인해보도록 하겠습니다. 

     @Before
     public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          //context = new GenericXmlApplicationContext("/appContext.xml"); @ContextConfiguration에 의하여 필요없는 코드가 되었습니다.
          bookApp = (BookApp) context.getBean("bookApp");
          System.out.println(bookApp);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }

테스트 코드의 console 창의 결과는 다음과 같습니다.

INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
2월 18, 2013 2:57:58 오후 org.springframework.context.support.AbstractApplicationContext doClose

보시면 BookApp의 모든 객체 Id가 동일함을 알 수 있습니다. ApplictionContext에 저장된 객체를 얻을 때, 새로 생성하는 것이 아닌 기존의 객체를 계속해서 사용하는 것을 알 수 있습니다.
왜 모든 객체들을 기본적으로 재사용하게 될까요? 기본적으로 spring은 enterprise development framework입니다. enterprise급의 대규모 시스템에서는 객체의 생성/삭제가 많은 부담을 주게 됩니다. 이에 대한 해결 방법으로 spring은 application context가 로드 될 때, 기본 객체들을 모두 생성, 로드 하는 것을 기본으로 하고 있습니다. 물론, 다른 방법역시 가능합니다.

Spring에서의 객체의 생명주기는 scope로 불리고, 다음 4가지로 관리가 가능합니다.

1.singleton
2.prototype
3.session
4.request

singleton은 default 값입니다. scope를 따로 설정하지 않으면 모두 singleton으로 동작합니다. 이는 객체를 static object와 동일하게 사용하게 됩니다.
prototype은 일반적으로 저희가 사용하던 객체의 생성방법과 동일합니다. new 를 통해서 객체를 생성하고, property 값을 모두 설정시킨 후, 그 객체를 넘겨주게 됩니다.
session과 request는 web programming에서 사용되는 scope입니다. 새로운 session이 생성될 때, 새로운 request가 생성이 될때 사용될 수 있는 scope 입니다.
크게는 singleton과 prototype을 이용하면 대부분의 객체 생명주기 관리는 가능하게 되며 그 차이를 한번 알아보도록 하겠습니다. scope를 prototype으로 선언해보도록 하겠습니다.

    <bean id="bookApp" class="com.xyzlast.bookstore01.domain.BookApp" scope="prototype">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean>

그리고, 테스트코드를 수행해보도록 하겠습니다.

2월 18, 2013 2:45:30 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@3622102f
com.xyzlast.bookstore01.domain.BookApp@78f14616
com.xyzlast.bookstore01.domain.BookApp@c4e6fe5
2월 18, 2013 2:45:31 오후 org.springframework.context.support.AbstractApplicationContext doClose

각각 4개의 BookApp bean이 생성됨을 알 수 있습니다. spring 설정 만으로 다음과 같은 코드가 만들어지는 것과 동일한 효과를 가지고 오게 되는 것입니다.

BookApp bookApp = new BookApp();
bookApp.setConnectionFactory(new ConnectionFactory);

이 부분을 보면 Factory Pattern에서의 Factory와 동일한 기능을 가지게 되는 것을 알 수 있는데요. ApplicationContext는 bean의 Map과 Factory를 지원한다. 라고 할 수 있습니다.
spring을 사용하게 되면, 객체들을 new로 새롭게 할당하는 일들이 얼마 없습니다. 모든 객체 bean들은 spring을 통해서 관리가 되고, bean들간의 데이터 교환을 위한 POJO(Plain Old Java Object)들만 new로 생성되어서 데이터 교환이 되는 것이 일반적인 패턴입니다. POJO에 대해서는 좀더 나중에 알아보도록 하겠습니다.


Spring에서의 객체 주입과 이용에 대한 심화 학습

먼저 이야기한 내용에서 bean의 Map을 제공한다는 이야기를 했습니다. Spring은 ApplicationContext라는 bean Map / Factory를 가지고 있고, ApplicationContext를 통해서 객체를 얻어내거나 생성하게 됩니다. 이때, 객체의 초기화 방법은 다음 두가지로 나눌 수 있습니다. 
1. property를 이용한 주입
2. 생성자를 이용한 주입

property를 이용한 주입은 기존 connectionFactory의 xml 선언을 보면 쉽게 알 수 있습니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
    <property name="driverName" value="com.mysql.jdbc.Driver"/>
    <property name="username" value="root"/>
    <property name="password" value="qwer12#$"/></bean>


생성자를 이용한 주입은 constructor-arg를 이용해서 선언 가능합니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <constructor-arg index="0" value="jdbc:mysql://localhost/bookstore"/>
    <constructor-arg index="1" value="com.mysql.jdbc.Driver"/>
    <constructor-arg index="2" value="root"/>
    <constructor-arg index="3" value="qwer12#$"/>
</bean>   


어떤 방법이 좋은지에 대해서는 찬/반이 나뉘고 있습니다. 장점과 단점은 다음과 같습니다.
방법장점단점
Property를 이용한 방법1. 설정값에 대한 설명을 Property를 통해 명확히 알 수 있다.
2. 객체가 변경되었을 때, 확장이 용의하다.
1. 사용되는 Bean의 내용을 정확히 알지 못하면, Property 값을 설정하는 것을 빼먹을 수 있다.
생성자를 이용한 방법1. 필요한 값을 모두 설정하는 것이 가능하다.2. 값을 index와 같이, 순서를 이용하기 때문에 어떤 값을 설정하는지 파악하기 힘들다.

둘다 좋은 방법이지만, spring을 이용하는 대부분의 library 들은 property를 이용한 주입 을 주로 하고 있습니다. 이건 개발을 하는 팀에서 한가지로 정해서 가는 것이 좋습니다.

객체를 사용하다보면, 객체를 초기화 시켜야지 되는 경우가 자주 생깁니다. 지금 코드에서는 ConnectionFactory의 Class.forName().newInstace()의 경우에는 한번만 실행되어도 코드의 동작에는 아무런 문제가 없기 때문에, 생성 후, 한번만 실행이 되어도 아무런 문제가 없습니다. 이러한 초기화 method를 실행시키는 xml선언은 다음과 같습니다. 

    <bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory" init-method="init">
        <property name="connectionString" value="jdbc:mysql://localhost/bookstore"/>
        <property name="driverName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="qwer12#$"/>
    </bean>


init-method가 포함된 객체의 생성 process는 다음과 같습니다. (singleton scope의 경우)

1. 객체의 생성
2. property 값들의 설정
3. init-method 의 실행

init-method가 property값들이 모두 설정 된 후에 실행됨을 확인해주셔야지 됩니다. 


Summary

이번 장에서는 Spring을 이용한 객체의 관리를 위주로 Simple Application을 작성해봤습니다. 다음 개념들은 반드시 다시 정리해보시는 것이 좋습니다.

1. ApplicationContext : Spring에서 제공하는 bean의 Map/ObjectFactory
2. property, constructor, init-method를 이용한 객체 초기화 방법
3. IoC, DI


Posted by Y2K
,

5. Spring 소개

Java 2013. 9. 6. 14:15

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



이 장에서는 Spring Framework에 대한 소개와 왜 Spring을 써야지 되는지에 대한 당위성을 간단한 application을 작성하면서 알아보도록 하겠습니다.
application을 작성하면서 놀랍게 줄어드는 코드 양과 Spring의 강력함을 느끼실 수 있으실겁니다.

Spring Framework 란 엔터프라이즈급 자바 어플리케이션 개발에서 필요로 하는 경량형 어플리케이션 프레임워크입니다. 
스프링 프레임워크는 J2EE[Java 2 Enterprise Edition] 에서 제공하는 대부분의 기능을 지원하기 때문에, J2EE를 대체하는 프레임워크로 자리잡고 있습니다. Spring이라는 이름의 기원은 기존 EJB로 대표되는 Enterprise Framework의 시대를 겨울(winter)로 정의하고, 이젠 봄(Spring)이 왔다 라는 의미로 지어졌습니다. 시작은 한권의 책의 예제에서부터 시작이 되었습니다. 

Spring Framework는 다음과 같은 특징을 가지고 있습니다.

1) 경량 컨테이너입니다. (light container) 스프링은 객체를 담고 있는 컨테이너로써 자바 객체의 생성과 소멸과 같은 라이프사이클을 관리하고, 언제든 필요한 객체를 가져다 사용할 수 있도록 도와주는 기능을 가지고 있습니다.
2) DI[Dependency Injection] 패턴 지원을 지원합니다. (DI : 의존성 주입)
= 별도의 설정 파일을 통해 객체들간의 의존 관계등을 설정할 수 있습니다.  그로인해 객체들간의 느슨한 결합을 유지하고 직접 의존하고 있는 객체를 굳이 생성하거나 검색할 필요성이 없이 구성이 가능합니다. 이는 IoC(Inversion of Controller)로 이야기되기도 합니다. 정확히는 DI로 인한 IoC를 가능하게 하는 Framework라고 할 수 있습니다.
3) AOP[Aspect Oriented Programming] 지원 (AOP : 측면 지향 프로그래밍 )
= AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법이다. 이는 문제를 해결하기 위한 핵심 관심 사항과 전체에 적용되는 공통관심 사항을 기준으로 프로그래밍 함으로써 공통 모듈을 여러 코드에 쉽게 적용할 수 있도록 한다.
스프링은 자체적으로 프록시 기반의 AOP를 지원하므로 트랜잭션이나 로깅, 보안등과 같이 여러 모듈에서 공통적으로 필요하지만 실제모듈핵심은 아닌 기능들을 분리하여 각 모듈에 적용할 수 있도록 한다.

Spring Framework는 위의 3가지의 특징을 가진 Framework입니다. 또한, 부가적인 기능으로서 ruby on rails에서 표방한 non shared status web 개발을 지원하는 @MVC 역시 지원하고 있습니다.


Spring Framework의 기본구조입니다. 위에서 말한 3가지의 특징은 Spring Core와 Spring AOP, Spring Context에 의하여 구성이 되어 있습니다. 나머지 ORM, WEB, DAO, WEB MVC의 경우에는 부가적 기능이라고도 볼 수 있습니다.

이런식으로만 적어두면, Spring이 과연 무엇을 하는 녀석인지를 알 수가 없습니다. 그래서 간단한 예제를 통해서 Spring을 통해서 점점 진화가 되어가는 코드의 변화를 보면서 Spring을 익혀보도록 하겠습니다. 


Posted by Y2K
,