토비의 스프링 3.1 - 3장_템플릿

Roeniss Moon·2020년 6월 24일
1

토비의 스프링 3.1

목록 보기
4/6
post-thumbnail

예외 처리의 필요성 (리소스 반환의 실패)

현재 UserDao.deleteAll()의 모양은 이러하다.

public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();
    PreparedStatement ps = c.prepareStatement("DELETE FROM users");
    
    ps.executeUpdate();

    ps.close(); // close() 실패 시 일어나는 에러도 대충 SQLException으로 퉁쳐져 있음
    c.close();
}

이것을 close()를 체크하는 방식으로 바꿔보자.


public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        ps = c.prepareStatement("DELETE FROM users");
        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (ps != null)
            try {
                ps.close();
            } catch (SQLException e) {
                // 이제 여기서 ps 리소스 반환 실패를 잡을 수 있다
            }
        if (c != null) 
            try {
                c.close();
            } catch (SQLException e) {
                // 이제 여기서 c 리소스 반환 실패를 잡을 수 있다
                // (이런 방법으로 처리 가능하다, 까지만 다룸)
            }
        // ResultSet을 사용하는 메소드는 resultSet도 같은 방식으로 처리해주어야 함 (ex. getCount)
    }
}

이러한 방식의 문제점은, 모든 메소드마다 try/catch/finally 구문이 반복된다는 것. 리팩토링을 해보자.

🖐 여기서 잠깐!

  • 만들어진 순서의 역순으로 리소스 반환하는 것이 원칙이다.

  • finally는 return 후에도 작동한다.

디자인패턴을 적용하여 분리 & 재사용

현재 반복되는 부분(변하지 않는 부분)은 리소스 호출 & 리소스 반환 파트.

방법 1. 변하는 부분을 메소드로 추출

"변하지 않는 부분이 변하는 부분을 감싸고 있어서 변하지 않는 부분을 추출하기가 어려워 보이기 때문에 반대로 해봤다"

// 이 메소드를 기존 ps 선언 부분에서 사용함
private PreparedStatement makeStatement(Connection c) throws SQLException{
    PreparedStatement ps;
    ps = c.prepareStatement("delete from user");
    return ps;
}

일단 바뀌는 부분과 바뀌지 않는 부분이 분리는 되었으나, 매우 구림. 모든 메소드에서 똑같이 변하지 않는 부분이 반복되고, 변하는 부분은 매번 새로 만들어주기 때문에 아무것도 좋아지지 않음.

방법 2. 템플릿 메소드 패턴 적용

"변하지 않는 부분(try/catch/finally)은 슈퍼클래스에 두고 변하는 부분(statement)은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것"

package springbook.user.dao;
// ...
public abstract class UserDao {
    // ...
    // executeDao()같은 메소드를 두고, 매번 반복되는 try~finally 구문을 기록하고
    // 그 사이에 이 아래 makeStatement()를 끼워 넣어둔다
    
    public abstract PreparedStatement makeStatement(Connection c) throws SQLException;
    
}
package springbook.user.dao;
// ...
public class UserDaoDeleteAll extends UserDao{
    private PreparedStatement makeStatement(Connection c) throws SQLException{
        PreparedStatement ps;
        ps = c.prepareStatement("delete from user");
        return ps;
    }
}

바뀌는 부분은 makeStatement() 뿐이고, 새로 메소드를 만들때 봐야하는 부분도 이 부분 뿐이다. 실제 호출은 책에 예제는 없지만 이런 느낌일 것이다.

UserDaoDeleteAll userDaoDeleteAll = new UserDaoDeleteAll();
userDaoDeleteAll.executeDao();

// 또 다른 메소드에 대응하는 서브클래스
UserDaoAdd userDaoAdd = new UserDaoAdd();
UserDaoAdd .exeucteDao(); // 단순한 예시를 위해 User 파라미터 생략..

두 가지 문제점이 있다.

  • 모든 DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 된다.

  • 컴파일 시점에 클래스 간(슈퍼-서브) 관계가 결정되어 있어서 유연하지 못하다.

방법 3. 전략 패턴 적용

"변하는 부분을, 아예 별도의 클래스로 만들어 추상화된 인터페이스를 통해 소통하도록 구성한다"

deleteAll()의 컨텍스트(변하지 않는 부분) :

  • DB 커넥션 획득

  • PreparedStatement 생성

  • PreparedStatement 실행

  • 예외는 throws

  • 모든 리소스 반환 (PreparedStatement, Connection, ResultSet)

