의존관계 주입(DI)

의존관계 주입(DI)

제어의 역전(Ioc)과 의존관계 주입

우리가 배우고 있는 IoC는 너무 폭넓게 사용하는 언어입니다. 그래서 스프링을 IoC 컨테이너 라고만 말하면 스프링이 제공하는 기능의 특징을 명확하게 설명할 수 없습니다.

이렇게 명확하지 않고 너무 폭넓게 사용하기 때문에 스프링이 제공하는 IoC방식의 핵심을 짚어주는 의존관계 주입(Dependency Injection) 이라는 단어를 사용해서 부르기로 약속하였습니다.

스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불립니다.


런타임 의존관계 설정

의존관계

우선 의존관계 라는 것은 방향성이라는 것이 있습니다. 의존을 하는 것과 의존을 받는 것 2가지로 나뉘어집니다.

만약 의존한다 라는 것은 의존 대상이 변하면 의존하는 것에 영향을 미친다는 것입니다.

다음과 같이 의존이라는 것은 점선으로 된 화살표로 나타냅니다.

현재는 A가 B에 의존하고 있다는 것을 나타냅니다.

만약, B가 변하면 A에 영향이 그대로 전달됩니다.

의존관계에는 방향성이 있습니다. 서로 의존관계라면 예를들어서 B가 현재 새로운 메서드가 추가되거나 기존 메서드의 형식이 변한다면 A에도 영향을 미쳐서 A도 변화를 시켜야합니다.


UserDao의 의존관계

UserDao 는 현재 ConnectionMaker 라는 인터페이스에 의존하고 있습니다.

그래서 만약, ConnectionMaker 의 메서드가 변하던가 이러한 일들이 일어난다면 UserDao에 영향을 직접적으로 받게 됩니다.

하지만 UserDaoConnectionMaker 를 구현한 클래스인 다른 회사의 Maker 예를들어서 AconnectionMaker 등이 변화할 때는 영향이 없습니다.

원래는 UserDaoConnectionMaker 라는 인터페이스가 없었을 경우는 다른 회사의 ConnectionMaker 로 변화했을 경우에 의존관계 때문에 UserDao 의 코드가 직접적으로 변경이 되어야 했지만 이제는 그렇지 않습니다.

이런식으로 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워질 수 있습니다.

우리가 이런식으로 ConnecitonMaker 라는 인터페이스를 통해서 새로 만들어서 UserDao 는 어떤 회사의 ConnectionMaker 를 사용하는지 알지도 못한채로 사용을 하고있습니다.

그래서 결론적으로 UserDaoConnectionMaker 라는 인터페이스를 사용하고 있기 때문에 UserDao 의 오브젝트가 런타임 시에 사용할 오브젝트(의존 오브젝트 / Dependent Object)가 어떤 클래스로 만든 것인지 미리 알 수가 없습니다.

결론적으로 의존관계 주입이란 구체적인 의존 오브젝트와 그것을 사용할 사용할 주체인 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말합니다.

이러한 의존관계 주입은 다음과 같은 세가지 조건을 충족하는 작업을 말합니다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야합니다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

우리는 현재로 DaoFactory 를 사용해서 DI에서 말하는 제 3의 존재를 설정하였습니다.


UserDao의 의존관계 주입

우리는 UserDao에 적용된 의존관계 주입 기술을 다시 살펴볼 것입니다.

우리가 만든 예전의 UserDao 클래스의 생성자는 다음과 같습니다.

public UserDao(){
    connectionMaker = new AConnectionMaker();
}

이러한 UserDao 의 예전 코드는 저런식으로 AConnectionMaker() 라는 구체적인 클래스를 알고있습니다.

이러한 것은 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는 점이 문제점입니다.

이를 우리는 IoC 방식을 사용해서 UserDaoDaoFactory 라는 제3의 존재에 런타임 의존관계 결정 권환을 위임을 시켰습니다.

이런방식으로 우리는 앞서말한 3가지 조건을 충족시켜서 의존관계 주입을 이용했습니다. 이렇게 만들어진 DaoFactory는 다음과 같은 기능을 합니다.

  • 두 오브젝트 사이의 런터임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재입니다.
  • IoC방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너입니다.
  • 또한, 이를 의존관계 주입을 담당하는 컨테이너라고 볼 수 있고 DI 컨테이너라고 불러도 됩니다.

