UserDao.java
import org.springframework.dao.EmptyResultDataAccessException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws SQLException {
Connection c = dataSource.getConnection();
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());
ps.executeUpdate();
ps.close();
c.close();
}
public User get(String id) throws ClassNotFoundException, SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
User user = null;
if(rs.next()){
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("Name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
if(user == null) throw new EmptyResultDataAccessException(1);
return user;
}
public void deleteAll() throws SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
public int getCount() throws SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select count(*) from users");
ResultSet rs = ps.executeQuery();
rs.next();
int count = rs.getInt(1);
rs.close();
ps.close();
c.close();
return count;
}
}
UserDao.java
public void deleteAll() throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try{
c=dataSource.getConnection();
ps = c.prepareStatement("delete from users");
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){
}
}
}
}
UserDao.java
public int getCount() throws SQLException{
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("select count(*) from users");
rs = ps.executeQuery();
rs.next();
return rs.getInt(1);
} catch (SQLException e){
throw e;
}finally {
if(rs!=null){
try{
rs.close();
}catch (SQLException e){
}
}
if(ps != null){
try{
ps.close();
}catch (SQLException e){
}
}
if (c!= null){
try{
c.close();
}catch (SQLException e){
}
}
}
문제의 핵심은 중복되는 코드와 로직에 따라 확장되며 자주 변하는 코드를 잘 분리하는 작업이다.
public void deleteAll() throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try{
c=dataSource.getConnection();
// 변하는 부분
ps = c.prepareStatement("delete from users");
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){
}
}
}
}
그럼 변하지 않는 부분을 효율적으로 재사용할 수 있는 방법은 무엇일까?
지금 해야될 것은 변하지 않는 부분의 재사용이 필요하다.
UserDaoDeleteAll.java
public class UserDaoDeleteAll extends UserDao{
protected PreparedStatement makeStatement(Connection c) throws SQLException{
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
추상 클래스가 된 UserDao.java
public abstract class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
...
}
deleteAll() 메소드에서 변하지 않는 부분이 contextMethod()가 된다.
deleteAll()에서 변하는 부분은 PreparedStatement를 만들어주는 외부 기능이 전략(Strategy)가 된다.
- ex) Connection.prepareStatement("delete from users"); 만 유연해야됨.
StatementStrategy.java
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() throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try{
c=dataSource.getConnection();
DeleteAllStatement strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
}catch (SQLException e){
throw e;
...
컨텍스트가 OCP잘 지키려면 특정 구현 클래스가 컨텍스트에 존재해서는 안된다.
전략패턴에 따르면 Context가 어떤 전략을 사용할지는 Client가 결정
Client가 구체적인 전략 하나를 선택하여 오브젝트로 만들고 Context에 전달
UserDao.jdbcContextWithStatementStrategy
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try{
c=dataSource.getConnection();
// 전략이 담길 그릇?
ps = stmt.makePreparedStatement(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){} }
}
}
public void deleteAll() throws SQLException{
// 선정한 전략 클래스의 오브젝트 생성
DeleteAllStatement st = new DeleteAllStatement();
// 컨텍스트 호출, 전략 오브젝트 전달
jdbcContextWithStatementStrategy(st);
}
마이크로 DI
- 때로는 원시적인 전략 패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 가질 수도 있다.
- DI가 매우 작은 단위의 코드와 메소드 사이에 일어나기도 한다.
- DI의 장점을 단순화하여 IoC 컨테이너의 도움 없이 코드내에 적용한 경우를 마이크로DI라고도 한다.
여기까지 링크
https://github.com/SpringFrameworkStudy/LeeJooHyun/tree/main/2week/problemDao/version3.2.2/src
문제점
DAO 메소드마다 새로운 StatementStrategy 구현 클래스(위의 DeleteAllStatement)를 만들어 하며, 기존의 UserDao때보다 클래스 파일의 갯수가 많이 늘어난다.
DAO 메소드에서 StatementStrategy에 전달할 추가적인 오브젝트가 있는 경우, 오브젝트를 전달받는 생성자, 인스턴스 변수를 새롭게 만들어야 한다.
1번의 문제점인 클래스 파일이 많아지는 문제를 해결할 수 있다.
StatementStrategy 구현 클래스를 UserDao 안에 정의하는 방법이다.
DeleteAllStatement, AddStatement는 UserDao 밖에서는 사용되지 않고, 둘 다 UserDao에서만 사용되며, UserDao 로직과 강하게 결합되어 있으므로 적용할 수 있는 방법이다.
변경 전 UserDao.add()
public void add(User user) throws SQLException {
AddStatement st = new AddStatement(user);
}
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy{
User user;
public AddStatement(User user) {
this.user = user;
}
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users() values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
AddStatement st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy{
User user;
public AddStatement(User user) {
this.user = user;
}
public void add(final User user) throws SQLException {
class AddStatement implements StatementStrategy{
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users() values(?,?,?)");
// 로컬 클래스의 코드에서 외부의 메소드 로컬 변수에 접근 가능.
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
// 생성자 파라미터로 user를 전달하지 않아도 된다.
AddStatement st = new AddStatement();
jdbcContextWithStatementStrategy(st);
}
메소드마다 추가해야 했던 클래스 파일을 줄일 수 있고,
로컬 변수를 바로 가져다 쓸 수 있다.
클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우 유용하다.
AddStatement 클래스는 add() 메소드에서만 사용할 용도로 만들어졌다. 클래스 이름도 굳히 필요 없으므로 제거해 보자.
변경 후 UserDao.add()
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() values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
);
}
jdbcContextWithStatementStrategy() 는 다른 DAO에서도 사용이 가능하므로 UserDao 클래스 밖으로 독립시켜 모든 DAO 클래스가 사용할 수 있도록 해보자.
public class JdbcContext {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException{
Connection c = null;
PreparedStatement ps = null;
try{
c=dataSource.getConnection();
ps = stmt.makePreparedStatement(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){} }
}
}
}
public class UserDao {
private JdbcContext jdbcContext;
public void setJdbcContext(JdbcContext jdbcContext) {this.jdbcContext = jdbcContext;}
public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { .. }
);
}
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3307/problem"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
<bean id="userDao" class="UserDao">
<property name="dataSource" ref="dataSource"/>
<property name="jdbcContext" ref="jdbcContext"/>
</bean>
<bean id="jdbcContext" class="JdbcContext">
<property name="dataSource" ref="dataSource"/>
</bean>
인터페이스를 적용하지 않고 DI를 하는것.. 문제가 있지 않나?
상관은 없다. 상황에 따라 사용하지만 인터페이스를 적용한 DI를 권고한다. 하지만 클래스로 DI를 적용해야 하는 상황도 있다. 그 방법들을 알아보자.
그럼 굳이 왜 DI 구조로 만들어야 했을까?
JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이된다.
JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다.
위 2가지 이유는 인터페이스를 사용해도 해결되는데, 왜 굳이 인터페이스를 사용하지 않는가?
인터페이스가 아닌 구현체를 스프링 빈으로 등록하는게 맘에 안들면 해당 방법이 있다.
UserDao내부에서 직접 DI를 적용하는 방법이다.
이런 경우, JdbcContext에 대한 제어권을 가진 UserDao에게 DI도 맡겨야 한다.
JdbcContext에 주입해줄 DataSource는 UserDao가 대신 DI 받도록 하고 내부적으로 UserDao 오브젝트를 JdbcContext에 DI한다.
UserDao.java
public class UserDao {
private JdbcContext jdbcContext;
// 수정자 메소드이면서, JdbcContext에 대한 생성, DI작업을 동시에 수행한다.
public void setDataSource(DataSource dataSource) {
this.jdbcContext = new JdbcContext();
// 의존 오브젝트 DI
this.jdbcContext.setDataSource(dataSource);
}
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3307/problem"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
<bean id="userDao" class="UserDao">
<property name="dataSource" ref="dataSource"/>
</bean>
위 2가지 방법 모두 장단점이 존재하며, 알잘딱으로 적용하면 되것다.
템플릿/콜백 패턴 - 전략 패턴 + 익명 내부 클래스를 활용한 방식
전략 패턴의 컨텍스트 - 템플릿
익명 내부 클래스로 만들어지는 오브젝트 - 콜백
템플릿
- 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
- 프로그래밍에서는 고정된 틀 안에서 바꿀 수 있는 부분을 넣어서 사용하는 경우
콜백
- 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
- 파라미터로 전달되나, 값을 참조하기 위함이 아닌 특정 로직을 담은 메소드를 실행시키기 위해 사용
- 자바에서는 메소드 자체를 파라미터로 전달하는 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달 -> functional object
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.jdbcContext = new JdbcContext();
this.jdbcContext.setDataSource(dataSource);
this.dataSource = dataSource;
}
private JdbcContext jdbcContext;
public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
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 User get(String id) throws SQLException {
Connection c = this.dataSource.getConnection();
PreparedStatement ps = c
.prepareStatement("select * from users where id = ?");
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
c.close();
if (user == null) throw new EmptyResultDataAccessException(1);
return user;
}
public void deleteAll() throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
return c.prepareStatement("delete from users");
}
}
);
}
public int getCount() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select count(*) from users");
ResultSet rs = ps.executeQuery();
rs.next();
int count = rs.getInt(1);
rs.close();
ps.close();
c.close();
return count;
}
}
위의 코드에서 메소드별로 변하는 부분은 SQL부분이다.
deleteAll() 메소드에서는 "delete from users"라는 문자열로 된 SQL 문장만 파라미터로 넘기면 원하는 기능이 동작할 것이다.
최종적으로는 아래의 형태가 될 것이다.
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(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) {} }
}
}
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
public void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
return c.prepareStatement(query);
}
}
);
}
executeSql()을 UserDao 뿐만 아니라 다른 Dao에서도 사용고자 한다.
executeSql(final String query) 메소드도 JdbcContext로 옮기면 아래의 형태가 된다.
JdbcContext.java
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(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) {} }
}
}
public void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
return c.prepareStatement(query);
}
}
);
}
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("delete from users");
}
public class UserDao{
...
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource){
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.dataSource = dataSource;
}
위 링크를 보면 템플릿/콜백 패턴으로 sql을 전달하는 로직을 볼 수있다.
public void deleteAll() {
this.jdbcTemplate.update("delete from users");
}
ex)
public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
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 add(final User user) {
this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
user.getId(), user.getName(), user.getPassword());
}
스프링 3.2.2 버전부터 deprecated 되었고, queryForObject()로 사용을 권장한다고 한다.
UserDao.getAll()
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id",
new RowMapper<User>() {
public User mapRow(ResultSet rs, int rowNum)
throws SQLException {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
}
});
}