템플릿/콜백 패턴은 템플릿 메소드 패턴과 다르다.
3장에서 다루는 패턴은 템플릿/콜백 패턴이다.
이번 장에서 처음으로 템플릿/콜백 패턴을 접했다.
올해 2월 초, 우테코 프리코스 미션 구현 도중 try-catch문에 대한 반복을 줄이고 싶었던 적이 있었다. 이에 대한 답을 3장에서 찾을 수 있어 기뻤다.
책에서는 익명클래스 기준으로 설명하지만, 자바 8부터 도입된 람다를 통해 더 간결한 코드 작성이 가능하다.
또한 Try-with-resources를 통해 AutoCloseable 인터페이스를 구현한 클래스의 객체를 자동으로 닫아줄 수 있다.
💬
관련해 토프링 읽기모임에서 나왔던 얘기들을 노션 링크로 공유한다.
패턴들 각각의 목적, “의도”를 알아야 한다.
단순히 패턴을 공부하는 것을 넘어, 어떤 상황에서 어떤 디자인 패턴을 적용할 수 있을 지 고민해봐야겠다.
개방 폐쇄 원칙
변화의 특성이 다른 코드
확장되는 성질을 가진 코드
가 있다.변하지 않으려는 성질을 가진 코드
가 있다.변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것
이 개방 폐쇄의 원칙
이다.
템플릿이란?
코드 중
일정한 패턴으로 유지되는 특성을 가진 부분
을자유롭게 변경되는 성질을 가진 부분
으로부터 독립
시킨다!초난감 DAO는 예외상황에 대한 처리를 하지 않았다.
DB 커넥션은 DB의 소중한 자원이므로, 어떤 이유로든 예외가 발생하더라도 리소스를 반드시 반환해야 한다. 그렇지 않으면 시스템에 큰 문제를 일으킬 수 있다.
예외처리 없는 JDBC 코드
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
// 여기서 예외 발생 시 실행이 중단된다.
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
위 메소드에서는 정상적인 흐름의 경우에는 ps.close()
와 c.close()
가 잘 호출되어 리소스를 반환한다.
PreparedStatement
를 처리하는 중에 예외가 발생하면?
Connection
과PreparedStatement
의 close()
메소드가 실행되지 않아 제대로 리소스가 반환되지 않을 수 있다.예외처리를 한 JDBC 코드
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
// 예외가 발생할 수 있는 부분은 전부 try 블록에 넣어준다.
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users");
ps.executeUpdate();
} catch (SQLException e) {
// 예외가 발생하면 던져준다.
throw e;
} finally {
if(ps != null) {
try {
ps.close();
// `ps.close()` 메소드에서도 `SQLException` 이 발생할 수 있다.
// 이를 잡아주지 않으면, 아래 `Connection (c)`을 반환하는 로직이 수행되지 않을 수 있다.
} catch (SQLException e) {
}
}
if(c != null) {
try {
c.close(); // Connection 반환
} catch (SQLException e) {
}
}
}
}
조회를 위한 JDBC 코드는 더 복잡해진다. ResultSet
이 더 추가되기 때문이다.
getCount()
의 예외처리 후 코드
public int getCount() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return rs.getInt(1);
}
catch (SQLException e) {
throw e;
} finally {
// `ResultSet`의 `null`을 체크하고 닫아주는 부분 추가
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) {
}
}
}
}
try/catch/finally
블록이 2중 중첩된다.try/catch/finally
블록 안에서 필요한 부분을 찾아서 수정해야 한다!new UserDao().deleteAll()
메소드를 먼저 개선해보자.
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 리소스 반환
}
}
s = c.prepareStatement("delete from users");
저 부분만 유연하게 변경될 수 있으면 ResultSet
이 필요 없는 어떠한 쿼리도 같은 템플릿으로 처리할 수 있게 될 것이다.
메소드를 추출하면 될까?
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 {
return c.prepareStatement("delete from users");
}
일단 나누긴 했는데, 별다른 이득이 없어보인다.
이 경우, 반대이다!
getConnection
, executeUpdate
)가 재사용이 필요한 부분이고,makeStatement
)는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이다.이번엔 템플릿 메소드 패턴을 이용해서 분리해보자. 템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다.
public abstract class UserDao {
PreparedStatement makeStatement(Connection c) throws SQLException {
return null;
}
public class UserDaoDeleteAll extends UserDao {
@Override
protected PreparedStatement makeStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
템플릿 메소드 패턴?
확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)
은 그럭저럭 지킬 수 있다.
그러나!
전략 패턴?
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 void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.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) { } }
}
}
이제 그럭저럭 전략 패턴은 적용했는데 이상하다.
(OCP 폐쇄 원칙)
전략을 바꿔 쓸 수 있다는 것(OCP 개방 원칙)
이다.DeleteAllStatement
를 사용하도록 고정되어 있다.StatementStrategy
인터페이스 뿐만 아니라 특정 구현 클래스인 DeleteAllStatement
를 직접 알고있다는 건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없다.전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다.
Client가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다.
이전 ConnectionMaker
에 전략 패턴을 적용했을 때와 동일한 그림이 나왔다.
결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리 시킨 것이 ObjectFactory
이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)
이었다.
결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.
StatementStrategy strategy = new DeleteAllStatement();
컨텍스트에 해당하는 부분은 별도의 메소드로 독립시켜보자!
DeleteAllStatement
오브젝트 같은 전략 클래스의 오브젝트를 컨텍스트 메소드로 전달해야 한다.StatementStrategy
를 컨텍스트 메소드 파라미터로 지정하자~!public void jdbcContextWithStatementStrategy(StatementStrategy statementStrategy) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = statementStrategy.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) { } }
}
}
마이크로 DI
의존관계 주입(DI)은 다양한 형태로 적용할 수 있다. DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 이 개념만 따른다면 DI를 이루는 오브젝트와 구성요소의 구조나 관계는 다양하게 만들 수 있다.
얼핏 보면 DI 같아 보이지 않지만, 세밀하게 관찰해보면 작은 단위지만 엄연히 DI가 이뤄지고 있음을 알 수 있다.
템플릿
템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우 템플릿이라고 부른다.
콜백
콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다.
자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. (자바 1.8부터 람다로 가능) 그래서 펑셔널 오브젝트(functional object)라고도 한다.
여러 메소드를 가질 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스
를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
토비의 스프링 3장 중반 이후 부분은 우아한테크코스 레벨 4 미션에 적용 가능한 부분이라 판단해, 책 속 코드를 참고해 미션에 적용해보았다.