Spring - (25) : Template

­이승환·2021년 12월 18일
0

spring

목록 보기
22/26

Overview


이번장도 역시 토비의 스프링을 보고 요약한 포스팅이다. 이번 포스팅에 관련해서는 디자인패턴이랑 항목으로 이전에 정리한 내용이 많기 때문에 함께 참고해서 읽으면 좋을것으로 보인다.

문제가 있을 시 lshn1007@hanyang.ac.kr 로 메일주시면 삭제하겠습니다.

템플릿이란?

  • 코드에서 변경이 거의 일어나지 않고, 일정한 패턴으로 유지되는 특성을 가진 코드를 독립시켜서 재활용할 수 있도록 하는 방법이다.
  • 중복된 코드를 반복해서 사용하면 이를 유지보수하거나 신규 개발할 때 일부 코드가 누락되어 문제가 발생하거나, 퍼포먼스가 저하될 수 있기 때문에 템플릿을 통한 개선이 필요하다.
  • 상속이라는 방법이 자연스러운 OOP에서 활용할 수 있는 디자인 패턴으로 이해하면 좋다.

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

  • 중복 코드를 재사용하기 위해 디자인 패턴(템플릿 메소드 패턴, 전략 패턴, 템플릿/콜백 패턴)을 적용해볼 수 있다.
  • 코드에서 변하는 부분과 변하지 않는 부분을 파악하고, 변하는 부분을 메소드로 추출하거나, 별도의 클래스나 인터페이스로 분리해 적용할 수 있다.
  • OOP의 기본이 되는 SOLID 에서 특히 OCP와 관련해서 이해하면 훨씬 도움이 된다.

OCP : 확장에는 열려있고, 변경에는 닫혀있게 설계하는 방식을 말한다.

템플릿 메소드 패턴

  • 변경되지 않는 부분(형식) 을 슈퍼클래스(부모클래스) 에서 선언하고, 변경될 수 있는 메소드를 추상메소드로 선언해서 서브 클래스(자식클래스) 에서 오버라이드 하여 로직은 완성시키는 디자인 패턴이다.
  • 슈퍼클래스에서 곶어된 코드에서 서브클래스의 오버라이드한 메소드를 호출하는 방식으로 로직이 수행된다.

슈퍼 클래스 예제

/**
 * 슈퍼 클래스
 */
@RequiredArgsConstructor
public abstract class UserDao {

    private final DataSource dataSource;
    private final Logger log = LoggerFactory.getLogger(UserDao.class);

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

        try {
            c = dataSource.getConnection();
            ps = makeStatement(c);    // 추상 메소드의 구현으로 변하는 부분의 기능을 수행
            ps.executeUpdate();
        } catch (SQLException e) {
            ...
        } finally {
            ...
        }
    }
    
    // 변하는 부분을 추상 메소드로 선언
    protected abstract PreparedStatement makeStatement(Connection c) throws SQLException;
}

서브 클래스 예제

/**
 * 서브 클래스
 */ 
public class UserDaoDeleteAll extends UserDao {

    public UserDaoDeleteAll(DataSource dataSource) {
        super(dataSource);
    }
    
    // 슈퍼 클래스의 추상 메소드를 구현해 변하는 부분을 정의
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}

템플릿 메소드의 단점

  • 추상 클래스와 구현 클래스가 강하게 연결되어 있어 슈퍼 클래스 내부의 변경이 발생하면 모든 서브 클래스를 함께 수정하거나 다시 개발해야 할 가능성이 있다.
  • 다중 상속이 불가능하기 때문에 서브 클래스에서 다른 클래스의 기능을 확장하고자 할 때 문제가 될 수 있다.

전략 패턴

  • 변경될 수 있는 부분(비지니스 로직)을 하나의 인터페이스로 만들어 두고 이를 구현한 클래스(전략)를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
  • VO와 비슷한 방식이라고 이해하면 좋을 것 같다.

변경될 수 있는 부분 인터페이스

// 변경될 수 있는 부분을 하나의 인터페이스로 분리
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");
    }
}

@AllArgsConstructor
public class AddStatement implements StatementStrategy {

    private User user;

    @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());

        return ps;
    }
}

클라이언트에서 활용

public class UserDao {
    ...

    public void deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement();    // 구체적인 전략 오브젝트 생성
        jdbcContextWithStatementStrategy(st);               // context method(변경되지 않는 고정된 코드) 호출. 생성한 전략 오브젝트 전달
    }

    public void add(User user) throws SQLException {
        StatementStrategy st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
    }

    public void jdbcContextWithStatementStrategy(StatementStrategy st) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = st.makePreparedStatement(c);    // 전달 받은 전략(오브젝트 구현체)에 따라 기능이 다르게 수행된다.
            ps.executeUpdate();
        } catch (SQLException e) {
            ...
        } finally {
            ...
        }
    }
}

템플릿/콜백 패턴

  • 전략패턴을 변형한 디자인 패턴
  • 매번 인터페이스를 구체화시켜서 비즈니슥 로직을 만들기 보단, 익면 내부 클래스를 생성해서 이용하는 패턴
  • 클라이언트가 변화하는 부분을 콜백으로 템플릿으로 전달하면, 템플릿에서 정해진 코드에 전달받은 콜백을 실행시킴
public class UserDao {

    private final DataSource dataSource;
    private final Logger log = LoggerFactory.getLogger(UserDao.class);
   
