본문 바로가기

Spring

템플릿 콜백 패턴

토비의 스프링은 참으로 좋은 책입니다. 

새로운 기술을 소개할 때, 그냥 사용법만을 소개하지 않고 그 기술의 바탕이 되는 지식을 먼저 알려주거든요.

정말 추천합니다..


여담이었고요, 이번에는 어떠한 기술의 바탕이 되는 템플릿 콜백 패턴에 대해 알아보겠습니다.

템플릿이란 뜻 그대로 미리 만들어 둔 틀을 의미합니다.

콜백이란, 실행될 것을 목적으로 다른 오브젝트의 메서드에 전달되는 오브젝트를 말합니다. 수동적인 오브젝트라고 할까요..

(자바에선 메서드를 전달할 순 없으니 콜백의 단위가 오브젝트가 됩니다)



템플릿과 콜백 추출하기

간단한 DAO의 메서드를 하나 보시죠.

public void deleteAll() throws Exception{
    Connection conn = null;
    PreparedStatement ps = null;
    
    try{
        conn = dataSource.getConnection();
        ps = conn.prepareStatement("delete from car");
        ps.executeUpdate();
    } catch(SQLException e){
        throw e;
    } finally{
        if(ps!=null){
            try{ps.close();}catch(Exception e){}
        }
        
        if(conn!=null){
            try{conn.close();}catch(Exception e){}
        }
    }
}

car 테이블의 전체 데이터를 삭제하는 메서드입니다. CarDAO에 존재하고 있습니다.

CarDAO 뿐만 아니라 다른 DAO에도 deleteAll 관련 메서드는 항상 존재할 것입니다. 

만약 user 테이블의 전체 데이터를 삭제하는 메서드가 있다고 가정합시다. 그 메서드는 UserDAO에 있겠네요.

UserDAO의 deleteAll 메서드와 CarDAO의 deleteAll 메서드는 코드가 얼마나 차이가 날까요?

저기 Connection 객체를 통해 PreparedStatement를 얻어오는 저 한줄, SQL 쿼리가 들어가는

ps = conn.preparedStatement(" ~~~ ");

이 부분 말고는 다 동일할 겁니다. 변하지 않는 부분이죠.

굳이 테이블의 데이터를 삭제하는 로직이 아니어도 됩니다. 

insert, update, delete를 사용하고, 전달하는 인자가 없는 경우, 위의 SQL 쿼리 외엔 코드가 다 동일합니다.

PreparedStatment를 얻는 부분을 뺀 동일한 부분은 여러 클래스, 혹은 메서드에 중복되어 나타나고 있겠죠.

언제나 그렇듯, 중복되는 부분은 따로 분리해주는 작업을 진행해줘야겠네요.

메서드 추출도 좋지만 앞서 관심사의 분리에 대해 얘기했었죠? 이는 분명 다른 관심사이므로 클래스 단위로 분리해봅시다.

클래스 이름은 JdbcContext, 메서드명은 executeUpdate 정도로 하겠습니다.


public class JdbcContext {
    DataSource dataSource;
    
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void executeUpdate(String query) throws SQLException{
        Connection conn = null;
        PreparedStatement ps = null;
        
        try{
            conn = dataSource.getConnection();
            ps = conn.preparedStatment(query); // 변경되는 부분
            ps.executeUpdate();
        } catch(SQLException e){
            throw e;
        } finally{
            if(ps!=null){
                try{ps.close();}catch(Exception e){}
            }
            
            if(conn!=null){
                try{conn.close();}catch(Exception e){}
            }
        }
    }
}

인자가 없는 데이터 조작 작업(insert, update, delete 등)에 대한 템플릿을 만들어보았습니다.

앞으로 모든 DAO에서는 굳이 위의 코드를 DAO에 중복하여 기술할 필요없이 위의 메서드를 호출하기만 하면 되는것이죠.

쿼리에 인자가 존재하는 경우도 만들 수 있습니다.

인자는 항상 개수가 다르지만, 자바의 가변 인자와 for-each 구문을 활용하면 메서드 하나로 충분히 구현 가능합니다.

그것까지 만들면 DAO내에 데이터 조작 작업들에 대한 템플릿이 완성되버리네요. 얻는 효과는 엄청납니다.

