Today I Learned_230626

3 분 소요

토비의 스프링 3.1

컨텍스트와 DI

JdbcContext의 분리

전략 패턴의 구조로 보자면..

  • UserDao의 add, deleteAll등의 메소드 : 클라이언트
  • 익명 내부 클래스 : 전략
  • jdbcContextWithStatementStrategy() : 컨텍스트
    • 컨텍스트를 UserDao에서만 사용하고 있었지만, 다른 Dao에서도 사용할 수 있게 분리시켜보자

UserDao의 jdbcContextWithStatementStrategy()를 별도의 클래스로 분리

package com.example.demo3.dao;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcContext {

    // DataSource타입 빈을 DI
    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에서 JdbcContext를 DI받아서 사용할 수 있도록 수정

private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }

    public void add(final User user) throws SQLException { //예외는 메소드 밖으로 던지기
        this.jdbcContext.workWithStatementStrategy(
                new StatementStrategy() { (이하 생략)

JdbcContext는 구체 클래스에 해당하지만 변경될 일이 거의 없다. UserDao는 JdbcContext에 의존적이 되었다.

기존에는 UserDao가 DataSource를 직접 의존했다면, 중간에 JdbcContext가 껴서 UserDao → JdbcContext → DataSource 순으로 의존한다.

의존관계 변경을 위한 XML 설정파일 수정

<bean id="jdbcContext" class="com.example.demo3.dao.JdbcContext">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="userDao" class="com.example.demo3.dao.UserDao">
    <property name="dataSource" ref="dataSource"/>  <!-- 이게 빠지고 -->
    <property name="jdbcContext" ref="jdbcContext"/>    <!-- 이게 들어온다 -->
</bean>

UserDao와 JdbcContext간에는 별도의 인터페이스가 없다. 인터페이스 없이 클래스 레벨에서 의존관계를 결정해 버린 셈이다. 인터페이스를 사이에 두지 않았으니 DI가 아니지 않을까? → 객체의 생성과 관계설정에 대한 제어권을 외부로 위임했기에 IoC 개념을 포괄하고 있고, DI의 기본을 따르고 있다고 할 수 있다.

JdbcContext를 UserDao와 DI구조로 만들어야 할 이유

  • 싱글톤 빈이 되기 때문
    • 읽기 전용
    • 컨텍스트 메소드를 제공해 주는 일종의 서비스 오브젝트로서의 이미
  • DI를 통해 다른 빈에 의존
    • DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록되어 있어야 함
    • 다른 빈을 DI받기 위해서라도 빈으로 등록되어야 함

두 빈 사이에 인터페이스가 없다 → 긴밀한 관계를 가지고 있다 → 강한 응집도를 가지고 있다

하지만 싱글톤으로 만들 필요가 있고 DI 필요성이 있기에 스프링 빈에 등록하여 DI되도록 만들었다.

코드를 이용하는 수동 DI

싱글톤은 포기. 대신 DAO마다 하나의 JdbcContext 오브젝트를 갖고 있게 하자.

스프링 빈으로 등록하지 않았기에 누군가가 생성과 초기화의 책임을 져야 한다. 자신이 사용할 오브젝트를 직접 만들고 초기화하는 방법을 사용한다.

→ UserDao에게 DI까지 맡긴다!

Untitled

위와 같이 의존관계가 바뀐다.

JdbcContext 빈 제거

<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
        <property name="username" value="root"/>
    </bean>
    <bean id="userDao" class="com.example.demo3.dao.UserDao">
        <property name="dataSource" ref="dataSource"/>
     </bean>

UserDao의 생성자로 JdbcContext 설정

private DataSource dataSource;
    private JdbcContext jdbcContext;

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

수정자 메소드에서 다른 오브젝트를 초기화 하고 코드를 이용해 DI하는 것의 장점

→ 굳이 인터페이스를 두지 않아도 될 만큼 긴밀한 관계의 클래스를 따로 빈으로 분리하지 않고 DI 적용

빈으로 등록하여 DI

  • 실제 의존관계가 설정파일에 명확하게 드러난다
  • DI의 근본적인 원칙에 부합하지 않은 클래스간의 관계도 노출된다

코드를 이용해 수동으로 DI

  • 관계를 외부에 드러내지 않는다
  • 싱글톤으로 만들 수 없고, 부가적인 코드가 필요하다.

상황에 따라 적절하다고 판단하는 방법 선택! 자신 없으면 인터페이스를 만들어서 평범한 DI 구조로 만들자.

템플릿과 콜백

템플릿/콜백 패턴 : 알고리즘의 일부 단계를 사용자가 정의하는 방식

보통 단일 메소드 인터페이스를 사용한다.

하나의 템플릿에서 여러 종류의 전략 → 여러 콜백 오브젝트 사용

템플릿

  • 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
  • 전략 패턴의 컨텍스트
  • 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우

콜백

  • 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
  • 파라미터로 전달되지만 특정 로직을 담은 메소드를 실행시키기 위해 사용
  • 익명 내부 클래스로 만들어지는 오브젝트
  • 콜백 인터페이스의 메소드에는 파라미터가 있다 → 컨텍스트 정보를 전달받을때 사용

템플릿/콜백의 작업 흐름

Untitled

  • 클라이언트의 역할 : 실행 로직을 담은 콜백 오브젝트 생성, 참조 정보 제공
  • 템플릿 : 참조 정보를 통해 콜백 메소드 호출
  • 콜백 : 클라이언트 메소드에 있는 정보와 참조정보를 이용해 작업을 하고 결과를 템플릿에 돌려줌
  • 템플릿인 콜백이 돌려준 정보를 사용하여 작업을 마저 수행

  • 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는다
  • 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조

→ 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법

콜백의 재활용

템플릿/콜백 방식을 사용하면 클라이언트 메소드는 간결해지고 최소한의 데이터 액세스 로직만 갖고 있게 된다.

하지만 매번 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 불편해진다.

public void deleteAll() throws SQLException {
        this.jdbcContext.workWithStatementStrategy(
                new StatementStrategy() {
                    @Override
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        PreparedStatement ps = c.prepareStatement("delete from users");
                        return ps;
                    }
                }
        );
    }

여기서 ps를 만드는 부분만 빼고 나머지는 반복된다 → 자주 바뀌지 않는 부분을 분리하자.

private void deleteAll() throws SQLException {
        executeSql("delete from users");
    }

public void executeSql(final String query) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(
                new StatementStrategy() {
                    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                        return c.prepareStatement(query);
                    }
                }
        );
    }

변하지 않는 부분 콜백 클래스 정의와 오브젝트 생성 부분을 분리하였다.

카테고리:

업데이트: