2020-4-27 토비의 스프링 3장 템플릿

토비의 스프링 3장 템플릿

public void deleteAll() throws SQLException {
		Connection c = dataSource.getConnection();

		PreparedStatement ps = c.prepareStatement("delete from users");
		ps.executeUpdate();

		ps.close();
		c.close();
	}
  • Connection, PrepareStatement라는 공유리소스를 가져오는데, 정상속으로 끝나면 close()를 하지만 중간에 예외가 발생하면 실행을 끝마치지 못하고 메서드를 빠져나가는 문제가 발생. 이렇게 예외가 발새해서 Connection을 반환하지 못하면 커넥션 풀이 부족하고 애플리케이션 전체가 종료될 수 있다.
  • connection.close(), prepared statement.close()는 리소스를 반환하는 의미로 이해하라. 미리 정해진 풀안에 제한된 수의 리소스를 만들어 필요할때 할당하고 다쓰고 반환하는 방식으로 운영.
  • JDBC에서는 try-catch-finally를 사용하도록 권장
public void deleteAll() throws SQLException {
  Connection c = null;
  PreparedStatement ps = null;
  try {
    c = dataSource.getConnection();
    ps = c.prepareStatement("delete from users");
  } catch (SQLException e) {
    throw e;
  } finally {
    if (ps != null) {
      try {
        ps.clearParameters();
      } catch (SQLException e) {
      }
    }
    if (c != null) {
      try {
        c.close();
      } catch (SQLException e) {
      }
    }
  }
}
  • 예외 발생할 수 있는 코드를 모두 try-catch로 감싼다. finally를 붙여서 예외가 발생하나 안하나 실행되도록 설계.
  • ps.close()에서 SQLException이 발생하면 Connection.close()하지 못하고 메서드를 빠져나갈 수 있다.
  • 예외상황
    • getConnection을 가져오다가 예외발생. c=null인데, null에서 close()를 하면 NPE발생.
    • PreparedStatement생성하다가 예외발생하면 c는 커넥션을 가져서 close()가 가능. ps는 안된다. ps를 실행하다가 예외가 발생하면 ps, connection모두 close()를 해야한다.
    • close()도 예외가 발생할 수 있다. 그래서 try-catch를 써야된다.
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) {
				}
			}
		}
	}
  • Try-catch를 중첩으로 모든 메서드에 넣을 수 없다… 이를 위해 변하는 부분과 변하지 않는 부분을 나누어서 분리해야한다. 템플릿메소드 패턴을 적용해야한다.

템플릿메서드 패턴의 적용

  • 템플릿메서드 패턴은 상속을 통해 기능을 확장시켜서 사용한다. 변하지 않는 부분을 슈퍼클래스로 두고, 변하는 부분을 추상메서드로 정의해서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록한다.
//문제
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
  • 이렇게 만들면 UserDao메서드가 4개일때 4개의 서브클래스를 만들어야되기때문에 단점이 더 많다. 전략패턴을 써서 해결한다.

전략패턴의 적용

  • 개방폐쇄의원칙(OCP)를 잘지키면서 템플릿 메서드패턴보다 더 유연하고 확장성이 뛰어나다. 오브젝트를 아예 둘로 분리하고 클래스레벨에서는 인터페이스를 통해서만 의존하게 끔 한다.
  • PreparedStatement를 만들어주는 외부기능이 전략패턴에서 말하는 전략이다.
public interface StatementStrategy {
    PreparedStatement makePreaparedStatement(Connection c) throws SQLException;
}

public class DeleteAllStatement implements StatementStrategy {
    public PreparedStatement makePreaparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

// UserDao method
public void deleteAll() throws SQLException {
  ...
  try {
    c = dataSource.getConnection();
    StatementStrategy strategy = new DeleteAllStatement();
    ps = strategy.makePreaparedStatement(c);
    ps.executeUpdate();
  } ....
}
  • 전략패턴을 적용한 코드이지만, 컨텍스트안에 전략클래스 DeleteAllStatement가 사용되도록 고정하면 이상하다.

  • 컨텍스트가 StatementStrategy뿐만 아니라 구현체인 DeleteAllStatement까지 알고있으면 OCP에 어긋난다.

  • DI적용을 위해 클라이언트와 컨텍스트를 분리해야한다.

  • 전략패턴에서 Context가 어떤 전략을 사용할지는 Context를 사용하는 Client가 결정하는게 일반적. Client가 구체적인 전략하나를 선택해서 Context에 전달하는것이다.

  • 전략 오브젝트 생성과 컨텍스트의 전달을 담당하는 책임을 분리한것이 ObjectFactory이고, 일반화한것이 DI였다.