코드의 중복이 굉장히 줄어들게 되고, 수정사항이 있어도 이 템플릿 하나만 바꿔주면 됩니다. 객체지향의 힘!


위에서 변경되는 부분을 좀 더 살펴볼까요.

현재 String 형태의 쿼리를 인자로 받고 있는데, 사실은 이것보단 PreparedStatement를 인자로 받는편이 낫습니다.


String형태의 쿼리를 전달받으면 우리는 템플릿을 2개 만들어야 합니다. 인자 없는 템플릿, 있는 템플릿으로요.

하지만 PreparedStatment를 전달할 경우 얘기가 다르죠. 

setXXX으로 이미 인자가 다 셋팅된 PreparedStatement을 전달할 수 있기 때문입니다.

즉, 하나의 executeUpdate 메서드로 템플릿 형성이 가능하게 되지요!


근데 PreparedStatment를 전달하기에는 클라이언트 쪽에서 설정해줘야할게 많으므로, 그냥 메서드로 만들어버리겠습니다.

메서드로 만들 경우 장점이 더욱 부각됩니다. 내부에서 로그를 찍는 등의 다른 작업을 추가할 수 있기 때문이죠.

메서드자체는 파라미터로 전달될 수 없으니, 오브젝트로 만들어서 전달하겠습니다. (위에서 언급했던 콜백이네요?!)


이제 저 부분에 PreparedStatement를 반환하는 메서드를 넣어주겠습니다.

생각해보니 역시 전략패턴이 젤 좋겠네요. 인터페이스 이름은 PSMaker 정도로 하겠습니다. (이름 잘 못지어요 저..)


최종적으로 템플릿은 아래와 같은 모습이 되겠네요.

public void executeUpdate(PSMaker psMaker) throws SQLException{
    Connection conn = null;
    PreparedStatement ps = null;
    
    try{
        conn = dataSource.getConnection();
        ps = psMaker.makePreparedStatement(conn);  // 전략패턴 적용
        ps.executeUpdate();
    } catch(SQLException e){
        throw e;
    } finally{
        if(ps!=null){
            try{ps.close();}catch(Exception e){}
        }
        
        if(conn!=null){
            try{conn.close();}catch(Exception e){}
        }
    }
}


PSMaker라는 인터페이스를 만들고, makePreparedStatement 메서드를 정의해줍니다.

PreparedStatement를 만들려면 Connection 객체가 필요하므로, 전달받게 해줍니다.

public interface PSMaker {
    public PreparedStatement makePreparedStatement(Connection conn) throws SQLException;
}


자동차 테이블의 모든 데이터를 삭제해주는 DeleteAllCar 클래스를 만들어보겠습니다.

public class DeleteAllCar implements PSMaker{
    @Override
    public PreparedStatement makePreparedStatement(Connection conn) throws SQLException {
        PreparedStatement ps = conn.prepareStatement("delete from car");
        return ps;
    }
}

보이십니까? 장점이?

PSMaker의 makerPreparedstatement를 사용하는 템플릿은 어떻게든 PreparedStatement를 반환받기만 하면 됩니다.

위와 같이 인자없는 SQL에 대한 PreparedStatement 생성 뿐 아니라, 인자 있는 SQL에 대해서도 생성가능하며,

내부에서 쿼리 로그를 찍던, 호출 횟수 카운팅을 하든 제 마음입니다.


이제 위의 템플릿과 구현 클래스를 사용하여, DAO의 메서드는 아래와 같은 형태로 변합니다.

public void deleteAll() throws Exception{
    this.jdbcContext.executeUpdate(new DeleteAllCar());
}

(JdbcContext는 당연히 상단에서 초기화 되었습니다. 

매번 메서드마다 JdbcContext를 생성하는 불필요한 코드의 중복은 당연히 피해야 합니다.)


보다시피 포스트 상단의 deleteAll에 비해 훨씬 짧아졌습니다.

이 외에도 얻는 장점은 상당합니다. 

매번 메서드마다 중복이 되는 부분을 적절히 분리해냄으로써 로직이 변경되더라도 수정할 곳은 단 한군데로 좁혀집니다.

그리고 매번 변경이 되는 부분은 인터페이스를 통해 설계함으로써, 구현 클래스만 바꿔서 템플릿을 호출해주면 됩니다.

