토비의 스프링 3장을 읽고 요약한 내용입니다. 모든 코드는 여기서 확인하실 수 있습니다.

요약

3장을 간단하게 요약한다면 다음과 같다. 기존 로직과 관련해서 예외 처리 및 로직을 변하는 것과 변화지 않는 것으로 명확하게 분리한다. 이 후 전략패턴을 통해 변경에 유연한 구조로 리팩토링한다. 하나의 메소드에 대해 다른 전략을 취하는 것들을 템플릿 콜백 패턴으로 분리하고 이를 적용 및 활용하는 방법을 알아본다. 스프링이 제공하는 JdbcTeplate을 알아보며 스프링은 어떤 식으로 템플릿 콜백 패턴을 활용하고 있는지 알아보고 템플릿 콜백 패턴이 주는 가치 및 사용법을 명확하게 인지하는 것이 이장의 목표이다.


템플릿이란 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부 터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다. 1장과 2장에 걸쳐 진행된 Dao 프로젝트는 아직 아쉬운 부분이 남아있다. 바로 예외 처리이다. Connection, Statement, ResultSet의 예외처리를 가장 간단한 수준에서부터 추상화되어 변경에 용이한 형태로까지 진행하며 템플릿이라는 것에 대해 학습해보자.

가장 간단한 예외처리

public void add(User user) {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = connectionMaker.openConnection();
            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());

            ps.executeUpdate();
        } catch (SQLException | ClassNotFoundException e) {
            throw new IllegalArgumentException();
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException throwables) {
                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException throwables) {
                }
            }
        }
    }

변하는 것과 변하지 않는 것

데이터베이스를 사용하는 모든 메서드에 위와 같은 try-catch 문을 사용할 것인가? 그렇다면 관리는 어떻게 할 것인가? 이런 코드를 효과적으로 다룰 수 있는 방법은 없을까? 그 해답은 변화는 것과 변하지 않는 부분을 명확하게 분리하는 것에서부터 시작한다.

  • 템플릿 메소드 패턴을 통한 분리 - 상속의 한계

    • 템플릿 메소드를 통해서 위의 코드를 분리하는 경우 클래스가 너무 많아진다는 단점과 런타임에 동적으로 하위 타입을 사용하기 힘들다. 이러한 부분을 개선하는 형태인 전략 패턴으로 진행해보겠다.
  • 전략 패턴을 통한 분리 및 DI

    // 전략과 구현체
    
    @FunctionalInterface
    public interface StatementStrategy {
        PreparedStatement makePreparedStatement(Connection c) throws SQLException;
    }
    
    public class DeleteAllStatement implements StatementStrategy{
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            PreparedStatement ps = c.prepareStatement("delete from users");
    
            return ps;
        }
    }
// 클라이언트

public void deleteAll() {
        StatementStrategy strategy = new DeleteAllStatement();
        jdbcContentWithStatementStrategy(strategy);
    }
// 컨텍스트