    • public void deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
      }
      
      public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
      		Connection c = null;
      		PreparedStatement ps = null;
      		try {
      			c = dataSource.getConnection();
      			ps = stmt.makePreaparedStatement(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()메서드가 클라이언트 역할을 한다. 전략오브젝트를 만들고 컨텍스트를 호출하는 책임이 있다.

    • dao메서드들이 jdbcContextWithStatementStrategy를 공유할 수 있게 되었다.DAO메서드는 전략패턴의 클라이언트로 컨텍스트에 해당하는 jdbcContextWithStatementStrategy()에 적절한 전략을 제공할 수 있다.
  • AddUser에 대한 전략패턴

    • public class AddStatement implements StatementStrategy {
        private User user;
      
        public AddStatement(User user) {
          this.user = user;
        }
      
        public PreparedStatement makePreaparedStatement(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;
        }
      }
      
      // UserDao 메서드
      public void add(User user) throws SQLException {
      		StatementStrategy st = new AddStatement(user);
      		jdbcContextWithStatementStrategy(st);
      	}
      
  • 문제점

    • DAO메서드마다 새로운 StatementStrategy 구현 클래스를 만들어야한다. UserDao와 관련된 클래스파일이 많이 늘어난다.
    • StatmentStrategy에 전달할 User같은 부가정보가 있으면 생성자와 인스턴스 변수를 번거롭게 해야한다.
    • 위의 2가지 문제를 해결하기 위해 로컬클래스를 사용한다.

로컬클래스

  • 클래스파일이 많아지면 StatementStrategy전략클래스를 클래스안에 내부클래스로 정의해라. DeleteAllStatement, AddStatement 둘 다 UserDao밖에서 사용되지 않는다.
public void add(User user) throws SQLException {
   class AddStatement implements StatementStrategy {
      private User user;

      public AddStatement(User user) {
         this.user = user;
      }

      public PreparedStatement makePreaparedStatement(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 st = new AddStatement(user);
   jdbcContextWithStatementStrategy(st);
}
  • 중첩클래스는 독립적으로 오브젝트를 만들어질 수 있는 static class와 자신이 정의된 클래스 오브젝트 안에서만 만들어지는 내부(inner class)로 구분된다.

  • add()안에 AddStatment클래스를 로컬클래스로서 사용한거다. 로컬클래스는 선언된 메서드 내부에서만 사용가능. 내부클래스이기때문에 선언된 곳의 정보를 접근할 수 있다. 따라서 AddStatement에 User를 전달할 필요가 없다. 다만 내부클래스에서 외부 변수를 사용할때 외부 변수는 반드시 final이여야 한다.

    • public void add(final User user) throws SQLException {
        class AddStatement implements StatementStrategy {
          public PreparedStatement makePreaparedStatement(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 st = new AddStatement();
        jdbcContextWithStatementStrategy(st);
      }
      

익명클래스

  • 내부클래스가 아닌 익명클래스를 이용해서 클래스 이름도 제거할 수 있다. 익명내부클래스

  • new 인터페이스이름() {클래스본문}

    • public void add(final User user) throws SQLException {
      		StatementStrategy st = new StatementStrategy() {
      			public PreparedStatement makePreaparedStatement(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;
      			}
      		};
      		jdbcContextWithStatementStrategy(st);
      	}
      // refactoring한번더
      public void add(final User user) throws SQLException {
      		jdbcContextWithStatementStrategy(new StatementStrategy() {
      			public PreparedStatement makePreaparedStatement(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;
      			}
      		});
      	}
      
    • 익명내부 클래스를 한번만 사용하기 때문에 변수에 담지 말고 jdbcContextWithStatementstrategy() 메서드의 파라미터에서 바로 생성하는것이 좋다. deleteAll()도 메서드 파라미터 방식으로 리팩토링하면된다.

컨텍스트와 DI

  • 전략패턴에서 보면 UserDao의 메서드가 클라이언트이고, 익명 내부 클래스가 개별적인 전략이고 jdbcContextWithStatementStrategy()는 컨텍스트
  • 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔 사용하는게 목적 또한 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의된다.
  • jdbcContextWithStatementStrategy는 일반적인 흐름을 담고 있어서 다른 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 = this.dataSource.getConnection();
            ps = stmt.makePreaparedStatement(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) {} }
        }
    }
}

//DAO Class Method
public void add(final User user) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
			public PreparedStatement makePreaparedStatement(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;
			}
		});
	}
  • DB커넥션과 관련된 부분을 JDBCContext로 다 옮겼다.
  • UserDao => JdbcContext => DataSource 방향으로 의존하고 있다.
  • JdbcContext의 경우 인터페이스로 정의하지 않고 구체적인 클래스를 통해 DI를 하고 있다. DI라는 개념에서 보면 인터페이스를 둬서 의존관계가 클래스레벨이 아니라 런타임 시에 의존하도록 설계해야되지만, 스프링의 DI는 넓게보면 객체의 생성과 관계설정에 대한 제어권을 오브젝트에서 제거하고 외부로 위임했다는 IoC개념을 포괄한다. JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입한건 DI의 기본을 따르고 있다고 할 수 있다.