🤔 사견 : ResultSet은 상황의 단순화를 위해 예시에 잘 나오지 않음. 일부러 ResultSet이 없는 deleteAll을 고른 듯.

package springbook.user.dao;
// ...
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
package springbook.user.dao;
// ...
public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = null;
        ps = c.prepareStatement("DELETE FROM users");
        return ps;
    }
}
package springbook.user.dao;
// ...
public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        // 이젠 이 아래 한 줄만 메서드에 따라서 바꾸면 된다.
        StatementStrategy stmt = new DeleteAllStatement();
        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 안에서, StatementStrategy를 상속한 클래스들을 이용해 객체를 만들고 똑같이 ps = stmt.makePreparedStatement(c) 구문으로 해결할 수 있다.

그런데 이렇게 되면 컨텍스트(UserDao)가 무엇을 실행할 지 컴파일 시점에 알고 있다. 템플릿 메소드 패턴과 동일한 문제를 가진 것이다.

이를 해결하기 위해, Context를 사용하는 Client가 전략을 선택하도록 수정하자.

즉, 전략 패턴은 다음과 같은 세 요소를 지니고 있다.

  • 여러 전략은 하나의 (전략) 인터페이스를 implement한다.

  • 컨텍스트는 이 각각의 전략들과 '인터페이스를 매개로' 느슨하게 연결되어 있기 때문에 어떤 전략이든 사용할 수 있다.

  • 클라이언트는 특정 전략을 선택하고 생성하고 컨텍스트에게 전달한다.

이미 1장(83p)에서 같은 결론에 도달한 적이 있다. UserDao가 DConnectionMaker를 본인 메소드 안에서 새로 생성하는 것이 문제가 되어서,

  • UserDaoTest에서 DConnectionMaker / NConnectionMaker (특정 전략들)를 생성하고

  • UserDao를 생성할 때 (생성자 DI 주입 방식으로) 특정 전략을 제공하고

  • UserDao는 이 주입받은 전략을 이용하였다. 이용할 때는 ConnectionMaker 타입으로 사용했기 때문에, 어떤 전략이든 문제없이 실행할 수 있었다.

이러한 관계, 즉 "전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 하나의 책임"을 통째로 드러내고 사용하는 방식이 DI며 전략 패턴을 잘 살리는 방식이라고 말할 수 있다.

🤔 사견 : 전략 패턴을 잘 유도하는 것이 DI의 장점이고, 스프링이 DI를 적극적으로 도와주는 프레임워크라면, 스프링은 전략 패턴이 거의 Primary Strategy라고 생각해도 되는걸까?

아무튼 deleteAll을 하나의 Client로 본다면, 우리는 반복되는 부분을 Context로 뽑아내고, 거기에 전략을 주입해볼 수 있다.

여기서 이해하는 데 시간을 많이 잡아 먹었다.
1장에서 습득한 것은 UserDaoTest-UserDao-Datasource가 Client-Context-Strategy의 전략 패턴 구조인데, 지금은 UserDao의 내부 메소드가 또다른 전략 패턴 구조를 만들어가고 있다.

deleteAll-PreparedStatement가 Context-Strategy의 구조를 보이고 있는 상태이며, 이제부터 할 것은 deleteAll-jdbc-statement를 Client-Context-Strategy 형태로 바꾸어 나가는 작업이다.

참고로 아까부터 계속 PreparedStatement랑 Statement, statement를 혼용해서 쓰고 있는데 다 같은 거라고 이해하시면 되겠다.

jdbc와 관련된 (리소스 요청, 반납 등의) 작업들을 Context라고 본다면, 아래와 같은 3단 전략 패턴 구조를 만들어 볼 수 있다.

// Client
public void deleteAll() throws SQLException {
    StatementStrategy stmt = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(stmt);
}

// Context
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
    // SQL 쿼리(PreparedStatement)와는 결합도가 낮은 "JDBC 작업 흐름"을 분리해 냄
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        ps = stmt.makeStatement(c); // 이 부분에서 strategy 사용

        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) {
        }
    }
}

// Strategy (이전과 바뀐 것 없음)
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

// ...

public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = null;
        ps = c.prepareStatement("DELETE FROM users");
        // add() 같은 경우엔 여기에 ps.setString()같은 추가 작업이 필요함 (아래에서 추가 설명)
        return ps;
    }
}

Strategy in Client

3.3.2 챕터부터 진행되는 내용으로, 여기의 소제목은 '전략과 클라이언트의 동거'이다.