private void jdbcContentWithStatementStrategy(StatementStrategy strategy) {

        try (Connection c = connectionMaker.openConnection();
             PreparedStatement ps = strategy.makePreparedStatement(c)) {
            ps.executeUpdate();
        } catch (SQLException | ClassNotFoundException throwables) {
            throw new IllegalArgumentException();
}

JDBC 전략 패턴 최적화

ADD 추가하기

public void add(User user) {
        StatementStrategy strategy = new AddStatement(user);
        jdbcContentWithStatementStrategy(strategy);
    }

아직도 남은 불만들

  • 익명 내부 클래스를 사용해서 클래스의 폭증을 막아보자!

    위의 코드에서 클래스를 제거하고 익명 클래스를 사용한다면 깔끔하게 코딩할 수 있다. 다만 아직 조금 아쉬운 부분은 결국 UserDao가 Strategy를 생성하고 있다는 것이다. 이를 대신해주는 Spring IoC 혹은 우리의 IoC 컨테이너 역할을 하는 Factory가 주입하는 방식으로 하는 것이 더욱 이상적일 것 같다.

    public void add(final User user) {
            jdbcContentWithStatementStrategy((con) -> {
                PreparedStatement ps = con.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 deleteAll() {
            jdbcContentWithStatementStrategy((con) -> con.prepareStatement("delete from users"));
        }

스프링과 DI

스프링 DI를 활용하여 위의 구조를 간단하게 바꿔보자.

// 빈 등록
		@Bean
    public UserDao userDao() {
        ConnectionMaker connectionMaker = new DConnectionMaker();

        return new UserDao(connectionMaker, jdbcContext());
    }

    @Bean
    public JdbcContext jdbcContext() {
        JdbcContext jdbcContext = new JdbcContext(new DConnectionMaker());

        return jdbcContext;
    }
// 여러 DAO에서 함께 사용할 수 있도록 클래스 분리

public class JdbcContext {
    private final ConnectionMaker connectionMaker;

    public JdbcContext(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public void workWithStatementStrategy(StatementStrategy strategy) {
        try (Connection c = connectionMaker.openConnection();
             PreparedStatement ps = strategy.makePreparedStatement(c)) {
            ps.executeUpdate();
        } catch (SQLException | ClassNotFoundException throwables) {
            throwables.printStackTrace();
        }
    }
}
// 사용하는 쪽 코드

public void deleteAll() {
        jdbcContext.workWithStatementStrategy((con) -> con.prepareStatement("delete from users"));
    }

의존관계 주입이라는 개념을 충실히 따르자면, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입해주는 것이 맞고 하지 않았나?

맞다. 인터페이스를 사용하지 않았다면 엄밀히 말해 서 온전한 DI라고 볼 수는 없다. 그러나 스프링의 DI는 넓게 보자면 객체의 생성과 관계 설정에 대한 제어 권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포 괄한다. 그런 의미에서 JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입 했다는건 DI의기본을 따르고 있다고 볼 수 있다.

그렇다면 인터페이스를 사용하진 않았지만 JdbcContent와 UserDao를 DI 구조로 만든 이유는 무엇일까?

  • JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.
  • JdbcContext는 dataSource 프로퍼티를 통해 DataSource 오브젝트를 주입 받도록 되어 있다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다.

인터페이스를 사용하지 않고 DAO와 밀접한 관계를 갖는 클래스를 DI하는 방법

  • 이렇게 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI 하는 것은 스프링에 서도 종종 사용되는 기법이다. → 굳이 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하게 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사 용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점이다.
  • 기존의 방법 사용

템플릿과 콜백

템플릿

템플릿(template)은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 학생들이 도형을
그릴 때 사용하는 도형자 또는 모양자가 바로 템플릿이다. 프로그래밍에서는 고정된 틀 안에 바꿀 수
있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다. JSP는 HTML이라는 고정된 부분에 EL
과 스크립릿이라는 변하는 부분을 넣은 일종의 템플릿 파일이다. 템플릿 메소드 패턴은 고정된 틀의
로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로
이뤄진다. 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달 리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다

콜백

콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다.
파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해
사용한다. 자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를
전달해야 한다. 그래서 펑셔널 오브젝트(functional object)라고도 한다.

작업 순서

  • 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참
    조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때
    파라미터로 전달된다.
  • 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고
    콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제
    공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
  • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클
    라이언트에 다시 돌려주기도 한다.

기본적으로 OCP를 지키고, 전략 패턴과 DI를 바탕에 깔고 있으니 원한다면 언제든지 확장해서 편리한 방법으로 사용할 수 있다. 그런 면에서 스프링의 기본이 되는 전략 패턴과 DI는 물론이고 템플릿/ 콜백 패턴도 익숙해지도록 학습할 필요가 있다.

접근

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되 는 코드를 분리할 방법을 생각해보는 습관을 기르자. 중복된 코드는 먼저 메소드로 분리 하는 간단한 시도를 해본다. 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터 페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하도록 만 든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있 다면 이번엔 템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다.

템플릿/콜백 패턴은 다양한 작업에 손쉽게 활용할 수 있다. 콜백이라는 이름이 의미하는 것처럼 다시 불려지는 기능을 만들어서 보내고 템 플릿과 콜백, 클라이언트 사이에 정보를 주고받는 일이 처음에는 조금 복잡하게 느껴질 지도 모르겠다. 하지만 코드의 특성이 바뀌는 경계를 잘 살피고 그것을 인터페이스를 사 용해 분리한다는, 가장 기본적인 객체지향 원칙에만 충실하면 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있을 것이다.

정리

  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시
    try/catch/finally 블록으로 관리해야 한다.

  • 일정한 작업흐름이 반복되면서 그 중 일부기능만 바뀌는 코드가 존재한다면 전략패턴을 적
    용한다. 바뀌지 않는 부분은 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해
    유연하게 전략을 변경할 수 있도록 구성한다.

  • 같은 애플리케이션 안에서 여러 가지 종류의 전략을 다이내믹하게 구성하고 사용해야 한다
    면 컨텍스트를 이용하는 클라이언트 메소드에서 직접 전략을 정의하고 제공하게 만든다.

  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간
    결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다.

  • 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리해서 공유하도
    록 만든다.

  • 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용
    한다. 클래스 내부에서 컨텍스트를 사용할 때 컨텍스트가 의존하는 외부의 오브젝트가 있다
    면코드를 이용해서 직접DI 해줄 수 있다.(메소드 인자로 DI)

  • 단일 전략 메소드를 갖는 전략패턴이면서 익명내부클래스를 사용해서 매번 전략을 새로 만
    들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라
    고 한다.

  • 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리
    하다.

  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.

  • 스프링은 JDBC 코드 작성을 위해 JdbcTemplate을 기반으로 하는 다양한 템플릿과 콜백을

    제공한다.

  • 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고,하나의 콜백을 여러번 호출할 수도
    있다.

  • 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야 한다.

0개의 댓글