그래서 결론적으로 DaoFactoryDI 컨테이너라고 부릅니다.

우리는 DI 컨테이너인 DaoFactory 를 통해서 UserDao를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 AConnectionMaker의 오브젝트를 전달합니다.

이런식으로 파라미터 전달을 생성자를 통해서 해줍니다. 그래서 우리는 결론적으로 다음과 같은 코드로 런타임 의존관계를 두 오브젝트간에 만들었습니다.

public class UserDao{
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }
}

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 래퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다. 라고 보이기 때문에 이를 의존관계 주입(DI)라고 합니다.


의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아닙니다.

의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용해서 하는 방식도 있습니다.

이러한 것을 의존관계 검색(dependency lookup)이라고 불리는 것도 있습니다. 한번 살펴봅시다.

다음과 같은 UserDao의 생성자 리스트를 만들었다고 합니다.

public UserDao(){
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

이렇게 만든 생성자는 자신이 어떤 기업의 ConnectionMaker 오브젝트를 사용할지 미리 알지 못합니다.

또한, 의존대상은 ConnectionMaker 인터페이스 뿐입니다.

코드를 보면 DaoFactory 가 만들어서 돌려주는 오브젝트와 다이내믹하게 런타임 의존관계를 맺고있습니다.

그래서 IoC 개념을 잘 따르고 있지만 적용 방법이 생성자에서 DaoFactroy 에게 스스로 요청을 하고 있습니다.

지금은 자바코드지만 스프링에서 Application Context를 사용했다고 생각하면 다음과 같이 코드가 바뀔것 입니다.

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

getBean() 이라는 메서드를 사용해서 의존관계 검색을 합니다.

이런식으로 ApplicationContext를 사용해서 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 하기 때문에 우리는 의존관계 검색이라고 합니다.

이렇게 만들어진 의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 가집니다. 또한, IoC 원칙에도 잘 들어맞습니다. 하지만 방법만 다릅니다.

결론적으로 어떤것이 더 낫냐라고 본다면 의존관계 주입이 더 깔끔합니다. 또한, 다른 단점들도 존재합니다.

  • 의존관계 검색을 사용하면 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타나서 성격이 다른 오브젝트에 의존하게 되어 바람직하지 않습니다.
  • 사용자에 대한 DB 정보를 어떻게 가져올건가에 집중하는 UserDao 에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여있으면 어색합니다.

그래서 의존관계 주입 방법을 사용하지만 의존관계 검색 방식을 사용해야할 떄도 있습니다.

앞서서 만들었던 UserDaoTest를 다시 봅시다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

확인해보면 의존관계 검색 방식을 사용한 것을 볼 수 있습니다. 이러한 경우는 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 합니다.

왜냐하면 main() 메서드는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문에 우리는 서블릿이라는 것을 사용하여 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다.

우리가 사용하는 의존관계 검색의존관계 주입은 차이점이 있습니다.

의존관계 검색 방식은 검색하는 오브젝트는 자신이 스프링의 빈일 필요는 없습니다.

위에 코드를 살펴보면 UserDao 는 굳이 getBean()이 아닌 new UserDao() 해서 만들어도 사용해도 됩니다.

의존관계 주입에서는 UserDaoConnectionMaker 사이에 DI가 적용되려면 UserDao 도 반드시 컨테이너가 만드는 빈 오브젝트여야 합니다.

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말아야합니다.


의존관계 주입의 응용

런타임 시에 사용 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점은 무엇인지 확인해봅시다.

우리가 앞에서 오브젝트 팩토리로 구현을 하였는데 이는 DI 방식을 구현한 것입니다. 이러한 DI의 장점을 그대로 얻을 수 있습니다.

  • 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않습니다.
  • 인터페이스를 통해 결합도가 낮은 코드를 만들어 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않습니다.
  • 변경을 통한 다양한 확장 방법에는 자유롭습니다.

우리는 이러한 DI 방식을 스프링을 사용하면 99% 혜택을 볼 수 있습니다. 이러한 DI 방식의 여러가지 응용사례를 살펴봅시다.


기능 구현의 교환

실제 운영에 사용할 데이터베이스는 매우 중요한 자원입니다. 이러한 데이터 베이스는 항상 부하가 많이 가해집니다. 그래서 개발 중에는 절대로 사용하지 말아야합니다.

그래서 이제 개발자의 로컬 DB로 사용한다고 생각해봅시다. 우리는 로컬 DB에 대한 기능이 있는 LocalDBConnectionMaker 라는 클래스를 만들고 제일 처음에 만들었던 초난감 DAO와 같이 사용하고 있다고 생각해봅시다.

이렇게 Local에서 사용을 하고나서 다시 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야한다고 생각해봅시다.

우리는 일단 초난감 DAO방식을 사용하고 있기 때문에 모든 DAO들에는 new LocalDBConnectionMaker() 라는 코드를 사용해서 LocalDBConnectionMaker에 의존을 하고 있을 것입니다.

만약 이제 배포를 해야한다고 생각해봅시다. 그러면 LocalDBConnectionMaker 에서 ProductionDBConnectionMaker 라는 클래스로 변경해줘야 한다고 생각해봅시다. 만약 DAO가 1000개라면 1000개의 코드를 모두 수정해야합니다. 이러한 방식은 매우 끔찍합니다.

만약, 초난감 DAO가 아닌 DI 방식을 적용해서 만들었다고 생각해봅시다.

그러면 DAO는 생성 시점에서 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공을 받습니다.

컨테이너에는 구체적으로 사용할 클래스 이름이 들어있습니다.

그래서 @Configuration이 붙은 DaoFactory를 사용한다고 한다면 개발자 PC에서 DaoFactory 코드를 이전에 만든 방식처럼 만들면 됩니다.

@Configuration
public DaoFactory{
    @Bean
    public ConnectionMaker connectionMaker() {
        return new LocalDBConnectionMaker();
    }
}

이런 방식처럼 만든다면 DAO 클래스에서는 어떤 코드도 변경을 안해도 됩니다. 단지 다음과 같이 변경만 해주면 됩니다.

@Configuration
public DaoFactory{
    @Bean
    public ConnectionMaker connectionMaker() {
        return new ProductionDBConnectionMaker();
    }
}

이런식으로 DB가 개발용 배포용이 나눠져있거나 다른 DB들을 여러개 사용할 경우 이런식으로 DI 컨테이너를 만들어서 모든 DAO가 사용할 수 있도록 DI 해줄 수 있습니다.


부가기능 추가

만약에 우리가 DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 생각해봅시다.

첫 번째 방법으로는 그냥 단순하게 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣을 수 있습니다. 하지만 이런 방식은 너무 비효율적 입니다.

두 번째 방법으로는 DI 컨테이너에서 사용할 수 있는 방법입니다. DI에서는 매우 간단하게 하결할 수 있습니다. 그냥 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면 됩니다.

DI 개념을 쉽게 응용한다면 기존코드를 수정하지 않고 그냥 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의하면 가능합니다. 우리는 CountingConnectionMaker라는 클래스를 리스트 1-30과 같이 만듭니다. 또한, 이 클래스는 ConnectionMaker 인터페이스를 구현해서 만든다고 합니다.

public class CoutingConnectionMaker implements ConnectionMaker{
    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    public Connection makeConnection() thorws ClassNotFoundException, SQLException {
        this.counter++;
        return realConnectionMaker.makeConnection();
    }

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

CountingConnectionMaker 클래스는 현재 ConnectionMaker 를 인터페이스를 implement해서 구현을 하였지만 직접 DB커넥션을 만들지는 않았습니다.

대신 DAO가 DB를 가져오는 makeConnection()에서 DB 연결횟수 카운터를 증가시킵니다.

CountingConnectionMaker 는 DB 연결횟수 카운팅이 관심사여서 이를 끝낸 뒤에는 DB 커넥션이 정의되어 있는 realConnectionMaker 에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려줍니다.

이러한 것은 CountingConnectionMaker도 DI를 받고 이에 대한 오브젝트가 DI를 받을 오브젝트도 ConnectionMaker 인터페이스를 구현한 오브젝트입니다. 결론적으로 우리가 사용하는 AConnectionMaker 같은 실제 DB 커넥션을 돌려주는 클래스의 오브젝트를 받습ㄴ디ㅏ.

이는 CountingConnectionMakerConnectionMakerUserDao 로 가는 것을 볼 수 있습니다.

AConnectionMaker 가 구현하고 있는 ConnectionMaker 라는 인터페이스에만 의존하고 있기 때문에 만약 ConnectionMaker 를 implement 하고 있는 어떤 클래스라도 바꿔치기가 가능합니다. 그래서 CountingConnectionMaker 가 사용이 가능한 것입니다.

우리는 이것만 사용하면 안됩니다. CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 AConnectionMaker 를 호출하도록 만들어야 합니다. 이럴 때도 DI를 사용하면 됩니다.

다음과 같이 CountingDaoFactory 라는 컨테이너를 만들어서 사용할 수 있게 만들 수 있습니다.

@Configuration
public class CountingDaoFactory {
    @Bean
    public UserDao userDao(){
        return new UserDao(connetionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new CountingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker(){
            return new AConnectionMaker();
    }
}

이제 커넥션 카운팅을 위한 실행코드를 만들어 봅시다. DL(의존관계 검색)을 사용해서 CountingConnectionMaker 빈을 가져와서 작성해봅시다.

public class UserDaoConnectionCountingTest {
    public static void main(String[] args) throws ClasNotFoundException, SQLException {
        AnnotaionConfigApplicationContext context = 
            new AnnotationConfigApplicationContext(CountingDaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        // DL 사용
        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("Connection counter의 수는 : " + ccm.getCounter());
    }

}

이런식으로 DAO가 여러개여도 관심사의 분리를 통해서 얻어지는 높은 응집도에서 DI의 장점이 나옵니다.

이러한 DI는 정말 중요하고 좋은 도구입니다. 그래서 우리는 DI를 어떻게 활용해야 할지를 공부해야합니다.


메서드를 이용한 의존관계 주입

지금까지는 UserDao의 의존관계 주입을 위해서 생성자를 사용하였습니다.

그래서 생성자에 파라미터를 만들어서 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주었습니다.

현재까지는 생성자에만 사용했지만 이는 일반 메서드를 통해서 의존 오브젝트와의 관계를 주입해줄 수도 있습니다.


수정자(Setter) 메서드를 이용한 주입

수정자는 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하는 것 입니다. 이러한 수정자는 DI 방식에서 활용하기에 적당합니다.


일반 메서드를 이용한 주입

수정자는 정해진 형태가 있고 파라미터는 한개받게 못받습니다. 만약 이런게 싫다면 일반 메서드를 이용할 수도 있습니다.

하지만 파라미터의 개수가 많아져 비슷한 타입이 여러 개라면 실수하기가 쉽습니다. 하지만, 여러개의 파라미터를 받아서 여러 개의 초기화 메서드도 만들 수 있기 때문에 이런것에 대해서 장점이 존재합니다.

스프링은 보통 메서드를 이용한 DI 방식중에서는 수정자 메서드를 가장 많이 사용했습니다.

수정자 메서드 DI를 사용할 때에는 메서드의 이름을 잘 결정하는 것이 좋습니다.

한번 만들어 봅시다. 우리는 기존 생성자를 제거하고 setConnectionMaker() 라는 메서드를 하나 추가한 뒤 파라미터로 ConnectionMaker 타입의 오브젝트를 받도록 선언합시다.

public class UserDao{
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

이런식으로 만들어 주었다면 DaoFactory의 코드도 함꼐 수정해야합니다.

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setConnectionMaker(connetionMaker());
    return userDao;
}

이러면 의존관계를 주입하는 시점과 방법만 달라지고 결과는 동일합니다. 이뿐만이 아니라 다양한 의존관계 주입 방법도 지원합니다.


XML을 이용한 설정

지금까지 DI 컨테이너인 스프링을 도입하면서 애너테이션을 추가해서 DI를 설정해주었습니다.

우리는 DaoFactory와 같은 자바 클래스로만 이용하여 DI 의존관계 설정정보를 만들었지만 다른 방법도 존재합니다.

우리는 그중에서 XML을 이용하는 방법을 살펴 봅시다.

XML의 장점은 다음과 같습니다.

  • 단순한 텍스트 파일이기 떄문에 다루기 쉽습니다.
  • 쉽게 이해할 수 있고 컴파일과 같은 별도의 빌드작업이 필요가 없습니다.
  • 환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 빠르게 변경사항을 반영할 수 있습니다.
  • 스키마나 DTD를 이용해서 정해진 포맷을 따라 작성되었는지 손쉽게 확인할 수 있습니다.

이제 한번 XML을 통해서 만들어 봅시다.


XML 설정

스프링 Application Context는 XML에 담긴 DI 정보를 활용할 수 있습니다.

우리는 <beans> 를 루트 엘리먼트로 사용해서 사용할 수 있습니다. 이러한 <beans> 안에는 여러개의 <bean> 가 들어갈 수 있습니다.

본래대로면 @Bean 메서드에서는 알 수 있는 빈의 DI 정보는 3가지가 존재합니다.

  • 빈의 이름 : 메서드의 이름입니다.
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스에 이용해서 만들지 결정합니다.
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메서드 등을 통해서 의존 오브젝트를 넣어줍니다. 이는 하나 이상일 수도 있습니다.

XML에서도 마찬가지로 이러한 3가지 정보를 정의할 수 있습니다.


connectionMaker() 전환

목록 자바 코드 설정정보 XML 설정정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean 메서드이름() <bean id=”메서드이름”
빈의 클래스 return new BeanClass(); class=”a.b.c...BeanClass”>

다음처럼 1:1교환으로 만들 수 있습니다. 여기서 <bean> 에 들어간느 class 속성에 지정하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름입니다.

그렇기 때문에 메서드의 리턴 타입을 class의 속성에 넣으면 안됩니다.

이를 변경하면 다음과 같습니다.

@Bean
public ConnectionMaker connectionMaker(){
    return new AConnectionMaker();
}

XML은 다음과 같습니다.

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

DI 컨테이너는 이러한 XML의 <bean> 태그의 정보를 읽어서 우리가 작성했던 connectionMaker() 메서드 와 같은 작업을 진행합니다.


userDao()전환

이번에는 userDao 를 XML로 변환해봅시다. 보통 수정자 메서드를 사용하는게 왜 스프링개발자들이 선호를 했었냐면 XML로 의존관계 정보를 만들 때 편리하기 때문입니다.

만약 수정자 메서드를 사용하면 setConnectionMaker() 가 있다면 set 을 제외한 connectionMaker 라는 프로퍼티를 가집니다.

XML에서는 <property> 태그를 사용해서 의존 오브젝트와의 관계를 정의합니다. 이는 2개의 속성을 가집니다.

  • name : 프로퍼티의 이름 → 이를 통해서 수정자 메서드를 알 수 있습니다.
  • ref : 수정자 메서드를 통해서 주입해줄 오브젝트의 빈 이름입니다. / DI할 오브젝트도 빈입니다. 그 빈의 이름을 지정해주면 됩니다.

그래서 만약에 다음과 같은 방식으로 UserDaoTest 에서 사용을 한다면 하나씩 확인을 해봅시다.

userDao.setConnectionMaker(connectionMaker());

여기서 connectionMaker()userDao 빈의 connectionMaker() 라는 프로퍼티를 이용해서 의존관계 정보를 주입한다는 것입니다. 그래서 ref의 값이 됩니다.

이를 XML로 고치면 다음과 같습니다.

<bean id="userDao" class="springbook.dao.UserDao">
    <property name="connectionMaker" ref="connectionMaker" />
</bean>

XML의 의존관계 주입정보

이를 모두 전환을 해서 적으면 다음과 같습니다.

<beans>
    <bean id="connectionMaker" class="springbook.user.dao.AConnectionMaker" />
    <bean id-="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="connectionMaker"/>
    </bean>
</beans>

현재는 property 태그의 nameref 가 같지만 이름이 같더라도 어떤 차이가 있는지 구별할 수 있어야 합니다.

  • name 속성은 DI에 사용할 수정자 메서드의 프로퍼티 이름입니다.
  • ref 속성은 주입할 오브젝트를 정의한 빈의 ID 입니다.

보통 둘이 같은 경우가 많습니다. 이러한 프로퍼티의 이름은 보통 주입할 빈 오브젝트의 인터페이스를 따르는 경우가 많습니다.

만약 빈의 이름이 중복되거나 의미를 좀 더 잘 드러낼 수 있는 이름이 있다면 변경을 할때 property 태그의 ref 속성의 값도 함께 변경을 해줘야 합니다.

만약 connectionMaker 에서 myConnectionMaker 로 변경했다고 하면 어떻게 되는지 봅시다.

<beans>
    <bean id="**myConnectionMaker**" class="springbook.user.dao.AConnectionMaker" />
    <bean id-="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="**myConnectionMaker**"/>
    </bean>
</beans>

만약, 인터페이스를 구현한 의존 오브젝트를 여러개 만들어 놓고 골라서 사용하는 경우도 있습니다. 아까전에 로컬 DB를 사용하는것 혹은 배포할 때DB를 사용하는것 이러한 기능들이 있는 경우도 있습니다. 그럴때는 다음과 같이 작성을 합니다.

<bean>
    <bean id="**localDBConnectionMaker**" class="...LocalDBConnectionMaker" />
    <bean id="testDBConnectionMaker" class="...testDBConnectionMaker"/>
    <bean id="productionDBConnectionMaker" class="...productionDBConnectionMaker"/>

<bean id-="userDao" class="springbook.user.dao.UserDao">
            <property name="connectionMaker" ref="**localDBConnectionMaker**"/>
    </bean>
</bean>

이런식으로 사용하고 ref 만 내가 사용할 것을 놓으면 됩니다. 바뀌면 이것도 바꾸면 됩니다.


XML을 이용하는 애플리케이션 컨텍스트

이제 Application Context가 DaoFactory 대신 XML 설정정보를 활용하도록 만들어 봅시다.

XML을 사용하면 IoC/DI 작업에서는 GenericXmlApplicationContext 를 사용해서 합니다. 이에대한 생성자 파라미터로 XML 파일의 클래스패스를 정하면 됩니다. 클래스패스를 정해야하기 때문에 보통은 클래스패스 최상단에 둡니다.

이러한 설정파일을 만들 떄는 보통 applicationContext.xml 로 만들어 저장합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="connectionMaker" class="springbook.user.dao.AConnectionMaker"/>

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="connectionMaker"/>
    </bean>
</beans>

이제 UserDaoTest를 수정해봅시다. 이제 AnnotationConfigApplicationContext 대신에 GenericXmlApplicationContext를 사용합니다.

ApplicationContext context = new GenericXmlApplicationContext(
        "applicationContext.xml");

이런식으로 바꿔서 사용하면 됩니다.

또한, GenericXmlApplicationContext 가 아닌 ClassPathXmlApplicationContext 도 존재합니다 이는 XML 파일을 클래스패스에서 가져올 때 사용할 수 있는 편리한 기능이 추가된 것입니다.

보통 XML 파일의 경로를 가져와야하는데 귀찮으면 자동으로 클래스 오브젝트를 사용해서 다음과 같이 변환을 할 수 있습니다.

new GenericXmlApplicationContexct("springbook/user/dao/daoContext.xml");

new ClassPathXmlApplicationContext("daoContext.xml", UserDao.class);

DataSource 인터페이스로 변환

DataSource 인터페이스 적용

우리는 ConnectionMaker 를 만들어 DB 커넥션을 생성해주는 기능 하나만을 정의한 매우 단순한 인터페이스를 만들었습니다.

하지만 사실 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 Datasource 라는 인터페이스가 이미 존재하고 있습니다.

그래서 우리는 ConnectionMaker 와 같은 인터페이스를 만들어서 사용할 일은 없습니다.

Datasource 에서 getConnection() 메서드는 우리가 만든 makeConnection() 과 동일한 기능을 하는 메서드입니다. 이를 통해서 DB 커넥션을 가져올 수 있습니다.

DataSource를 이용해서 한번 UserDao를 리펙토링 해봅시다.

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.getConnection();
  }
}

구현을 하였는데 우리는 DataSource 구현 클래스가 필요합니다. 우리는 스프링에서 제공하는 테스트환경에서 간단히 사용할 수 있는 SimpleDriverDataSource 라는 것을 사용해서 이 클래스를 사용하도록 DI를 재구성하려고합니다.


자바 코드 설정 방식

먼저 DaoFactory의 설정방식을 이용해봅시다. 기존의 connectionMaker() 메서드를 dataSource()로 변경하고 SimpleDriverDataSource의 오브젝트를 리턴하게 합니다.

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

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

    return dataSource;
}

이제 DaoFactoryuserDao() 메서드도 수정해봅시다.

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    **userDao.setDataSource(dataSource());**
    return userDao;
}

이런식으로 connectionMaker()dataSource() 로 변경하였습니다.


XML 설정 방식

먼저 id가 connectionMaker<bean> 을 없앤 뒤 dataSource 라는 이름의 <bean>을 등록합니다. 그리고 SimpleDriverDataSource 로 변경합니다.

<bean id="dataSource"
    class="org.springframwork.jdbc.datasource.SimpleDriverDataSource"/>

이런식으로 SimpleDriverDataSource의 오브젝트를 만드는 것까지 가능하지만 datasource() 메서드에서 SimpleDriverDataSource 의 오브젝트의 수정자로 넣어준 DB접속정보는 없습니다.

UserDao 처럼 다른 빈에 의존하면 그냥 property 태그랑 ref 속성을 사용해서 의존할 빈의 이름을 넣어주면 되었는데 datasource() 인경우는 어떤식으로 해결을 해야할까요?


프로퍼티 값의 주입

값 주입

DaoFactorydatasource() 메서드에서 본것 처럼 수정자 메서드에는 다른 빈이나 오브젝트 뿐만 아니라 스트링 같은 단순 값을 넣어줄 수도 있습니다.

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

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

    return dataSource;
}

