본문 바로가기
Spring/Toby's Spring Reading Club

1-3장. 템플릿

by Soono991 2023. 1. 7.

템플릿이란 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경하는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.
(p209)

3장에서는 초난감 DAO에 "예외 처리"를 추가하면서 템플릿의 필요성에 대해 설명합니다.

초난감 DAO에 예외 처리를 하기 위해서 try-catch-finally 구문을 사용합니다.

Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;

User user = null;

try {
    c = connectionMaker.makeConnection();
    ps = c.prepareStatement(
            "select * from users where id = ?"
    );

    ps.setString(1, id);

    rs = ps.executeQuery();


    if (rs.next()) {
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }
} catch (Exception e) {
    throw e;
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
        }
    }
    if (ps != null) {
        try {
            ps.close();
        } catch (SQLException e) {
        }
    }
    if (c != null) {
        try {
            c.close();
        } catch (SQLException e) {
        }
    }
}

예외를 처리하기 위해 catch문을, 그리고 자원을 반납하기 위해 finally 구문을 사용합니다.

이 부분은 다른 쿼리문을 작성해도 동일하게 적용되어야 하는 부분이기 때문에 반복되는 부분이라고 볼 수 있습니다.

하지만 현재 상황에서는 쿼리문마다 catch, finally 문을 작성해야 하기 때문에 코드의 중복이 발생하게 됩니다.

중복되는 부분은 DB 관련 객체들의 선언 부분과, catch, finally 부분입니다.

이 경우 "템플릿 메서드 패턴"을 사용하여 해결 할 수 있습니다.

public abstract class UserDao {

    private final ConnectionMaker connectionMaker;

    public UserDao() {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
    }
    abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;

}
@Component
public class UserDaoDeleteAll extends UserDao {
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}

하지만 템플릿 메소드 패턴은 위와 같이 추상 클래스 상속을 사용하기 때문에 각 기능을 구현하는 클래스마다 UserDao를 상속받아야 하기 때문에 객체 구조에서 유연성이 크게 떨어지게 됩니다.

템플릿 메소드 패턴의 단점을 인터페이스를 사용하는 전략 패턴을 사용하여 해결할 수 있습니다.

개방 폐쇄 원칙(OCP)를 잘 지키는 구조이면서도 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.
(p.219)

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
public class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}
public class UserDao {

    private final ConnectionMaker connectionMaker;

    public UserDao() {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
    }

    public void deleteAll() throws SQLException, ClassNotFoundException {
        StatementStrategy strategy = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(strategy);
    }
    public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException, ClassNotFoundException {
        Connection c = null;
        PreparedStatement ps = null;
        try {
            c = connectionMaker.makeConnection();

            ps = stmt.makePreparedStatement(c);

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

실제 바뀌는 부분을 인터페이스의 메소드에 각 기능별로 구현하고 해당 객체들을 공통 로직이 들어있는 jdbcContextWithStatementStrategy 메소드에 전달하여 보다 유연하게 코드를 분리할 수 있게 됩니다.

로컬 클래스

하지만 기능별로 매번 새로운 구현 클래스를 만들게 되면 불필요한 클래스 파일들이 늘어나게 됩니다. 위 경우처럼 구현 클래스들이 사용되는 곳이 한 곳이라면 별도의 클래스로 분리하기보다는 내부 클래스로 만드는 방법도 있습니다.

public void add(final User user) throws ClassNotFoundException, SQLException {
    class AddStatement implements StatementStrategy {
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            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());

            ps.executeUpdate();

            return ps;
        }
    }

    StatementStrategy stmt = new AddStatement();
    jdbcContextWithStatementStrategy(stmt);
}

사용되는 곳이 한 곳이라는 가정하에 작성된 것이기 때문에 당연히 재사용은 되지 않지만,

내부 클래스를 사용하게 되면 add 메소드에 전달된 user 객체를 내부 클래스인 AddStatement에서 바로 참조할 수 있다는 장점이 있습니다. (별도의 파라미터를 전달할 필요가 없음.)

그리고 AddStatement는 한 번 사용되기 때문에 따로 클래스를 선언하지 않고 익명 내부 클래스로도 변경할 수 있습니다.

public void add(final User user) throws ClassNotFoundException, SQLException {
    StatementStrategy stmt = new StatementStrategy() {
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            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());

            ps.executeUpdate();

            return ps;
        }
    };

    jdbcContextWithStatementStrategy(stmt);
}

그리고 익명 내부 클래스는 람다식으로도 변경 할 수 있습니다.

public void add(final User user) throws ClassNotFoundException, 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());

        ps.executeUpdate();

        return ps;
    };

    jdbcContextWithStatementStrategy(stmt);
}

이번 3장에서는 바로 이 리팩토링 과정이 가장 좋았던 부분입니다.

실제 사용에서는 당연스럽게도 익명 내부 클래스, 또는 람다식을 전달하는 코드였기 때문에 "왜" 이렇게 코드를 작성해야 하는지에 대해 의문이 있었는데, 이렇게 직접 리팩토링을 하니 단순히 글로 보는 것보다 훨씬 더 와닿았습니다.

리팩토링 한 코드는 전략 패턴과 익명 내부 클래스를 활용한 방식입니다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부릅니다.

전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부릅니다.
(p.241)

템플릿/콜백의 특징

여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메서드 인터페이스를 사용합니다. (때문에 람다식이 가능)

템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문입니다.

스프링에 내장된 것을 원리도 알지 못한 채로 기계적으로 사용하는 경우와 적용된 패턴을 이해하고 사용하는 경우는 큰 차이가 있다.

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자.
(p.248)

'Spring > Toby's Spring Reading Club' 카테고리의 다른 글

1-5장. 서비스 추상화  (0) 2023.01.08
1-4장. 예외  (0) 2023.01.08
1-2장. 테스트  (0) 2022.09.19
1-1장. 오브젝트와 의존관계  (0) 2022.09.05
토비의 스프링 3.1 시작하기  (0) 2022.09.04

댓글