티스토리 뷰

이번 글에서는 스프링의 가장 기본이 되는 개념인 IoC와 DI에 대해 이해해보겠습니다.

 

최종적으로 IoC와 DI를 이해하기 위해 데이터베이스에 접속하여 데이터를 추가하고 조회하는 간단한 기능을 하는 DAO를 예로 살펴보겠습니다. (프로젝트는 Gradle 빌드 기반을 사용하였습니다.)

 

초기 코드

데이터베이스에 접속하는 방식에 대한 내용을 담은 ConnectionMaker 인터페이스를 만들고 이를 구현한 두 개의 DConnectionMaker, NConnectionMaker를 만들었습니다. (추후 또 다른 데이터베이스 접근 방식이 필요한 경우 ConnectionMaker 인터페이스를 구현한 또 다른 클래스를 생성하면 됩니다.)

package dao;

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

public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
public class DConnectionMaker implements ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // DConnectionMaker 사용하는 사람의 connection 정보
    }
}

public class NConnectionMaker implements ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // NConnectionMaker 사용하는 사람의 connection 정보
    }
}

 

그리고 나서 데이터를 추가하고 조회하는 기능을 가진 UserDao를 만들고 UserDao에서 ConnectionMaker 인터페이스를 사용할 수 있도록 연결하였습니다. 이때, ConnectionMaker 인터페이스를 구현한 실제 클래스를 UserDao에서 선택해야 합니다.

public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao() {
        connectionMaker = new DConnectionMaker();  // 실제 구현 클래스를 선택!!!!!
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();

        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();

        ...
    }
}

 

위 코드의 UserDao 생성자에서 new DConnectionMaker();는 간단하지만 UserDao가 직접 사용할 ConnectionMaker의 특정 구현 클래스를 선택하고 생성하는 기능을 담고 있습니다.

토비의 스프링 그림 1-5 참고

 

만약 UserDao의 구현 내용을 모른 채 DB 연결 방식을 변경하여 제공하고 싶은 경우(DConnectionMaker 대신 NConnectionMaker를 사용하고 싶다면) 위와 같은 방식은 UserDao를 직접 수정해야하기 때문에 사용할 수 없습니다.

 

따라서  ConnectionMaker 의 구현 클래스를 선택하고 생성하는 기능을 UserDao를 사용하는 클라이언트에게 주어 그 클라이언트가 UserDao를 생성할 때 사용할 ConnectionMaker를 전달하는 방식으로 변경해보겠습니다.

 

IoC

외부에서 만든 오브젝트를 전달받으려면 메소드 파라미터나 생성자 파라미터를 이용하면 됩니다.

// UserDao를 사용하는 클라이언트에서 
// DaoFactory 클래스를 통해 ConnectionMaker 구현체를 선택하여 생성된 UserDao를 전달 받아서 사용

public class Main {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDao dao = new DaoFactory().userDao();

        ....
    }
}
package dao;

// ConnectionMaker 객체를 선택하고 생성하여
// UserDao 객체에 ConnectionMaker를 전달한 뒤
// 생성된 UserDao를 반환하는 클래스 분리

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(getConnection());
    }

    // 추가로 다른 Dao들이 생겼을 때를 중복을 방지하기 위한 메소드 분리
    public ConnectionMaker getConnection() {
        return new DConnectionMaker();
    }
}
// 생성자 파라미터를 통해 외부로부터 ConnectionMaker 구현 객체를 전달받기
public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}

 

UserDao의 클라이언트(main)는 UserDao를 사용하는 입장에서 DaoFactory 클래스를 통해 ConnectionMaker의 구현 클래스를 선택하고 이를 UserDao에 연결해줍니다. 이렇게 함으로써 UserDao 클래스와 DConnectionMaker 클래스의 오브젝트 사이에 런타임 사용관계 또는 링크, 또는 의존 관계라고 불리는 관계가 만들어지며 UserDao 오브젝트에서 DConnectionMaker 오브젝트를 사용할 수 있게 됩니다.

토비의 스프링 그림 1-8 참고

 

위 코드는 다음과 같은 객체지향 기술에 따라 개선되었다고 볼 수 있습니다.

✔️개방 폐쇄 원칙

