템플릿이란 코드에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로 부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.  쉽게 생각해서 코드에서 변하는 부분과 변하지 않는 부분을 분리시키자라는 의미이다.

아래 코드를 한번보자.  deleteAll() 함수와 add함수에서 함수별로 반복적으로 나타나는 부분(변화되지 않는 부분) 과 함수별로 다른 부분을 찾을수 있다.

두함수의 변하는 부분을 템플릿 메소트 패턴을 이용하여  따로 빼보자.

하지만 여기서도 문제가 존재한다. 새로운 DAO logic이 추가될때 마다 상속을 통해 새로운 클래스를 만들어한다는 것이다.

 

그래서 이번에는 전략패턴을 적용해 보도록 하자.  전략패을 적용하기 전에 용 변하는 부분을 인페이스로 만들어 보자.

시작전에  Context는 변하지 않는 부분, 전략은 바뀌는 부분  <- 이것을 머리에 넣어두고 아래글을 보면 더 이해가 쉽다.

deleteAll() 컨텍스트를 정리해보면 다음과 같다.

  • DB커넥션 가져오기
  • PreparedStatement를 만들어줄 외부기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기

여기서 두번째 PreparedStatement를 만들어줄 외부기능 <- 이부분이 전략패턴에서 전략이라고 볼수 있다.  전략패턴의 구조를 따라 이부분을 인터페이스로 만들어 인터페이스메소드를 통해 PreparedStatement를 생성하는 전략을 호출해주면 된다. 이때 PreparedStatement를 생성할때 Context에서 만든 DB커넥션을 전달해줘야 한다.

전략 interface를 다음과 같이 만들어보자.

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public interface StatementStrategy {
	public PreparedStatement makeStatement(Connection c) throws SQLException; 
}

전략  interface를 상속받아 실제 전략 클래스를 만들면 다음과 같다.

public class DeleteAllStatement implements StatementStrategy {

	@Override
	public PreparedStatement makeStatement(Connection c) throws SQLException {
		// TODO Auto-generated method stub
		PreparedStatement ps=c.prepareStatement("delete from users");
		return ps;
	}
}

이제 delete전략 클래스를 실제 deleteAll메소드에 적용해 보도록 하자.

public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		StatementStrategy strategy= new DeleteAllStatement();
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c);
			
			ps.executeUpdate();
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}	

그런데 컨텍스트 안에 이미 구체적인 적략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다. 컨텍스트 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략패턴에도 OCP(Open Closed Principle)에도 맞지 않다.

DI 적용을 위한 클라이언트/컨텍스트 분리

전략패턴에 따르면 컨텍스트가 어떤 전략을 사용할지는 클라인트가 결정하는게 일반적이다. 클라이언트가 구체적인 전략 하나를 오브젝트로 만들어 컨텍스트에 전달해준다. 그러면 컨텍스트는 전달받은 오브젝트를 사용하게된다. 이를 표현 한것이 아래 그림이다.

결국 이구조에서 전략 오브젝트를 생성하고 컨텍스트로 전달 하는 것은 Object Factoty(client)이며, 이것이 바로 의존관계 주입(DI)에 해당된다. 이제 이패턴을 적용시켜보자. 아래 코드를 다시 살펴보면 try/catch/finally 부분이 context에 해당되며 StatementStrategy strategy= new DeleteAllStatement();  이부분이 전략에 해당된다.

public void deleteAll() throws SQLException {
		Connection c = null;
		PreparedStatement ps = null;
		StatementStrategy strategy= new DeleteAllStatement();
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c);
			
			ps.executeUpdate();
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}	

 

이제 context를 함수로 분리해보자. context은 전략을 전달 받아야 하니, 메소드 작성시  파라미터로 전략을 넣어주자.

public void jdbcContextWithStatementStrategy(StatementStrategy strategy) throws SQLException{
		Connection c = null;
		PreparedStatement ps = null;
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c);
			ps.executeUpdate();
			
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}

이제 클라이언트에 해당하는 deleteAll()부분도 수정해보자.  여기서 보면, 전략 객체 strategy를 만들어 Context에 넘겨 주고 있음을 확인할 수 있다. ( 이것이 바로 DI 이다)

public void deleteAll() throws SQLException {		
		StatementStrategy strategy= new DeleteAllStatement(); //1.선택한 전략 클래스의 오브젝트를 생성  
		jdbcContextWithStatementStrategy(strategy);           //2. 선택한 전략을 context에 넣어준다. 		
}	

이번에는 add()함수에서 사용할 AddStatement를 만들어보자.  

전략 Interface StatementStartegy 상속받은 AddStatement 클래스를 하나 만들었다.  그런데 여기서 DeleteAllStatement전략 클래스와 다른점이 있다. 바로 Usesr정보이다. 클라이언트에서 User정보를 넘겨주어야 한다. 그래서 생성자로 User정보를 넘겨받도록 수정하였다. 

public class AddStatement implements StatementStrategy {
	User user;
	public AddStatement(User user) {
		super();
		this.user = user;
	}
	@Override
	public PreparedStatement makeStatement(Connection c) throws SQLException {
		// TODO Auto-generated method stub
		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;
	}
}

 이제 AddStatement 전략 클래스를 Context로 넘겨주도록 add() 함수를 수정 하면 아래와 같다. 전략 AddStatement 의 객체 생성시 user정보를 넘겨주었다. 그리고 strategy를 넘겨주어 PreparedStatement를 받고 있음을 확인할 수 있다.