    // 템플릿 메소드 호출 시 콜백을 전달. 별도 클래스로 만들어둘 필요가 없어 코드가 간결해짐
    public void deleteAll() throws SQLException {
        jdbcContextWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                return c.prepareStatement("delete from users");
            }
        });
    }

    public void add(final User user) throws SQLException {
        jdbcContextWithStatementStrategy(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());
                return ps;
            }
        });
    }

    public void jdbcContextWithStatementStrategy(StatementStrategy st) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = st.makePreparedStatement(c);
            ps.executeUpdate();
        } catch (SQLException e) {
            ...
        } finally {
            ...
        }
    }
}

템플릿

  • 고정된 작업 흐름을 가진 코드로 재사용을 위해서 분리한 코드를 말한다.

콜백

  • 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말함
  • 템플릿에 파라미터로 절달되지만 값을 참조하기 위한 것이 아니라, 특정 로직을 담은 메소드를 실행시키기 위함
  • 자바에서 람다식을 제외하고는 메소드 자체를 파라미터로 전달할 수는 없기 때문에 익명클래스를 활용하는 방식

기타

중첩클래스에 대해서..

  • 다른 클래스 내부에서 정의된 클래스를 의미함
  • 크게 Static, Inner 로 분리하고, inner는 또 3가지 방법으로 구성되어 있음(총 4가지)

static nested class : 독립적으로 객체 생성이 가능하도록 static으로 선언된 중첩 클래스
Member inner class : 멤버필드처럼 오브젝트 레벨에 정의되는 중첩 클래스(instance value처럼)
Local inner class : 메소드 레벨에서 정의되는 클래스(마치 로컬변수와 같이 선언)
Anonymous inner class : 인터페이스를 활용해서 정의되는 클래스

각 중첩 클래스의 접근 범위와, 스코프의 경우에는 자세한 포스팅을 구글링해보도록 하자.. (추후 추가하려고는 계획중)

스프링이 제공하는 템플릿/콜백 기술

  • 스프링은 JDBC를 이용하는 DAO에서 사용할 수 있도록 다양한 템플릿과 콜백을 제공한다.
  • 거의 모든 종류의 JDBC 코드에 사용 가능한 템플릿과 콜백을 제공할 뿐만 아니라, 자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜서 간단한 메소드 호출만으로 사용이 가능하도록 만들어져 있다.
  • 스프링이 제공하는 JDBC 코드용 기본 템플릿은 JdbcTemplate 이다.
  • JdbcTemplate은 DataSource를 생성자 파라미터로 받아 생성할 수 있다.
  • 김영한 강좌에서 말하기로는 JdbcTemplate이 레거시로 구성된 상황이 아니라면 굳이 찾아볼 필요는 없다곤 했다..

update() - JdbcTemplate

/**
 * [콜백을 직접 만들어 전달한 경우]
 */
jdbcTemplate.update(new PreparedStatementCreator() {
    @Override
    public PreparedStatement createPreparedStatement(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());
        return ps;
    }
});

/**
 * [JdbcTemplate의 내장 콜백을 사용한 경우]
 * 바인딩할 파라미터는 순서대로 넣어주면 된다.
 */
jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
        user.getId(), user.getName(), user.getPassword());

queryForInt() - JdbcTemplate

/**
 * [콜백을 직접 만들어 전달한 경우]
 */
return jdbcTemplate.query(
    // 첫번째 콜백 - PreparedStatement 생성
    new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection c) throws SQLException {
            return c.prepareStatement("select count(*) from users");
        }
    },

    // 두번째 콜백 - PreparedStatement 실행 결과로 받은 ResultSet에서 데이터 알맞게 가공
    new ResultSetExtractor<Integer>() {
        @Override
        public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
            rs.next();
            return rs.getInt(1);
        }
    }
);

/**
 * [JdbcTemplate의 내장 콜백을 사용한 경우]
 * jdbcTemplate.queryForInt()는 @Deprecated 되어 다른 방법을 사용
 */
return jdbcTemplate.queryForObject("select count(*) from users", Integer.class);

queryForObject() - JdbcTemplate

/**
 * ResultSetExtractor는 ResultSet을 한 번만 전달해주기 때문에 수동으로 반복문을 돌려서 원하는 타입에 맞게 매핑해줘야 되지만,
 * RowMapper는 ResultSet의 각 행을 반복적으로 리턴해주기 때문에 반복문 없이 각 행을 원하는 타입에 맞게 매핑할 수 있음
 */
jdbcTemplate.queryForObject("select * from users where id = ?",
        new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int i) throws SQLException {
                return User.builder()
                        .id(rs.getString("id"))
                        .name(rs.getString("name"))
                        .password(rs.getString("password"))
                        .build();
            }
        });
        

query()

jdbcTemplate.query("select * from users order by id",
        new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int i) throws SQLException {
                return User.builder()
                        .id(rs.getString("id"))
                        .name(rs.getString("name"))
                        .password(rs.getString("password"))
                        .build();
            }
        });

결론

  • 고정된 작업 흐름을 가지면서 자주 반복되는 코드가 있다면 중복되는 코드를 분리하는 방법을 생각해보자.
  • 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
  • 그 중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 전략 패턴을 적용할지 고려해보자.
  • 만약 바뀌는 부분이 한 애플리케이션에서 여러 종류가 만들어질 수 있다면(종류가 많다면) 템플릿/콜백 패턴을 적용할지 고려해보자.
  • JDBC를 사용하는 DAO 코드에서 템플릿/콜백 패턴의 적용을 고려하고 있다면, 스프링이 제공하는 템플릿/콜백 기술인 JdbcTemplate 사용을 고려해보자.
profile
Mechanical & Computer Science

0개의 댓글