본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

템플릿 콜백 패턴으로 가독성 높이기

by 민휘 2023. 3. 13.

전략 패턴을 사용해서 변경되지 않는 코드를 컨텍스트에 두고, 변경되는 코드는 인터페이스를 두어 전략 클래스를 구현했다.

클라이언트는 전략 클래스를 생성해 컨텍스트에게 주입하여 사용했다.

이 방법, 다 좋은데.. dao 메소드마다 새로운 구현 클래스를 만드니 클래스 개수가 많아져 번잡스럽다. 어차피 한번 쓰고 말건데!

또 클래스를 분리하니 User와 같이 외부 정보가 필요할 때 매개변수를 받아야 한다. 이 문제를 한번 개선해보자.

 

 

익명 내부 클래스로 개선하기

그렇다면 익명 내부 클래스를 사용하자! Dao 안에서 StatementStrategy(전략 인터페이스)를 구현할 수 있으니, 메소드마다 클래스 파일을 따로 만들지 않아도 된다. 필요한 정보가 있으면 내부 필드로 바로 접근 가능하므로 매개변수가 필요하지 않다.

이와중에 StatementStrategy가 함수형 인터페이스이니 람다로 간단하게 표현할 수 있다. 더 깔끔한 코드 완성!

public void deleteAll() throws SQLException {
    StatementStrategy st = c -> c.prepareStatement("delete from users");
    jdbcContextWithStatementStrategy(st);
}

public void add(User user) throws SQLException {
    StatementStrategy addStatementStrategy = 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;
    };

    jdbcContextWithStatementStrategy(addStatementStrategy);
}

 

 

클래스로 추출하기

많이 개선됐지만 아쉬운 부분이 있다. 컨텍스트는 여러 Dao에서 재사용될 수 있음에도 UserDao 안에 있어서 다른 Dao가 추가됐을 때 재사용할 수 없다. 클래스로 추출하자.

public class JdbcContext {

    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에 의존하도록 하자.

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

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

        // ...
}

 

JdbcContext의 수정자 주입은 xml 설정 파일에서 한다.

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

 

 

콜백 재활용하기

아직 조금 아쉬운 부분이 있다. deleteAll()을 보면 치환자도 없고 반환값도 없는 sql문을 실행하고 있는데, 이런 형태의 sql을 실행하는 메소드가 여러개 추가된다고 생각해보자. 완전히 동일한 역할을 하는데 매번 람다식으로 StatementStrategy를 생성해서 넘겨주기보다는, sql 문자열만 넘겨주고 알아서 로직을 실행하는 것이 더 깔끔하고 중복 없는 방법일 것이다. 분리한 콜백 안에서 동일하게 나타내는 부분을 다시 추출해서 템플릿에 추가하자.

public class JdbcContext {

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        // ...
    }

    public void executeSQL(String sql) throws SQLException {
        workWithStatementStrategy(c -> c.prepareStatement(sql));
    }
}

 

중복되는 StatementStrategy의 생성을 템플릿으로 넘겼다. 이제 치환자가 없고 반환값이 없는 모든 sql은 컨텍스트에 있는 executeSQL에 sql 문자열만 넘겨서 실행할 수 있다.

public void deleteAll() throws SQLException {
    jdbcContext.executeSQL("delete from users");
}

한결 깔끔하고 확장에 유리해진 dao 메소드가 완성됐다.