: '클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.'는 원칙으로 인터페이스를 통해 제공되는 확장 포인트는 확장을 위해 열려 있으면서 인터페이스를 이용하는 클래스는 자신의 변화가 불필요하게 일어나지 않도록 닫혀있게 만드는 방식입니다.

 

✔️높은 응집도와 낮은 결합도

: 응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 것을 말합니다. 결합도란 하나의 오브젝트가 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도를 나타내며 이런 결합도가 낮을수록 변화에 대응하는 속도가 높아지고 확장에 용이합니다.

 

✔️전략 패턴

: 자신의 기능 맥락(Context)에서 필요에 따라 변경이 필요한 알고리즘(독립적인 책임으로 분리가 가능한 기능)을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴입니다. 컨텍스트(UserDao)를 사용하는 클라이언트(Main 내 DaoFactory)는 컨텍스트가 사용할 전략(ConnectionMaker를 구현한 DConnectionMaker 등)을 컨텍스트의 생성자 등을 통해 제공해주는 게 일반적입니다.

 

위와 같이 오브젝트가 자신이 사용할 오브젝트를 스스로 선택, 생성하지않으며 자신 또한 어떻게 만들어지고 어디서 사용되는지를 알 수 없이 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하는 것을 제어의 역전 (IoC)이라고 합니다.

 

IoC 방식은 스프링을 사용하지 않고도 구성할 수 있습니다. 하지만 애플리케이션 전반에서 IoC 방식을 사용하려면 스프링과 같은 프레임워크에서 제공하는 IoC를 사용하는 것이 좋습니다. 그렇다면 지금부터 스프링에서 제공하는 IoC 방식을 사용해보도록 하겠습니다.

 

스프링 IoC

스프링에서는 빈(스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트)의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 합니다. 이런 빈 팩토리를 IoC 방식에 따라 확장한 애플리케이션 컨텍스트가 있는데, 이는 별도의 정보를 참고해서 빈(오브젝트)의 생성, 관계설정 등의 제어 작업을 총괄하게 됩니다.

 

DaoFactory를 애플리케이션 컨텍스트가 사용할 수 있는 설정 정보로 변경해 보겠습니다.

  • @Configuration: 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
  • @Bean: 오브젝트 생성을 담당하는 IoC용 메소드라는 표시
package dao;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(getConnection());
    }

    // 추가로 다른 Dao들이 생겼을 때를 중복을 방지하기 위한 메소드 분리
    @Bean
    public ConnectionMaker getConnection() {
        return new DConnectionMaker();
    }
}
public static void main(String[] args) throws SQLException, ClassNotFoundException {
    // @Configuration 이 붙은 자바 코드를 설정정보로 사용하는 애플리케이션 컨텍스트 생성
    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    // 애플리케이션 컨텍스트가 관리하는 오브젝트를 getBean 메소드로 요청 (메소드명, 클래스 타입)
    UserDao dao = context.getBean("userDao", UserDao.class);
    
    ...
}

 

애플리케이션 컨텍스트의 동작 방식은 다음과 같습니다.

토비의 스프링 그림 1-9 참고

 

애플리케이션 컨텍스트를 사용할 때의 장점은 아래와 같습니다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없습니다. DaoFactory처럼 IoC를 적용한 오브젝트들이 증가할 경우, 클라이언트가 필요한 오브젝트를 가져오기 위해 어떤 팩토리클래스를 사용할지 정하고 변경해야하는 번거로움을 없앨 수 있습니다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 할 수 있으며 부가적으로 자동생성, 후처리, 정보 조합, 설정 방식 다변화, 인터셉팅 등 다양한 기능을 제공합니다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다. getBean 메소드를 통한 빈 이름으로 검색 뿐만 아니라 타입으로 검색 또는 특정 애노테이션 설정이 되어있는 빈도 검색할 수 있습니다.

 

싱글톤 레지스트리

애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 합니다. 스프링의 별도의 설정이 없다면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만듭니다. 스프링은 주로 서버 환경에서 사용이 되는데 클라이언트의 요청이 있을 때마다 새로 오브젝트를 생성하게 되면 부하가 많이 걸려 서비스 제공에 어려움이 있기 때문에 애플리케이션 안에서 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 싱글톤 패턴이 적용되어 있습니다.

 

