템플릿이란 바뀌는 성질이 다른 코드 중, 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜 효과적으로 활용할 수 있는 방법이다. 복붙신공을 막기 위해서는, 그리고 효율적으로 코드를 관리하기 위해서는 템플릿 패턴을 적용해야 한다.

분리와 재사용을 위한 디자인 패턴 적용

아래와 같이 user를 삭제 처리를 해주는 메소드가 있다 :

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

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

보통 USER에서는 delete기능 외에도 get, insert와 같은 기능이 필요하다. 여기서 메소드에 따라 ps=c.prepareStatement("delete from users");부분만 변경이 되는데, 복붙해서 같은 코드를 여러 메소드에 적용하는 것은 좋지 않은 방식이다. 중복성 개선과 가독성 높은 코드를 위해 점진적으로 코드를 바꿔보자.

메소드 추출

변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 변하는 코드를 분리하는 첫번째 방법은 메소드 추출이다. 우선 위 기능은 변하지 않는 부분이 변하는 부분을 감싸고 있어 변하지 않는 부분을 추출하기가 어렵기 때문에 반대로 적용해보았다.

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

        try {
            c = dataSource.getConnection();
            ps = makeStatement(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) {
                }
            }
        }
    }
private PreparedStatement makeStatement(Connection c) throws SQLException {
	PreparedStatement ps;
	ps = c.prepareStatement("delete from users");
	return ps;
}

자주 바뀌는 부분을 메소드로 독립시켰지만 별 이득이 없다. 보통 메소드 추출 리팩토링을 적용하는 케이스는, 분리시킨 메소드를 다른 곳에서 재사용 할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드는 DAO 로직마다 새로 추가를 해주어야 하기 때문이다. 무언가 반대로 되었다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고, 변하는 부분은 추상메소드로 정의해두어 서브클래스에서 오버라이드해 새롭게 정의해 쓰도록 하는 것이다.
추출해서 별도의 메소드로 독립시킨 makeStatement()메소드는 아래와 같이 추상메소드로 변경하고, 이 메소드를 정의한 UserDao도 추상클래스로 변경한다.

abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;

서브클래스는 이 추상메소드를 구현하도록 한다.

public class UserDaoDeleteAll extends UserDao {

	protected PreparedStatement makeStatement(Connection c) throws SQLException {
		PreparedStatement ps;
		ps = c.prepareStatement("delete from users");
		return ps;
	}
}

템플릿 메소드 패턴을 통해 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장 때문에 기존의 상위 DAO클래스에 불필요한 변화는 생기지 않도록 할 수 있어 OCP를 적용했다고 볼 수 있다. 하지만 DAO로직마다 상속을 통해 새로운 클래스를 만들어야 하며, 확장 구조가 클래스를 설계하는 시점에 고정되어 버리는 단점이 생긴다.

전략 패턴

전략패턴은 OCP를 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어나다. 오브젝트를 아예 둘로 분리하고, 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 하기 때문이다. 전략패턴에서 가지는 각각의 항목은 아래와 같다 :

  • 컨텍스트 : 메소드에서 변하지 않는 부분
  • 전략 : PreparedStatement를 만들어주는 외부 기능

이제 전략 패턴을 적용해보자 :

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

PreparedStatement를 만드는 전략의 인터페이스는 컨텍스트가 만들어둔 Connection을 전달받아 PreparesStatement를 만들고, 만들어진 해당 타입의 오브젝트를 돌려준다. 이제 이 인터페이스를 상속해서 실제 전략, 즉 바뀌는 부분인 PreparedStatement를 생성하는 클래스를 만들어보자.

public class DeleteAllStatement implements StatementStrategy {

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

이제 확장된 전략을 contextMethod()에 해당하는 deleteAll() 메소드에 사용하면 적용되었다고 볼 수 있다.

public void deleteAll() throws SQLException {
       ...
        try {
            c = dataSource.getConnection();
            StatementStrategy strategy = new DeleteAllStatement();
            ps = strategy.makePreparedStatement(c);
            ps.executeUpdate();
           
        } catch (SQLException e) {
            .........
    }

하지만 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있도록 하는 것이 원칙인데, 컨텍스트 안에서 구체적인 전략인 DeleteAllStatement을 결정하도록 하는 것은 OCP에 맞다고 볼 수 없다. 이 문제를 해결하기 위해서 클라이언트와 컨텍스트를 추가적으로 분리해야 한다.

// 아규먼트로 받는 StatementStrategy는 클라이언트가 컨텍스트를 호출할 때 넘겨줄 실제 전략이다.
public void jdbcContextWithStatementStrategy(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) { }
            }
        }
    }

이 메소드는 컨텍스트의 핵심적인 내용을 잘 담고 있다. 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고, JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에 작업을 수행한다.
이제 클라이언트가 실제로 사용해야 할 전략을 만들고, 컨텍스트에 넘겨주는 코드를 작성해보자. 컨텍스트를 별도의 메소드로 분리하였으니 deleteAll()이 클라이언트 메소드가 되고, 이 메소드는 전략 오브젝트를 만들고 컨텍스트에 넘겨줄 책임을 지고 있다. 이 책임을 갖도록 재구성한 deleteAll()메소드는 아래와 같다 :

public void deleteAll() throws SQLException {
	StatementStrategy st =  new DeleteAllStatement(); // 설정한 전략 클래스의 오브젝트 생성
	jdbcContextWithStatementStrategy(st); // 생성한 전략 오브젝트를 전달하여 컨텍스트 호출
}

클라이언트가 컨텍스트를 사용할 전략을 정해 전달한다는 면에서 DI구조를 갖고 있다고 할 수 있으며, 비로소 완벽한 전략 패턴을 갖추었다고 볼 수 있다. 이런 패턴을 통해 UserDao뿐 아니라 다른 Dao도 컨텍스트를 적용할 수 있게 되었다.


참고 자료

  1. 토비의 스프링 3.1 3장 – 템플릿

oksusutea's blog

꾸준히 기록하려고 만든 블로그