3장 템플릿

개발 99·2025년 3월 27일

확장에는 자유롭게 열려 있고 변경에는 굳게 닫혀 있게 설계를 해야한다.(OCP)

어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고,
어떤 부분은 고정되 있고 변하지 않으려는 성질이 있음을 의미한다.

템플릿이란 이렇게 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며
일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시키는 것을 의미한다.

3.1 다시 보는 초난감 DAO

아직 예외상황에 대한 처리가 남아있다.

3.1.1 예외처리 기능을 갖춘 DAO

정상적인 JDBC 코드의 흐름을 따르지 않고 중간에 어떤 이유로든 예외가 발생했을 경우에도
사용한 리소스를 반드시 반환하기 위해서 "예외처리"는 필수이다!

public void deleteAll() throws SQLException {

	// 커넥션 획득
	Connection c = dataSource.getConnection();
    
    PreparedStatement ps = ... ;
    ps.excuteUpdate();
    
    ps.close();
    c.close();
    
}

여기 만약 예외가 발생할 경우 리소스(c,ps)를 반환하지 못하고 메소드를 빠져나올 수 있다.

이런식이면 반환되지 못한 Connection의 수가 증가해서 메모리 누수가 발생하고, 커넥션도 부족해진다.

그래서 try/catch/finally를 통해서 리소스를 반환한다.

public void deleteAll() throws SQLException{
	...
    
    try{
    	c=dataSource.getConnection();
        ...
    }
    catch(SQLException e ){
    	handling error ...
    }
    finall{
    	// Must return Resources!!!
    	if(ps!=null){
        	ps.close();
        }
        
        if(c!=null){
        	c.close();
        }
    }
}

JDBC 조회 기능의 에외처리

Connection, PreparedStatement + ResultSet
ResultSet도 반드시 반환해야 한다.


c = dataSource.getConnection();
ps = c.prepareStatement(...);
rs = ps.executeQuery();

/*
	c -> ps -> rs순으로 진행이 되므로,
    반드시 안쪽 rs부터 역순으로 close를 해야만 한다.
*/

3.2 변하는 것과 변하지 않는 것

3.2.1 JDBC try/catch/finally 코드의 문제점

try/catch/finally 블록이 이중으로 중첩되었고, 모든 메소드마다 반복이 된다.

그래서 가변/불변을 구분지어야 한다.

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

  1. 메소드 추출
    변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 함. -> 반대로가 적절함.

  2. 템플릿 메소드 패턴의 적용
    상속을 통해 기능을 확장해서 사용하는 부분
    불변 = 슈퍼 클래스,
    가변 = 추상 메소드로 둬서 서브 클래스에서 오버라이드하도록!

그런데 DAO로직마다 상속을 통해 새로운 클래스를 만들어야 한다.
4개면 4가지의 서브 클래스 생성해야 함 -> 비효츌적임.

  1. 전략 패턴의 적용
    오브젝트를 아예 둘로 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 한다.

3.3 JDBC 전략 패턴의 최적화

변하지 않는 부분을 템플릿으로 생성

3.3.1 전략 클래스의 추가 정보

public class AddStatement implements StatementStrategy{
	User user;
    
    // 외부에서 주입을 받는다.
    public AddStatement(User user){
    	this.user = user;
    }
    
    public PreparedStatement makePreparedStatement(Connection c){
    	...
        ps.setString(1,user.getId());
        ps.setString(2,user.getNamer());
        ps.setString(3,user.getPassword());
        ...
    }
}

이런식으로 외부에서 주입을 받도록 설계를 한다.
(코드의 양을 많게 70~80%까지 줄일 수 있음)

3.3.2 전략과 클라이언트의 동거

문제상황

  1. DAO 메소드마다 새로운 StatementStartegy 구현 클래스를 만들어야 한다.
    (이는 클래스 파일의 개수가 많이 늘어난다.)

  2. DAO 메소드에서 외부에서 주입되는 User와 같은 부가적인 정보가 있을 경우,
    이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 생성을 해야 한다.

로컬 클래스

클래스 파일이 많아지는 경우, 전략 클래스를 매번 독립된 파일로 만들지 말고
UserDao 클래스 안에 내부 클래스로 정의한다.

public void add(User user) {
	
    class AddStatement implements StatementStrategy{
    ...
    public AddStatement(User user){...}
    public PreparedStatement makePreparedStatement(Connection c){...}
    
    }
    
}

중첩 클래스의 종류
중첩 클래스는 다른 클래스 내부에 정의되는 클래스를 의미하며,
독립적으로 오브젝트로 만들어질 수 있는 "스태틱 클래스"와
자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 "내부 클래스"로 구분된다.