public void add(User user) throws SQLException {
    // 1.선택한 전략 클래스의 오브젝트를 생성  , user정보 넘겨줌
	StatementStrategy strategy = new AddStatement(user);  
    //2. 선택한 전략을 context에 넣어준다. 
	jdbcContextWithStatementStrategy(strategy);
	
}

그런데 지금까지 해온 방법에는 두가지 문제가 있다.

  1. DAO 메소드마다 새로운 StaementStrategy 구현 클래스를 만들어야 하는점
  2. User와 같이 부가적인 정보가 있을때 전략클래스에 오브젝트를 전달 받는 생성자와 이를 저장할 인스턴스 변수를 생성해야 한다는 점

지금부터 이 두가지 문제를 해결해 보도록 하자.

첫번째 문제의 해결법은 간단하다. 아래와 같이 로컬 클래스를 사용하면 된다. 그러면 매번 전략 클래스을 따로 만들 필요가 없다.  다음 해결법을 시작하기전에 아래 add()함수를 한번 더보자. Addstatement 클래스 안에 user정보를 받는 생성자가 없다. 로컬 클래스를 사용하면 자신이 선언된 곳의 변수를 직접 사용할수 있기 때문이다.단, 로컬클래스에서 외부 변수를 사용할때는 final로 선언해줘야 한다. 그래서 add()함수의 파라미터 앞에 final을 선언했다. (user은 외부변수다.) 

public void deleteAll() throws SQLException {		
	class DeleteAllStatement implements StatementStrategy {
		@Override
		public PreparedStatement makeStatement(Connection c) throws SQLException {
			PreparedStatement ps=c.prepareStatement("delete from users");
			return ps;
		}
	}
	StatementStrategy strategy= new DeleteAllStatement(); // 이부분을 로컬 클래스로 변경
	jdbcContextWithStatementStrategy(strategy);		
}	
public void add(final User user) throws SQLException {
	class AddStatement implements StatementStrategy {
		@Override
		public PreparedStatement makeStatement(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;
	    }
	}
	StatementStrategy strategy = new AddStatement();  
	jdbcContextWithStatementStrategy(strategy); 
}

여기서 좀더 간단하게 고쳐보자. AddStatement와 DeleteAllStatement 클래스는 해당 메소드에서만 사용되는 클래스이이므로 간결하게 클래스 이름도 제거해보자. 익명 클래스를 이용하면 가능하다. 

익명 내부 클래스는  이름을 갖지 않는 클래스이다. 클래스 선언과 오브젝트 생성이 결합된 형태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.
 
new 인터페이스 이름( ) { 클래스 본문 }

아래는 익명 클래스로 수정한 코드다. 코드가 한결 간결해졌다.

public void deleteAll() throws SQLException {		
	jdbcContextWithStatementStrategy(
		new StatementStrategy() {
			public PreparedStatement makeStatement(Connection c) throws SQLException {
				PreparedStatement ps=c.prepareStatement("delete from users");
				return ps;
			}
		}				
	);		
}	
public void add(final User user) throws SQLException {	
	jdbcContextWithStatementStrategy(
		 new StatementStrategy() {
			 public PreparedStatement makeStatement(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;
				}
		 }				
	);
}

 

JDBC Context의 분리 

우리가 지금까지 UserDao클래스 내부에서 사용했던 jdbcContextWithStatementStrategy() 메소드는 JDBC의 일반적인 작업 흐름을 담고 있기때문에 다른 DAO에서도 사용할 가능성이 크다. UserDao클래스 밖으로 독립시켜서 모든 DAO가 사용가능 하도록 분리해보자. JdbcContext 클래스를 만들고  jdbcContextWithStatementStrategy() 메소드를workWithStatementStrategy() 메소드로 이름만 변경했다.

public class JdbcContext {
	private DataSource dataSource;
	
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	public void workWithStatementStrategy(StatementStrategy strategy) throws SQLException{
		Connection c = null;
		PreparedStatement ps = null;
		try {
			c = dataSource.getConnection();	
			ps=strategy.makeStatement(c); // <-- 이부분이 바뀌는 부분이다.. "전략" 적용하는 부분 
			ps.executeUpdate();
			
		}catch(SQLException e) {
			throw e;
		}finally {
			if(ps != null) {
				try {
					ps.close();
				}catch(SQLException e) {
					
				}
			}
			if(c != null) {
				try {
					c.close();
				}catch(SQLException e) {
					
				}
			}
		}
	}	
}

 그리고 deleteAll()메소드와 add()메소드에서 DI가 일어나는 부분의 함수 호출 부분을 아래와 같이 변경되었다.  

public class UserDao{
	JdbcContext jdbcContext;	
	public void setDataSource(DataSource dataSource) {		
		this.jdbcContext = new JdbcContext();	 
		this.jdbcContext.setDataSource(dataSource);
		this.dataSource = dataSource;
	}
	public void deleteAll() throws SQLException {		
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() {
				public PreparedStatement makeStatement(Connection c) throws SQLException {
					PreparedStatement ps=c.prepareStatement("delete from users");
					return ps;
				}
			}				
		);	
	}	
	public void add(final User user) throws SQLException {	
		this.jdbcContext.workWithStatementStrategy(
			 new StatementStrategy() {
				 public PreparedStatement makeStatement(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;
					}
			 }				
		);
	}
}

스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 JdbcContext는 구현방법이 바뀔 가능성이 없다. 따라서 인터페이스로 구현하지 않았다. 그리고 한가지 재미 있는 점은 UserDAO에서 JdbcContext 에 직접 dataSource를 DI 해주고 있다는 점이다. 이 방법은 스프링에서 종종 사용되는 기법이다.

+ Recent posts