사람들과 스프링 서버를 직접 구현해보는 프로젝트를 하며 JDBC를 이용하여 외부 데이터베이스와 연결되는 레포지토리를 작성하게 되었습니다. 구현하는 과정에서 SQL 작성 부분만 바뀜에도 불구하고, 데이터베이스 Connection을 얻어오는 부분 같이 변경되지 않는 부분이 하나의 메소드에 있게 되었습니다. 따라서 변경되는 부분과 변경되지 않는 부분을 분리할 필요가 있다고 생각했고, 그에 대한 방법으로 템플릿 매소드 패턴과 템플릿 콜백 메소드 패턴 두가지를 떠올렸고, 최종적으로 템플릿 콜백 메소드 패턴을 적용했습니다. 또한 PR에서 관련 질문을 받았기에, 좀 더 확실히 익히고자 해당 글을 작성하게 되었습니다.
1. 템플릿 메소드 패턴
템플릿 메소드 패턴이란 상속을 통해 기능을 확장해서 사용하는 패턴입니다. 즉, 변하지 않는 부분은 상위클래스에 두고 변하는 부분은 추상 메소드로 정의해서 하위클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것입니다.
대표적으로 Spring MVC에서 DispatcherServlet, FrameworkServlet을 비롯한 서블릿 계층구조가 있습니다.
- HttpServlet은 Java EE의 일부로 제공되는 클래스로, HTTP 요청을 처리하기 위한 기본적인 메서드와 생명주기를 정의합니다.
- Spring MVC에서는 HttpServlet을 직접 사용하기 보다 이를 확장하는 FrameworkServlet 클래스를 사용합니다. FrameworkServlet은 스프링 애플리케이션 컨텍스트를 초기화하고 관리하는 역할을 합니다.
- DispatcherServlet은 클라이언트의 요청을 받아 적절한 컨트롤러로 전달하고, 컨트롤러의 처리 결과를 바탕으로 뷰를 선택하여 응답을 생성합니다.
이러한 서블릿 계층구조에서 템플릿 메소드 패턴이 사용된 이유로는 상속을 통해 공통의 기능을 재사용할 수 있고, 요청 처리의 구조를 유지할 수 있기 때문입니다.
그렇지만 이번 프로젝트에서 레포지토리의 기능을 구현할 때는 템플릿 메소드 패턴을 적용하지 않기로 했습니다.
그 이유는
- 필요한 쿼리마다 계속 클래스를 만들어야 하고 클래스를 설계하는 시점에서 확장구조가 고정되어 버리기 때문입니다.
- 클래스 간 결합도가 높아질 수 있기 때문입니다. 상위 클래스와 하위 클래스 간 결합도가 높아 로직에 변화가 생겨 상위 클래스를 변경할 때, 하위 클래스의 변경이 일어날 수 있습니다.
- JdbcTemplate과 JdbcRepository는 상위클래스와 하위클래스 관계로 설정할 시, 리스코프 치환 법칙을 위반할 가능성이 있기 때문입니다.
따라서 템플릿 콜백 패턴을 통해 변하는 부분과 변하지 않는 부분인 템플릿으로 처리하고 변하는 부분을 콜백으로 처리해서 템플릿에 전달하게끔 했습니다.
2. 템플릿 콜백 패턴
템플릿 콜백 패턴이란 전략 패턴의 일종으로 런타임 시점에 람다표현식, 메소드 참조를 이용해서 동적으로 전략 알고리즘을 주입합니다. GOF 디자인 패턴은 아니고 전략 패턴 중 하나입니다.
여기서 콜백이란 실행가능한 코드 조각으로 다른 코드에 인수(argument)로 전달이 되고 해당 콜백은 적절한 시점에 실행될 수 있습니다.
@Repository
public class JdbcUserRepository implements UserRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcUserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
...
@Override
public Optional<User> findById(Long id) {
String sql = "SELECT * FROM user WHERE id = ?";
RowMapper<User> rowMapper = getUserRowMapper();
User user = jdbcTemplate.queryForObject(connection -> {
PreparedStatement statement = connection.prepareStatement(sql);
statement.setLong(1, id);
return statement;
}, new Object[] {id}, rowMapper);
return Optional.ofNullable(user);
}
private RowMapper<User> getUserRowMapper() {
return (resultSet, rowNum) -> {
if (resultSet.next()) {
Long userId = resultSet.getLong("id");
String name = resultSet.getString("name");
String email = resultSet.getString("email");
String password = resultSet.getString("password");
String phoneNumber = resultSet.getString("phoneNumber");
return new User(userId, name, password, email, phoneNumber);
}
return null;
};
}
}
@Component
public class JdbcTemplate {
private final Logger log = LoggerFactory.getLogger(JdbcTemplate.class);
private final DriverManager driverManager;
public JdbcTemplate(DriverManager driverManager) {
this.driverManager = driverManager;
}
...
public <T> T queryForObject(StatementStrategy strategy, @Nullable Object[] args, RowMapper<T> rowMapper) {
try {
Connection connection = driverManager.getConnection();
PreparedStatement statement = strategy.makePreparedStatement(connection);
ResultSet resultSet = statement.executeQuery();
return rowMapper.mapRow(resultSet, 1);
} catch (SQLException exception) {
throw new DataAccessException("sql exception occurs", exception);
}
}
}
여기서 데이터베이스 커텍션을 얻어오고 SQL 쿼리를 실행하고 결과를 가져오는 부분은 변하지 않는 부분으로 템플릿으로 구현하고자 했고, SQL문과 파라미터 바인딩은 변하는 부분으로 콜백으로 구현하고자 했습니다. 이를 통해 책임을 분리함으로써 단일책임원칙을 지킬 수 있게끔 했습니다.
참고자료
'(개발) 프로젝트 > Java로 직접 만드는 WAS' 카테고리의 다른 글
[스프링 서버 구현하기] 외부 데이터베이스 도입과 JDBC (0) | 2024.03.27 |
---|---|
디미터의 법칙(Law of Demeter) (2) | 2024.03.14 |
댓글