싱글톤 레지스트리는 일반적인 싱글톤 패턴의 문제를 개선한 방식으로 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용해서 생성과 관계설정, 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있습니다.

 

싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지 방식으로 만들지 않고 파라미터와 로컬 변수, 리턴 값 등을 이용하여 정보를 정의하고 사용합니다. 그러나 자신이 사용하는 다른 싱글톤 빈을 저장하는 용도이거나 읽기 전용 속성의 정보라고 한다면 인스턴스 변수를 사용해도 무방합니다.

 

DI

클래스 A가 클래스 B를 사용하는 경우, 즉 A에서 B에 정의된 메소드를 호출하는 경우 "사용에 대한 의존관계"가 있다고 할 수 있습니다. 여기서 의존이라는 것은 B 클래스의 변화가 A 클래스에 영향을 줄 수 있다는 것입니다. 주로 이런 의존 관계는 A 클래스 내에서 직접 B 클래스를 생성(new) 함으로써 맺어집니다.

 

하지만 구체적인 의존 오브젝트(런타임 시에 의존 관계를 맺는 대상)와 그것을 사용할 주체(클라이언트) 오브젝트를 런타임 시에 연결해주는 작업을 통해 외부로부터 오브젝트를 받아와 관계를 맺을 수 있는데, 이를 "의존관계 주입"이라고 합니다.

 

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것입니다. 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제3의 존재라고 볼 수 있습니다.

 

주입이라는 것은 외부에서 내부로 무엇인가를 넘겨준다는 것인데, 자바에서는 오브젝트에 무엇인가를 넣어준다는 개념은 메소드를 실행할 때 파라미터로 오브젝트의 레퍼런스를 전달하는 방법 뿐이며 가장 손쉽게 사용할 수 있는 메소드는 생성자입니다.

 

의존관계 주입을 사용하기 위해서는 주입을 받으려는 오브젝트와 주입되는 오브젝트 모두 컨테이너가 만드는 빈 오브젝트로써 관리되어야 합니다.

 

xml 설정 파일

애플리케이션 컨텍스트가 사용할 설정 정보는 @Configuration 애노테이션을 사용한 java 클래스로 만들 수 있지만 xml을 사용하여 만드는 것도 가능합니다. xml 은 단순한 텍스트 파일이기 때문에 다루기 쉽고 컴파일 같은 별도의 빌드 작업도 없다는 것이 장점입니다.

 

위에서 사용한 DaoFactory를 xml 설정 파일로 변경해보겠습니다.

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

 

변경 사항을 비교하여 살펴보면 다음과 같습니다.

@Configuration <beans></beans>

@Bean <bean></bean>

메소드명 <bean id="메소드명" />

반환 타입(오브젝트) <bean id="메소드명" class="반환 클래스 경로"/>

 

객체의 의존관계 주입이 필요한 경우에는 <property> 태그를 사용할 수 있습니다. name 애트리뷰트는 프로퍼티의 이름으로 수정자 메소드를 알 수 있고, ref 애트리뷰트는 수정자 메소드를 통해 주입해줄 오브젝트의 빈 이름(id)을 의미합니다.

 

xml에서 빈의 의존 관계 정보를 이용하는 IoC/DI 작업에는 GenericXmlApplicationContext를 사용합니다.

public static void main(String[] args) throws SQLException, ClassNotFoundException {
    // xml로 작성된 설정정보로 사용하는 애플리케이션 컨텍스트 생성 ("class path")
    ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
    UserDao dao = context.getBean("userDao", UserDao.class);
    
    ...
}

 

의존관계 주입 대신 실제 값을 전달하고 싶은 경우, <property> 태그 내 value 애트리뷰트에 전달하고자 하는 값을 넣어주면 됩니다. 이 방식의 예제를 살피기 위해 소스를 수정하도록 하겠습니다.

 

위에서는 ConnectionMaker를 통해 DB 커넥션을 생성해주는 간단한 인터페이스를 만들었지만, 사실 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 DataSource라는 인터페이스가 존재합니다.

 

따라서 위 코드에서 ConnectionMaker 인터페이스 대신 DataSource를 사용하도록 수정하고 이때 설정파일에 값을 지정하는 방법을 알아보겠습니다.

 

먼저 UserDao에서 ConnectionMaker 인터페이스를 쓰던 부분을 DataSource 인터페이스를 사용하도록 변경합니다.