위 코드의 문제점 두 가지.

  • 모든 DAO 메소드마다 새로운 Strategy 구현 클래스를 만든다.

  • 부가정보를 전달하려면, Strategy 구현체에 이것저것(생성자와 인스턴스 변수 코드) 손이 많이 간다. 예를 들어 add() 메소드는 User 정보를 Strategy 구현체에 추가해 주어야 한다.

🖐 잠깐! 중첩 클래스(nested class)의 분류

  1. 스태틱 클래스
  2. 내부 클래스
    a. (멤버) 내부 클래스 : 스코프가 클래스에 걸림
    b. 로컬 클래스 : 스코프에 메소드에 걸림
    c. 익명 내부 클래스 : 어디에 선언했는지에 따라 스코프가 다름

로컬 클래스

어차피 각 전략은 각 메소드에서만 쓰이니까, 로컬 클래스로 선언해볼 수 있다.

익명 내부 클래스

🤔 사견 : 사실 이걸 말하고 싶었던 듯 하다...

⚠️ 주의! 익명 내부 클래스를 사용할 때는...

  • 클래스 밖의 변수는 final 키워드가 붙어있어야만 쓸 수 있다.

아래는 Client와 Strategy가 한 몸이 된 모습니다.

// Client (원래 여기 있었음)
public void deleteAll() throws SQLException {
    // Context (선언은 분리되어 있음..)
    jdbcContextWithStatementStrategy(
        // Strategy (선언 따로 없음)
        new StatementStrategy() {
            @Override
            public PreparedStatement makeStatement(Connection c) throws SQLException {
                return c.prepareStatement("DELETE FROM users");
            }
        }
    );
}

Context 분리하고 DI

jdbcContextWithStatementStrategy()UserDao 외에도 적용할 수 있으므로 별도 클래스로 분리할 수 있다. 분리하면서 'JdbcContext 클래스의 workWithStatementStrategy() 메소드'로 이름도 바뀌었다.

그런데 이때, 한 가지 새로운(?) 문제가 발생한다. 바로 dataSource다.

jdbcContext 메소드는 지금까지 UserDao 내부에 위치했다. 따라서 그냥, UserDao의 인스턴스 변수 dataSource를 사용하면 되었다. 이 변수는 현재 UserDaoTest 클래스의 @ContextConfiguration(locations = "/test-applicationContext.xml") 어노테이션에 의해 xml 파일에서 주입받고 있으며, 생성자 DI 방식을 사용하고 있다.

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

그런데 클래스를 별도로 두면, 어떤 식으로는 jdbcContext에 dataSource를 전달해주어야 한다. 이때 컴파일 단계의 의존관계는 다음과 같다. (오른쪽이 왼쪽에 의존하고 있음)

dataSource(Interface) <-- jdbcContext(Class) <-- UserDao(Class)

그런데, UserDao는 구체 클래스에 의존하고 있다. "스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스(예를 들면 dataSource 구현체인 SimpleDriverDataSource)를 바꿔서 사용하도록 하는 게 목적이다" 라고 책에 써있고 그 직후 "하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 '서비스 오브젝트'로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 거의 없다"라고 하고 있다. 일단 그냥 넘어가자 라고 읽으면 되는거같다

아무튼 먼저 xml을 이용해 의존성을 주입해보자.

방법 1. 빈

<!-- ... -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <!-- DB 접속 정보 properties -->  
    </bean>

    <!-- datasource가 jdbcContext에 주입됨 -->
    <bean id="jdbcContext" class="springbook.user.dao.JdbcContext">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- jdbcContext가 userDao에 주입됨 -->
    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="jdbcContext" ref="jdbcContext" />
        <!-- dataSource를 안지우는 것은, 아직 다른 메소드들 리팩토링이 덜 끝나서 -->
        <property name="dataSource" ref="dataSource" />
    </bean>
<!-- ... -->

jdbcContext 구체 클래스를 빈으로 등록하는 세 가지 이유가 있다.

  1. 싱글톤이 되기에 충분한 조건을 갖추고 있음 - 따라서 공유자원으로 활용하면 좋음

  2. UserDao에서 사용하고 있음 - "DI를 위해서는 주입되는/주입하는 두 오브젝트가 모두 스프링 빈으로 등록돼야 한다

  3. 두 오브젝트 사이의 실제 의존관계가 설정 파일에 명확하게 드러난다.

하지만 DI의 근본적인 원칙에 부합하지 않는다. 즉, 구체적인 클래스 간 관계가 컴파일 단에 노출된다.

