토비의 스프링 정리 프로젝트 #3.4 컨텍스트와 DI

Jake Seo·2021년 7월 20일
0

토비의 스프링

목록 보기
20/29

JdbcContext의 분리

이전의 방식을 전략패턴 구조로 보자면, 다음과 같다.

  • UserDao.deleteAll(), UserDao.add(): 클라이언트
    • 어떤 전략을 사용할지 의존성을 결정
  • 익명 내부 클래스: 전략
    • 구체적인 전략
  • UserDao.jdbcContextWithStatementStrategy(): 컨텍스트
    • PreparedStatement를 실행하는 변하지 않는 부분

JDBC의 일반적인 작업 흐름을 담고있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다. jdbcContextWithStatementStrategy()UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해보자.

클래스 분리

JdbcContext라는 클래스를 생성하여 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 된다.

JdbcContextDataSource에 의존하게 되므로 JdbcContextDataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.

JdbcContext.java

public class JdbcContext {
    private DataSource dataSource;

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

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = stmt.makePreparedStatement(c);

            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
            if(c != null) { try { c.close(); } catch (SQLException e) { } }
        }
    }
}

UserDao.java

public class UserDao {
    ...
    JdbcContext jdbcContext;
    
    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }
    ...
    
    public void add(User user) throws SQLException {
      StatementStrategy stmt = c -> {
          PreparedStatement ps = c.prepareStatement(
                  "insert into users(id, name, password) values (?, ?, ?)"
          );

          ps.setString(1, user.getId());
          ps.setString(2, user.getName());
          ps.setString(3, user.getPassword());

          return ps;
      };

      jdbcContext.workWithStatementStrategy(stmt);
    }
    
    public void deleteAll() throws SQLException {
        StatementStrategy strategy = c -> c.prepareStatement("delete from users"); 
        jdbcContext.workWithStatementStrategy(strategy); 
    }
    
    ...

빈 의존관계 변경

UserDao는 이제 JdbcContext에 의존하고 있다. 그런데, JdbcContext는 인터페이스인 DataSource와 달리 구체 클래스이다.

스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적이다. 하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고, 구현 방법이 바뀔 가능성은 없다.

따라서 인터페이스를 구현하도록 만들지 않았고, UserDaoJdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.

위 그림은 JdbcContext를 적용한 UserDao의 의존관계이다

위 그림은 JdbcContext가 적용된 빈 오브젝트 관계이다.

xml파일은 아래와 같이 변경되면 된다.

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

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

    <bean id="jdbcContext" class="toby_spring.chapter1.user.jdbc_context.JdbcContext">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
        <property name="jdbcContext" ref="jdbcContext" />
    </bean>
</beans>

아직은 UserDao 클래스의 모든 메소드가 jdbcContext를 사용하는 것은 아니라서 dataSource에 대한 의존성도 필요하다.

이쯤에서 테스트를 한번 돌려보면 잘 동작한다.

JdbcContext의 인터페이스 없는 DI

UserDaoJdbcContext사이에는 인터페이스를 사용하지 않고 DI를 적용했다. UserDaoJdbcContext는 클래스 레벨에서 의존관계가 발생한다. 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수는 없다.

인터페이스 없이 스프링 빈으로 DI하면 문제가 있을까?

인터페이스를 사용하지 않는다면 엄밀히 말해서 온전한 DI라고 볼 수는 없다. 하지만 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다.

JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는 건 DI의 기본을 따르고 있다고 볼 수 있다.

JdbcContext를 UserDao와 DI구조로 만들어야 할 이유를 꼽자면 어떤 것이 있을까?

  • 싱글톤 레지스트리에 등록된 싱글톤 빈으로 만든다는 것에 의미가 있다.
    • 많은 DAO에서 사용되더라도 JdbcContext는 1개의 빈으로 관리 가능하다.
    • 변경되는 상태정보가 없기 때문에 서비스 오브젝트로서 의미가 있고 여러 오브젝트에 공유해 사용되는 것이 이상적이다.
  • JdbcContextdataSource라는 다른 빈에 의존해야 하기 때문이다.
    • DI를 위해서 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어야 한다.
    • 다른 빈을 주입받기 위해서라도 스프링 빈에 등록되어야 한다.

인터페이스가 없다는 건 UserDaoJdbcContext 클래스와 강한 결합을 갖고 있다는 의미이다. OOP의 설계 원칙에는 위배되지만, JdbcContext는 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.

이런 경우는 굳이 인터페이스를 두지 않아도 상관 없다.

단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말자.

코드를 이용하는 수동 DI

JdbcContext를 빈으로 등록하지 않고, UserDao 내부에서 직접 DI를 적용할 수도 있다. 이 방법을 쓰려면 JdbcContext를 스프링 빈으로 등록해서 사용했던 첫번째 이유인 싱글톤으로 만드려는 것은 포기해야 한다.

하지만 JdbcContext 자체는 싱글톤이 아니더라도, DAO 객체들은 빈으로 등록되어 싱글톤으로 관리될 것이기 때문에 JdbcContext도 DAO와 1:1로 형성될 것이다. 웬만큼 대형 프로젝트라도 수백개면 충분할 것이다.

UserDao가 직접 JdbcContextDataSource를 DI해주도록 코드를 변경해보자.

public class UserDao {
    DataSource dataSource;
    JdbcContext jdbcContext;

    public UserDao() {
    }

    public UserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

        this.jdbcContext = new JdbcContext();
        jdbcContext.setDataSource(dataSource);
    }
    
    ...
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

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

이 방법의 장점은 굳이 인터페이스를 두지 않아도 될만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하게 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점이다. 이렇게 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI하는 것은 스프링에서도 종종 사용되는 기법이다.

정리

지금까지 JdbcContext와 같이 인터페이스를 사용하지 않고 DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 두가지를 알아보았다.

빈으로 등록하는 방법

  • 장점
    • 의존관계가 설정파일에 명확하게 드러난다.
    • 싱글톤 레지스트리에 등록 가능
  • 단점
    • DI의 근본적 원칙에 부합하지 않는 구체적인 클래스와의 관계가 설정에 직접 노출된다.

수동으로 DI하는 방법

  • 장점
    • 관계를 외부에 드러내진 않는다.
  • 단점
    • 싱글톤 불가능
    • DI작업을 위한 부가적인 코드가 필요하다.

상황에 따라 적절한 방법을 선택해야 하며, 왜 그렇게 선택했는지에 대한 근거가 있어야 한다. 분명하게 설명할 자신이 없다면 차라리 인터페이스를 만들어 평범한 DI 구조로 만드는 게 나을 수도 있다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글