import javax.sql.DataSource;
import java.sql.*;
...

public class UserDao {
    private DataSource dataSource;

    // 수정자 메소드를 통해 DataSource 인터페이스를 주입받습니다.
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = dataSource.getConnection();
        
        ...
    }
    
    public void get(User user) throws ClassNotFoundException, SQLException {
        Connection c = dataSource.getConnection();
        
        ...
    }
}

 

다음으로 DataSource를 구현한 클래스를 생성하고 수정자 메소드(set 메소드)에 전달하도록 DaoFactory 소스를 변경합니다. DataSource를 구현한 클래스 중 SimpleDriverDataSource라는 것을 사용해보겠습니다. 이를 위해 추가로 org.springframework.jdbc 라이브러리를 추가합니다.

package dao;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;

import javax.sql.DataSource;

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao();
        // 아래에서 생성한 SimpleDriverDataSource를 수정자 메소드를 통해 주입하여 UserDao 오브젝트를 생성합니다.
        userDao.setDataSource(dataSource());
        return userDao;
    }

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

        dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
        dataSource.setUrl("jdbc:mysql://localhost/tobyspring_db");
        dataSource.setUsername("admin");
        dataSource.setPassword("admin123");

        return dataSource;
    }
}

 

xml 설정 파일은 다음과 같습니다. 이때 driverClass는 class 타입의 파라미터를 전달받아야 하지만 문자열로 값을 전달하였습니다. 이유는 스프링이 프로퍼티 값을 수정자 메소드의 파라미터 타입을 참고해서 적절한 형태로 변환해주기 때문입니다.

<beans>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver.class">
        <property name="url" value="jdbc:mysql://localhost/tobyspring_db">
        <property name="username" value="admin">
        <property name="password" value="admin123">
    </bean>
    <bean id="userDao" class="dao.UserDao">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

정리

IoC는 오브젝트가 생성되고 여타 오브젝트와 관계를 맺는 작업의 제어권을 별도의 오브젝트 팩토리를 만들어서 넘기는 것입니다. 또는 오브젝트 팩토리의 기능을 일반화한 IoC 컨테이너로 넘겨서 오브젝트가 자신이 사용할 대상의 생성인 선택에 관한 책임으로부터 자유롭게 만들어주는 것입니다.

 

IoC 의 핵심 기능인 DI(의존관계 주입)은 설계 시점과 코드에는 클래스와 인터페이스 사이의 느슨한 의존관계만 만들어놓고, 런타임 시에 실제 사용할 구체적인 의존 오브젝트를 제3자(DI 컨테이너)의 도움으로 주입받아서 다이나믹한 의존관계를 가지도록 하는 것입니다.

 

DI 컨테이너가 관리할 빈 객체에 대한 설정들은 java 코드 또는 xml을 통해 만들 수 있으며, 의존 오브젝트를 주입할 때는 생성자 또는 수정자 메소드를 사용할 수 있습니다.

 

💡주요 용어 정리

✔️빈(= 빈 오브젝트)
스프링이 직접 생성과 제어를 담당하는 IoC 방식으로 관리되는 오브젝트를 말합니다.


✔️빈 팩토리
스프링의 IoC(Inversion of Control)를 담당하는 핵심 컨테이너로, 빈을 등록하고 생성하고 조회하고 반환하고 그 외에 부가적인 빈을 관리하는 기능을 담당합니다.

✔️애플리케이션 컨텍스트
빈 팩토리를 확장한 IoC 컨테이너입니다. 빈을 등록, 관리하는 빈 팩토리의 기능과 더불어 스프링이 제공하는 애플리케이션 기능을 모두 포함한 것을 말합니다.

✔️설정 정보 / 설정 메타 정보
애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말합니다. 주로 IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용됩니다. @Configuration 애노테이션을 사용한 java 클래스로 만들거나 xml 파일로 작성할 수 있습니다.

✔️컨테이너 또는 IoC 컨테이너
IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 합니다. 하나의 애플리케이션에 ApplicationContext 오브젝트가 여러 개 만들어져 사용될 수 있는데, 이를 통틀어서 스프링 컨테이너라고 합니다.

✔️스프링 프레임워크
IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 사용합니다.
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함