방법 2. 수동 DI

각 DAO 마다 JdbcContext 클래스 하나를 보유하고 해당 DAO 내부에서 돌려쓰는 것 정도를 용인할 수 있다면, 이 방법을 시도할 수 있다. 이는 UserDao가 JdbcContext에 대한 제어권(생성, 초기화(의존성 주입), 사용)을 가진다는 말이다.

코드로는 JdbcContext 내부에 setDataSource 메소드를 두고, UserDao 생성자를 다음과 같이 수정하여 구현한다.

public class UserDao {
    JdbcContext jdbcContext;
    DataSource dataSource;

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

이 방법의 장점 :

  1. 굳이 인터페이스도 없는 '긴밀한 관계'를 같은 두 객체를 '어색하게' 빈으로 분리하지 않는다.

  2. DI는 나름대로 은밀하게 구색을 갖추었다.

  3. 2번 덕분에, 두 클래스 사이 관계가 노출되지 않는다.

하지만 싱글톤으로 사용이 불가하고, DI를 위한 추가 코드가 발생했다는 단점이 있다.

템플릿/콜백 패턴

개념

🤔 사견 : Javascript에서는 함수가 일급 객체다. 따라서 콜백 함수를 별다른 문제없이 바로 구현할 수 있다. 콜백이 필요한 자리에 그냥 function을 던진다는 소리다.
그런데 놀랍게도 자바는 함수를 파라미터로 전달할 수 없다고 한다. 그러면 당연히, function을 감싼 class를 전달해야 할 것이다.

이제부터 나올 템플릿/콜백 패턴은 자바 스타일의 콜백 함수 사용법이라고 말 할 수 있을 것 같다.

템플릿/콜백 패턴에서,

  • 템플릿 : 전략 패턴의 Context

  • 콜백 : 익명 내부 클래스로 구현된 오브젝트. 일반적으로 단일 메소드 '인터페이스'를 지닌다.

고유한 특징으로는, 자신을 호출한 Client의 정보를 직접 참조한다는 것. (final 변수들을 가져다 쓸 수 있다)

왜 인터페이스인가 : 틀은 그대로 두면서 값이나 구현을 즉석에서 '바꾸기' 위해서. 구현체면 바꿀 수 없으니까!
(엄청 당연한건데 아직 자바가 익숙치 않아서 적음...)

사실 이 패턴은 이미 위에서 익명 내부 클래스 예시로 등장했다.

이 패턴은 "메소드 레벨에서 일어나는 DI"라고 할 수 있다. Context가 무슨 메서드를 쓸 것인지를, Client의 메서드 안에서, 바로 짜내어서 삽입하는 느낌으로 말이다.

이 패턴을, "전략패턴과 수동 DI"의 결합체라고 이해할 수 있다.

Javascript에서의 callback은 asyncronous다. 즉, caller는 callback이 언제 어디서 실행될지 알지 못하고 알 필요도 없다. Java는 그와 다르다. 결과를 클라이언트가 받아야만 한다.

  1. 클라이언트가 템플릿에 콜백을 전달하고,
  2. 템플릿은 자기 로직 실행 후 콜백을 실행한다.
  3. 콜백 내부에서 리턴된 값은 템플릿으로 돌아가고,
  4. 템플릿은 자기 로직 나머지를 수행한 후, 최종 결과를 클라이언트에 전달한다.

그리고 여기에 더해, 클라이언트 메소드 안에서 콜백 함수가 구현된다는 특징까지,가 템플릿/콜백 패턴이라고 말할 수 있을 것이다.

사칙연산으로 이해하기

책은 이 장면에서 갑자기 사칙연산을 이용해 리팩토링 with 템플릿/콜백 패턴을 진행하기 시작한다. 요약하자면,

  1. 성공을 전제로 하는 테스트 코드를 미리 작성하고 출발

  2. (Client) BufferedReader를 열고 닫는 과정 때문에 try~finally가 반복됨 (데자부..)

  3. (Callback) BufferedReaderCallback 이라는 인터페이스를 만들고, 그 안에 Integer doSomethingWithReader(BufferedReader br) 메소드를 선언

  4. (Template) try~finally 부분을 통째로 빼서, Integer fileReadTemplate(String filepath, BufferedReaderCallback cb)라는 메소드로 분리