JdbcContext, UserDao를 DI 구조로 해야되는 이유
  • JdbcContext가 싱글톤빈이다. 변경되는 상태정보를 가지고 있지않기때문에 싱글톤이 되는데 문제가 없다.
  • JDBC컨텍스트 메소드를 제공해주는 서비스 오브젝트이기때문에 싱글톤으로 사용해야한다.
  • JdbcContext가 DI를 통해 다른 빈에 의존하고 있기때문에.

템플릿과 콜백

  • 전략패턴의 기본구조에 익명 내부 클래스를 활용한 방식을 스프링에서는 템플릿/콜백 패턴이라 한다. 전략패턴의 컨텍스트를 템플릿이라 하고, 익명내부클래스로 만들어지는 오브젝트를 콜백이라 부른다.
  • 템플릿 메서드패턴 : 고정된 메서드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이뤄진다. 서브클래스의 경우 상위클래스에서 추상메서드로 정의
  • 콜백 : 다른 오브젝트의 메서드에 전달되는 오브젝트. 템플릿안에서 사용하기 위해 전달. 값을 참조하기 위해서가 아닌 특정 로직을 담은 메서드를 실행시키기 위해 사용된다. 자바에서는 메서드를 파라미터로 전달할 방법이 없어서 메소드가 담긴 오브젝트를 전달. Functional Object라 한다.
  • DI를 할때 일반적으로 변수를 만들어주고 전달하는 것이 아닌, 메서드 단위로 사용할 오브젝트를 전달하는 것이 특징.
public void add(final User user) throws SQLException {
		this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
			public PreparedStatement makePreaparedStatement(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;
			}
		});
	}
  • userDao가 전략패턴에서 클라이언트 역할을 한다. jdbcContext.workWithStatementStrategy가 템플릿역할이고, new StatementStrategy() {… }이 콜백으로 전달되는 파라미터, 여기서는 전달되는 메서드가 담긴 오브젝트다.
  • jdbcContext.workWithStatementStrategy() 템플릿은 리턴값이 없는 단순한 작업이다.
  • DAO메서드에서 매번 익명내부 클래스를 사용해서 코드를 작성하고 읽기가 불편하다. 따라서 콜백의 분리와 재활용을 해야한다.
public void deleteAll() throws SQLException {
  this.jdbcContext.executeSql("delete from users");
}

// JdbcConext로 이동.
private void executeSql(final String query) throws SQLException {
  this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
    public PreparedStatement makePreaparedStatement(Connection c) throws SQLException {
      return c.prepareStatement(query);
    }
  });
}
  • executeSql로 콜백을 생성하는 부분을 메서드 분리한다.

JdbcTemplate

  • 이전까지 사용하던 JdbcContext보다 훨씬 좋은 기능을 스프링에서는 템플릿/콜백을 위해 JdbcTemplate을 제공한다.
// delete Before
public void deleteAll() throws SQLException {
  this.jdbcTemplate.update(new PreparedStatementCreator() {
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
      return connection.prepareStatement("delete from users");
    }
  });
}
// delete After
public void deleteAll() throws SQLException {
  this.jdbcTemplate.update("delete from users");
}

// add before
public PreparedStatement makePreaparedStatement(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;
}
// add after
public void add(final User user) throws SQLException {
  this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                           user.getId(),
                           user.getName(),
                           user.getPassword());
}
  • 파라미터 전달도 간단하게 할 수 있다.

  • getCount()는 쿼리를 실행하고 ResultSet을 통해 결과값을 가져오는 코드.

    • //Before
      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) {
            }
          }
        }
      }
      //After1
      public int getCount() throws SQLException  {
        return this.jdbcTemplate.query(new PreparedStatementCreator() {
          public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            return connection.prepareStatement("select count(*) from users");
          }
        }, new ResultSetExtractor<Integer>() {
          public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {
            resultSet.next();
            return resultSet.getInt(1);
          }
        });
      }
      // Refactoring
      public int getCount() throws SQLException  {
        return this.jdbcTemplate.queryForInt("select (*) from users");
      }
      
    • queryForInt의 내부는 queryForObject로 동작한다. queryForObject를 Number로 Cascading한거다.

    • queryForObject에서는 Integer.class를 전달해서 getSingleColumnRowMapper에 Integer.class를 전달한다.
  • queryForObject()

    • 기존에 get(String id)를 이용해서 sql where id = id를 통해 조회하던 부분을 구현.

    • // Before
      public User get(String id) throws SQLException {
      		return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{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;
      					}
      				}
      		);
      	}
      
      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;
      					}
      
      // Refactoring
      public User get(String id) throws SQLException {
      		return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id},
      				this.userMapper
      		);
      	}
      
      	public List<User> getAll() {
      		return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
      	}
      
      	private RowMapper<User> userMapper = 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;
      		}
      	};
      
    • rowMapper를 리팩토링하자.
Written on April 27, 2020