이중 setDriverClass() 메서드는 Class 타입의 오브젝트를 넣었지만 다른 빈 오브젝트를 DI 방식으로 가져와서 넣는 것은 아닙니다.

이렇게 사용하는 수정자 또한, <property>를 사용해서 값을 주입할 수 있습니다. 성격은 다르지만 일종의 DI라고도 볼 수 있습니다.

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

이렇게 된 코드들을 XML을 사용해서 DB 연결정보를 설정할 수 있습니다.

<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>

다음과 같은 ref 대신 value 를 써서 나타낼 수 있습니다.


value 값의 자동 변환

그런데 우리는 이상한 점을 볼 수 잇습니다. url, username, password 들은 모두 스트링 타입이기 때문에 텍스트로 정의되는 value 속성 값을 사용하는 것은 문제가 되지 않습니다.

하지만 현재 driverClass 를 보면 java.lang.Class 타입인것을 볼 수 있습니다.

텍스트로 정의되어 있다고 해쓴ㄴ데 별다른 타입정보가 없이 그냥 클래스이름이 텍스트형태로 현재 value에 들어가 있습니다.

직관적으로만 보면 현재 다음과 같은 상황입니다.

Class driverClass = "com.mysql.jdbc.Driver";

이러면 본래 스트링 값을 Class에 넣는것 이기 때문에 컴파일조차 안되기 마련입니다.

하지만 우리는 컴파일을 하면 아무런 문제 없이 성공하는 것을 볼 수 있습니다. 이러한 이유는

스프링이 프로퍼티의 값을 수정자 메서드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해줍니다.

"com.mysql.jdbc.Driver" 라는 텍스트 값을 오브젝트로 자동 변환을 시켜줍니다. 결론적으로는 다음과 같은 코드로 스프링이 자동적으로 변환작업을 해주는 것을 알 수 있습니다.

Class driverClass = Class.forName("com.mysql.jdbc.Driver");
dataSource.setDriverClass(driverClass);

이런식으로 스프링은 value에 지정한 텍스트 값을 적절한 자바 타입으로 변환해주기도 합니다.

결론적으로 우리는 모두 적용하면 다음과 같은 코드를 얻을 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dataSource"
            class="org.springframwork.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost/springbook"/>
        <property name="username" value="spring"/>
        <property name="password" value="book"/>
    </bean>

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

+ Recent posts