  5. 아래와 같은 결과물. filepath는 numbers.txt인데, Test 코드에서 전달받았고 파일에는 숫자 4개가 한 줄에 하나씩 쓰여있다.

public Integer calcSum(String filepath) throws IOException {
    BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
        @Override
        public Integer doSomethingWithBufferedReader(BufferedReader br) throws IOException {
            String line = null;
            Integer sum = 0;
            while ((line = br.readLine()) != null) {
                sum += Integer.valueOf(line);
            }
            return sum;
        }
    };
    // fileReadTemplate의 자세한 코드는 생략한다.
    return fileReadTemplate(filepath, sumCallback);
    
    /* 좀 더 줄이면 아래와 같다.
    return fileReadTemplate(filepath, new BufferedReaderCallback() {
        @Override
        public Integer doSomethingWithBufferedReader(BufferedReader br) throws IOException {
            String line = null;
            Integer sum = 0;
            while ((line = br.readLine()) != null) {
                sum += Integer.valueOf(line);
            }
            return sum;
        }
    });*/
}
  1. 그런데 위 코드는 덧셈의 경우이고, 곱셈의 경우에는 Integer sum = 0, sum *= Integer.valudOf(line)으로 바꾸는 걸로 충분하다. 즉, 아직도 변하지 않는 부분이 변하는 부분 위아래로 조금씩 붙어있다. 따라서 이를 살짝 개량한다.
public Integer calcSum(String filepath) throws IOException {
    LineCallback callback = new LineCallback<Integer>() {
        @Override
        public Integer doSomethingWithLine(String line, Integer value) {
            return value + Integer.valueOf(line);
        }
    };
    return lineReadTemplate(filepath, callback, 0); // 곱셈의 경우에는 0 대신 1
}

private Integer lineReadTemplate(String filepath, LineCallback callback, Integer initVal) throws IOException {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(filepath));
        T res = initVal;
        String line = null;
        while ((line = br.readLine()) != null) {
            res = callback.doSomethingWithLine(line, res); // 이 한 줄로 콜백이 압축됨
        }
        return res;
    } catch (IOException e) {
        System.out.println(e.getMessage());
        throw e;
    } finally {
        if (br != null) try {
            br.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
  1. 제네릭을 이용해서 Integer가 아니어도 작동하도록 여기저기 수정. 예를 들면 lineReadTemplate의 시그니처가 private <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException로 바뀌었다.

스프링의 템플릿/콜백 기술들

스프링은 JDBC를 이용하는 DAO에서 사용할 수 있도록 다양한 템플릿, 콜백을 제공한다. 여기서는 JdbcTemplate 클래스를 사용한다.

  • update()

  • queryForInt()

  • queryForObject()

  • query()

... 정도가 언급된 것 같은데, 내부 기술을 이해하는 게 더 중요하니 자세한 언급은 하지 않겠다. 다만 여기서 한 가지 예시를 짚고 가자면,

public class UserDao {
    private JdbcContext jdbcContext;
    private JdbcTemplate jdbcTemplate;
    
    // 생성자 코드 생략
    
    // 자주 쓰는 콜백(익명 내부 클래스)를 이렇게 미리 생성해 둘 수 있다
    private RowMapper<User> userMapper =
        new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password"));
                user.setUnique_id(rs.getString("unique_id"));
                return user;
            }
        };
        
    public void add(final User user) throws SQLException {
        this.jdbcTemplate.update("INSERT INTO users(id, name, password) VALUES(?, ?, ?)",
                user.getId(), user.getName(), user.getPassword());
    }
    
    // ...

}

🤔 사견 : 위 익명 내부 클래스는 엄밀히 말하면 스코프가 객체에 걸리기 때문에 기존의 메소드 속에서 선언된 것과 질적으로 다르다고 생각한다. 다만 이렇게 쓸 수 있는 것은 (책에서 언급했다시피) stateless하기 때문이 아닐까.

정리

  • 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리한다

  • 전략 패턴 짱짬맨

  • 중복 코드는 분리하자. 외부에서도 사용될 수 있으면 클래스도 분리하라.

  • 컨텍스트는 빈으로 등록해서 DI 받거나, 수동으로(클라이언트 클래스에서) 직접 컨텍스트를 생성(+ 의존성 주입)하는 두 가지 방법으로 운용할 수 있다.

  • 템플릿/콜백 패턴은 컨텍스트 호출과 동시에 전략 DI를 수행하는 패턴이다.

  • 콜백에서도 일정 패턴이 반복된다면 그것마저 템플릿에 넣고 재활용할 수 있다.

profile
기능이 아니라 버그예요

0개의 댓글