내부 클래스는
오브젝트 레벨에 정의되는 "멤버 내부 클래스",
메소드 레벨에 정의되는 "로컬 클래스",
이름을 갖지 않는 "익명 내부 클래스"
로 구성된다.

위 예제는 "로컬 클래스"로 선언이 되었으며, 선언된 메소드 내에서만 사용할 수 있다.
(클래스 파일 1개 줄일 수 있다.)

로컬 클래스는 또한 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근을 할 수 있다.
(굳이 생성자를 통해서 주입을 받을 필요가 없다.)

다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야 한다.

public void add(final User user) ...{
	class AddStatement implements StatementStrategy{
    ...
    }
    
    StatementStrategy st = new AddStatement();// 생성자 파라미터로 user를 전달하지 않아도 된다.
    jdbcContext...(st);
}

클래스 파일을 줄일 수 있는 장점이 있다.

익명 내부 클래스

AddStatement 클래스는 add()메소드에서만 사용할 용도로 만들어졌기 때문에 익명으로도 가능하다.

StatementStrategy st = new StatementStrategy(){ 

	@Override
    public PreparedStatement makePreparedStatement(Connection c){...}
    
}

3.4 컨텍스트와 DI

3.4.1 JdbcContext의 분리

public class JdbcContext{
	private DataSource dataSource;
    
    public void setDataSource(DataSource dataSource){
    	...this.datasource = datasource;
    }
    
    public void workWithStatementStrategy(StatementStrategy stmt){Error handling logic}
}
public class UserDao{
	private jdbcContext jdbcContext;
    
    public void setJdbContext(JdbcContext jdbcContext){
    	...
    }
    
    public void add(final User user){...}
    
    public void deleteAll(){...}

}

UserDao와 JdbcContext를 완전히 분리를 시킴

스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다.

그런데, JdbcContext는 완전한 구현체이지만, 딱히 바뀔일이 없어서 그냥 써도 될듯하다.

3.4.2 JdbcContext의 특별한 DI

UserDao와 JdbcContext 사이에는 인터페이를 사용하지 않고 DI를 적용했다.
( 의존 오브젝트의 구현 클래스를 변경할 수는 없다.)

스프링 빈으로 DI

무조건 인터페이스를 통해서 주입할 필요는 없다.

원래는 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고,
런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입하는 것이 맞다.
(인터페이스를 사용하지 않았다면 엄밀히 온전한 DI라고 볼 수 없다.)

그러나 객체의 생성과 관계 설정에 대한 제어 권한을 오브젝트에서 제거하고 외부로 위임했기 때문에 IoC를 충실히 따르고 있고,
이는 DI를 따른다고 볼 수 있다.

그런데 왜 JdbcContext를 UserDao와 DI 구조로 만들어야 하는가?

1)
JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 때문이다.
(그 자체로 변경되는 상태정보는 없다.)

싱글톤으로 등록이 되어서 여러 오브젝트에서 공유해 사용되는 것이 이상적이다.

2)
JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.(핵심)

DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다.
(스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문)

3)

드물게 인터페이스를 사용하지 않고 등록이 가능한데 왜 그럴까?

굳이 다른 구현으로 대체가 될 필요가 없어서 강한 응집도를 유지해도 큰 문제가 없다.

코드를 이용하는 수동 DI

이 방법은 싱글톤으로 만들려는 것은 포기해야 한다.

무식하게 DAO가 호출될 때마다 JdbcContext 오브젝트를 새로 만드는 것은 아니다.

DAO마다 하나의 JdbcContext 오브젝트를 가지게 한다.

JdbcContext는 내부에 상태 정보가 없기 때문에 오브젝트가 많이 만들어져도 메모리에 주는 부담은 거의 없다.

자주 만들어졌다가 제거되는 게 아니기 떄문에 GC에 대한 부담도 없다.

@Bean 등록이 되지 않았다는 것은 UserDao가 책임을 진다.

JdbContext는 빈 등록이 되지 않아서 DI가 안되는데 이를 UserDao가 관리한다.

public class UserDao{
	...
    private JdbcContext jdbcContext;
    
    public void setDataSource(DataSource dataSource){...}
 
}

이 방법의 장점은 굳이 인터페이스로 분리할 필요가 없이 긴밀한 관계를 유지한다는 것에 있다.

3.5 템플릿과 콜백

기존 설계는 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고
그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 구조이다.

전략 패턴의 기본 구조는 익명 내부 클래스를 활용한 방식이다.
이를 "템플릿/콜백 패턴"이라고도 부른다.

전략 패턴의 컨텍스트를 템플릿이라 부르고,
익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.

  • 템플릿
    어떤 목적을 위해 미리 만들어둔 모양이 있는 틀

  • 콜백
    다른 오브젝트의 메소드에 전달되는 오브젝트
    (파라미터로 전달되지만, 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다.)