템플릿이 진정한 템플릿이 되었네요.

※ 적절한 관심사의 분리, 높은 응집도, 낮은 결합도의 원칙을 지켜 코드를 작성했기 때문에 나온 결과입니다.



템플릿 콜백 패턴

템플릿은 어떤 목적을 위해 미리 만들어 둔 모양이 있는 틀 을 의미합니다

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메서드에 전달되는 오브젝트 를 의미합니다.

앞서 만든 변화된 CarDAO에서 템플릿과 콜백이 보이십니까?


템플릿은 JdbcContext의 executeUpdate 메서드이고,

콜백은 PSMaker를 구현한 DeleteAllCar과 같은 클래스가 되겠네요. 

템플릿에 의해 실행되려고 DAO로 부터 전달되고 있고, 실제로 템플릿에서 그 오브젝트의 메서드가 호출되고 있습니다.

이것이 템플릿 콜백 패턴의 형태입니다. 앞서 CarDAO를 변경했던 것이 템플릿 콜백 패턴을 적용하는 과정이었던거죠.


템플릿 콜백 패턴이 전략패턴과 다른 점은, 메서드 단위로 매번 새로운 오브젝트가 생성되어 전달된다는 것입니다.

일반적인 DI는 구현 클래스를 빈으로 등록해놓고, 수정자 메서드를 통해 주입해 줍니다.

그러나 템플릿 콜백 패턴에서는 매번 메소드마다 새로운 오브젝트를 생성해서 전달해줍니다.


그리고 콜백도 조금 다릅니다. 대부분이 단일 메서드 인터페이스를 사용합니다.

단일 메서드 인터페이스. 즉, 인터페이스에 메서드 하나만 있다는 의미입니다.


근데.. 인터페이스 구현 클래스를 매번 만들자니 좀 낭비같네요.. 복잡해지기도 하고요..

(DeleteAllCar, InsertCar, SelectCar .. 사용하는 쿼리마다 클래스를 만들순 없지 않겠습니까?)

그래서 콜백은 클래스를 생성하지 않고, 일반적으로 익명 내부 클래스(Annonymous 클래스)를 사용합니다.


Annonymous 클래스를 적용한 소스입니다. 이번엔 where 조건이 있는 delete 메서드로 만들어보겠습니다.

public void delete(final String id) throws Exception{
    this.jdbcContext.executeUpdate(new PSMaker() {
        
        @Override
        public PreparedStatement makePreparedStatement(Connection conn) throws SQLException {
            PreparedStatement ps = conn.prepareStatement("delete from car where id=?");
            ps.setString(1, id);
            return ps;
        }
    });
}

굳이 클래스를 만들지 않아도 되는, 편리한 방법입니다.


여기까지가 템플릿 콜백 패턴에 대한 설명입니다..

템플릿 콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 두고 설계해야 합니다.


스프링에서는 이 템플릿 콜백 패턴을 가지고 만든 JDBC용 템플릿이 있습니다. 이름은 JdbcTemplate 입니다.

우리가 앞서 만든 템플릿들을 스프링에서 이미 만들어 놓은 것이죠. 훨씬 편리하게 사용 가능하도록요.

this.jdbcTemplate.update("delete from car");

이런식으로, 우리가 PreparedStatement를 직접 구현해 사용하는 방식보다 훨씬 편리하게 사용 가능합니다.

모든 형태의 쿼리에 사용가능합니다.

JdbcTemplate의 소스를 따라가보면, 수 많은 메서드들이 오버로딩 되어있고 메서드들을 타고가며 콜백 오브젝트를 생성하고,

결과적으로 최종 템플릿 메서드에 도달하는 형태로 되어있습니다.

그냥 사용만 하는 것보다, 템플릿 콜백 패턴을 알고 사용하니 훨씬 잘 보이네요.

시간날 때 한번 포스팅 하도록 하겠습니다.


읽어주셔서 감사합니다.

'Spring' 카테고리의 다른 글

JDBC 트랜잭션 동기화  (1) 2016.07.11
예외처리, 스프링 예외처리  (0) 2016.07.05
테스트와 JUnit  (3) 2016.06.27
싱글톤과 스프링  (0) 2016.06.24
스프링 DI  (0) 2016.06.22