본문 바로가기
Spring/토비의 스프링 3.1

토비의 스프링 chap 3 - 템플릿

by 성건희 2022. 6. 2.
반응형

실습 코드

GitHub - gbeea1004/toby-spring at chap3 에서 실습을 진행할 수 있다.

템플릿 메서드 패턴

템플릿 메서드 패턴은 상속을 통해 기능을 확장해서
변하지 않는 부분은 부모 클래스에 두고 변하는 부분은 자식 클래스에서 재정의 해서 사용하는 패턴이다.
이를 적용하여 OCP 를 만족하는 구조로 개선을 할 수 있는데, 몇가지 단점이 존재한다.

  1. 로직마다 상속을 통해서 새로운 클래스를 만들어야 한다.
  2. 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다. (유연성이 떨어진다.)

전략 패턴

OCP 를 잘 지키는 구조이면서 템플릿 메서드 패턴보다 유연하고 확장성이 뛰어난 방법이 전략 패턴이다.
변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방법이다.

private void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    Connection con = null;
    PreparedStatement pstmt = null;

    try {
        con = dataSource.getConnection();

        pstmt = stmt.makePreparedStatement(con);

        pstmt.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (SQLException e) {}
        }
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {}
        }
    }
}

위처럼 전략(변하는 부분)에 해당하는 StatementStrategy 인터페이스를 메서드의 파라미터로 받아 실행한다.
클라이언트가 컨텍스트가 사용할 전략을 정해서 메서드 파라미터로 전달하는 면에서 DI 구조라고 할 수 있다.

전략 패턴의 문제점

전략 패턴으로 개선하고 난 뒤 코드가 깔끔하게 개선되었다.
하지만 개선할 부분이 있다.

  1. DAO 메서드마다 새로운 전략 인터페이스 구현 클래스를 만들어야 한다.
  2. DAO 메서드에서 전략 파라미터에 전달할 User 와 같은 부가 정보가 있는 경우,
    오브젝트를 전달받는 생성자와 이것을 저장할 인스턴스 변수를 번거롭게 만들어야 한다.

위 문제를 해결할 수 있는 방법은 아래와 같다.

로컬 클래스

전략 클래스를 매번 만들지 않고 Dao 클래스의 메서드 안에 내부 클래스로 정의하는 것이다.
DeleteAllStatement 나 AddStatement 는 Dao 밖에서는 사용되지 않는다.
로컬 클래스로 만들어 두면 클래스가 내부 클래스이므로 자신이 선언된 곳의 정보에 접근할 수 있다는 장점이 있다.
따라서 User 인스턴스를 만들필요 없이 사용할 수 있다.

익명 내부 클래스

이름을 갖지 않는 클래스로 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 선언과 동시에 오브젝트를 생성할 수 있다.
이름이 없으므로 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다.
보통 재사용할 필요가 없고 구현한 인터페이스 타입으로만 사용할 경우 유용하다.

public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(con -> con.prepareStatement("delete from users"));
}

JdbcContext 를 수동 DI 적용하기

JdbcContext 를 빈으로 등록하지 않고, UserDao 내부에서 직접 DI 하는 방법이다.

public class UserDao {

    private final JdbcContext jdbcContext;

    public UserDao(DataSource dataSource) {
        this.jdbcContext = new JdbcContext(dataSource);
    }

    ...
}

이렇게 하게되면 굳이 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext 를 어색하게
따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI 를 적용할 수 있다는 장점이 있다.

아래 두 차이점을 잘 알아두자.


인터페이스를 사용하지 않는 클래스와의 의존관계이지만 스프링의 DI 를 이용하기 위해 빈으로 등록해서 사용하는 방법

  • 장점 : 오브젝트 사이의 실제 의존관계가 설정파일에 명확하게 드러난다.
  • 단점 : 구체적인 클래스와의 관계가 설정에 직접 노출된다.

DAO 코드를 이용해 수동으로 DI 하는 방법

  • 장점 : JdbcContext 가 UserDao 의 내부에서 만들어지고 사용되면서 그 관계를 외부에는 드러내지 않고, 필요에 따라 내부에서 은밀히 DI 를 수행하고 그 전략을 외부에는 감출 수 있다.
  • 단점 : JdbcContext 를 여러 오브젝트가 사용하더라도 싱글톤으로 만들 수 없고, DI 작업을 위한 부가적인 코드가 필요하다.

둘 중 어느 방법이 더 낫다라고 말하기는 힘들고, 상황에 따라 적절하다고 판단되는 방법을 사용하면 된다.
일반적으로는 인터페이스를 만들어 평범한 DI 구조로 만든다.

템플릿 콜백 패턴

전략 패턴 + 익명 내부 클래스 구조를 템플릿 콜백 패턴이라고 한다.
템플릿은 변하지 않는 부분, 콜백은 템플릿 안에서 호출되는 부분 (변하는 부분) 으로 보면 된다.
가장 전형적인 템플릿_콜백 패턴 후보는 try_catch/finally 블록을 사용하는 코드다.

JdbcTemplate

스프링은 JDBC 를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.

update()

JdbcTemplate 의 콜백은 PreparedStatementCreator 인터페이스의 createPreparedStatement() 메서드다.
JdbcTemplate 은 예제에서 만들었던 executeSql() 기능처럼 SQL 문장만 String 으로 전달하면 미리 준비된 콜백을 만들어서 템플릿을 호출하는 것 까지 한 번에 해주는 편리한 메서드를 제공한다.

/**
 * 유저 전체 삭제
 */
public void deleteAll() {
    jdbcTemplate.update("delete from users");
}

queryForInt()

해당 메서드는 Deprecated 되었다.
jdbcTemplate.queryForObject 를 사용하자.

/**
 * 유저 수 조회
 */
public int getCount() {
    return jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
}

queryForObject()

예제에서 사용하는 queryForObject 는 Deprecated 되었다.

대안으로 아래 메서드를 사용하면 된다.

query()

queryForObject 가 쿼리의 결과가 1개 일 때 사용하는 것이라면,
query 는 여러 개의 로우 결과로 나오는 일반적인 경우에 사용한다.

public User get(String id) {
    return jdbcTemplate.queryForObject("select * from users where id = ?",
            (rs, rowNum) -> new User(rs.getString("id"), rs.getString("name"), rs.getString("password")),
            id);
}

중복 제거하기

UserDao 의 get() 과 getAll() 의 경우 RowMapper 의 내용이 똑같다.
추후 기능이 확장되면서 이 중복이 계속 늘어날 수 있을 것 같다.
그렇다면 이것을 인스턴스 변수로 뽑아서 중복을 제거할 수 있다.

public class UserDao {
    private final RowMapper<User> userMapper = (rs, rowNum) -> new User(rs.getString("id"), rs.getString("name"), rs.getString("password"));

    /**
     * 유저 조회
     *
     * @param id 유저 아이디
     * @return 조회된 유저 정보
     */
    public User get(String id) {
        return jdbcTemplate.queryForObject("select * from users where id = ?",
                userMapper,
                id);
    }

    /**
     * 유저 전체 조회
     */
    public List<User> getAll() {
        return jdbcTemplate.query("select * from users order by id",
                userMapper);
    }
}

참고

반응형

댓글