3.5.1 템플릿/콜백의 동작원리

템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다

콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다.

템플릿/콜백의 특징

여러개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴과 달리
템플릿/콜백 패턴의 콜백은 보통 "단일 메소드"인터페이스를 사용한다.
(특정 기능을 위해 한 번 호출되는 경우가 일반적임)

  • 클라이언트는 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고,
    콜백이 참조할 정보를 제공하는 것이다.
    만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출 시 파라미터로 전달된다.

  • 템플릿은 정해진 로직에 따라 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다.
    콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조 정보를 이용해서 작업을 수행하고, 결과를 다시 템플릿에 반환

  • 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행 후,
    경우에 따라 최종 결과를 클라이언트로 반환한다.

클라이언트가 템플릿 메소드를 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨에서의 DI다.

일반적인 DI는 템플릿에 인스턴스 변수 만들고, 사용할 의존 오브젝트를 setter 메소드로 받아서 사용하는 것이나,
템플릿/콜백은 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달받는 것이 특징이다.

이를 통해서 클라이언트와 콜백이 강하게 결합되었음을 알 수 있다.
(전략 패턴, DI 결합)

JdbcContext에 적용된 템플릿/콜백

템플릿과 클라이언트가 메소드 단위인 것이 특징.

UserDao에서 콜백 Object를 만든 다음, Template에서 실행을 한 후, 결과를 반환하든가 예외처리를 하든가 한다.

3.5.2 편리한 콜백의 재활용

이를 통해서 클라이언트인 DAO의 메소드는 간결해지고 최소한의 데이터 엑세스 로직만 갖고 있게 된다.

그러나 한가지 문제점이 있다.

매번 익명클래스를 생성해야만 한다.

콜백의 분리와 재활용

분리를 통해 재사용이 가능한 코드를 찾아낼 수 있다면,
익명 내부 클래스를 사용한 코드를 간결하게 만들 수 있다.

중복될 가능성이 있는 자주 바뀌지 않는 부분을 분리해서 재활용하면 된다.

// 변하는 SQL 문장
public void deleteAll() throws SQLException {
	executeSql("delete from users");
}

// 아래 final String에 "delete from users"을 주입하면 된다.
private void executeSql(final String query) throws SQLException {
	this.jdbcContext.workWithStatementStrategy{
    	new StatementStrategy(){...}
    }
}

변하는 것과 변하지 않는 것을 분리하고 변하지 않는 건 유연하게 재활용할 수 있게 만든다(핵심)

콜백과 템플릿의 결합

public으로 바꿔서 다른 오브젝트들도 사용할 수 있게 하자.

3.5.3 템플릿/콜백의 응용

기본적으로 OCP를 지키고, 전략 패턴과 DI를 바탕에 깔고 있으니
원한다면 언제든지 확장해서 편리한 방법으로 사용할 수 있다.

코정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리하자

가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다.

(일정한 리소스를 만들거나 가져와서 작업하면서 예외처리하는 패턴이 서로 비슷하다.
왜냐하면, 예외상황을 처리하기 위한 catch와 리소스를 반납하거나 제거하는 finally가 비슷한 패턴이기 때문이다.)

중복의 제거와 템플릿/콜백 설계

EX)
템플릿은 파일을 열고 각 라인을 읽어올 수 있는 BufferedReader를 만들어서 콜백에게 전달해주고,
콜백이 각 라인을 읽어서 알아서 처리한 후,
최종 결과만 템플릿에게 반환하는 것이다.

-> 리소스를 연결하고 닫는 작업은 템플릿이 하고,
세부적인 처리는 콜백이 한다.

public Integer calcSum(String filepath) {
	BufferedReaderCallback sumCallback =
    new BufferedReaderCallback(){ read file.txt ...}
    
    
    return fileReadTemplate(filepath,sumCallback);// return value
}

public class CalcSumTest{

	@Test public void sumOfNumbers(){
    	assertThat(calcuator.calcSum(this.numFilepath),is(10))
    }
}

제네릭스를 이용한 콜백 인터페이스

제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메소드를 정의할 수 있다.

3.6 스프링의 JdbcTemplate

스프링이 제공하는 템플릿/콜백 기술을 살펴보자

3.7 정리

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

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

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

  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현할 수도 있다.

  • 컨텍스트는 별도의 빈으로 등록해서 DI받거나 클라이언트 클래스에서 직접 생성해서 사용한다.

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

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

  • 제네릭스를 통해서 템플릿, 콜백 타입을 다양하게 한다.

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

profile
구구구구구!

0개의 댓글