티스토리 뷰
UserDao 리소스 반환 시 예외처리
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) {
}
}
}
}
- 리소스 반환 부분이 복잡하게 반복된다.
해결 방법1 : 메소드 추출
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = 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) {
}
}
}
}
private PreparedStatement makeStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
- 소득이 없다. 메소드 추출된 부분을 재활용할 수 있어야하는데, 추출된 부분은 계속해서 변하는 부분이기 때문에 재활용이 불가능하다.
해결방법2 : 템플릿 메소드 패턴
- 상속을 통해 기능 확장
- 변하지 않는 부분은 슈퍼클래스에, 변하는 부분은 추상 메소드로 정의 후 서브클래스에서 오버라이드하여 새롭게 정의해서 쓴다.
public abstract class UserDao {
//...
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
//...
}
public class UserDaoDeleteAll extends UserDao {
@Override
protected PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
- 문제점
- DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 함.
해결방법3: 전략 패턴
- 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식
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();
//구체 전략 클래스 --> 클라이언트에서 선택하는 것이 바람직.
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
throw e;
}
//...
}
- 문제점
- 구체 전략 클래스인
DeleteAllStatement
를 사용하도록 고정되어 있음
- 구체 전략 클래스인
- 해결방법
- 구체 전략 클래스를 선택하는 클라이언트 메소드 생성
//클라이언트 역할을 하는 메소드
public void deleteAll() throws SQLException {
StatementStrategy strategy = new DeleteAllStatement();
jdbcContextWithStatementStrategy(strategy);
}
//컨텍스트 메소드
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) {
}
}
}
}
- 전략 패턴의 4 주체를 기억하자
- 클라이언트
- 오브젝트 팩토리(DI 컨테이너)
- 의존 하는 오브젝트1
- 의존 받는 오브젝트2
전략패턴 응용 : 새로운 전략 추가
- add() 메소드 추가
public 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(id, name, password) values(?,?,?)"
);
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
}
}
public void add(User user) throws SQLException {
StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
추가 개선 사항
- 메소드 추가시 클래스파일을 새로 만들어야 함
- User와 같은 부가 정보 전달을 위해 인스턴스 변수와 생성자를 활용해야 함.
해결방법 : 로컬 클래스
//UserDao 내 add() 메소드
public void add(User user) throws SQLException {
//Inner Class
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(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);
}
- 위 코드에서 AddStatement 클래스는 add(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(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);
}
- 익명 내부 클래스를 활용하면 코드가 더욱 간결해진다.
public void add(final User user) throws SQLException {
StatementStrategy st = new StatementStrategy() {
@Override
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;
}
};
jdbcContextWithStatementStrategy(st);
}
- 람다식 활용
public void add(final User user) throws SQLException {
StatementStrategy st = c -> {
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);
}
컨텍스트 분리
- jdbcContextWithStatementStrategy() 를 UserDao 밖으로 분리해서 모든 DAO가 컨텍스트를 사용할 수 있도록 개선
public class JdbcContext {
private DataSource dataSource;
public JdbcContext(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 UserDao(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
//클라이언트
public void add(final User user) throws SQLException {
//콜백
StatementStrategy st = c -> {
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;
};
//템플릿
this.jdbcContext.workWithStatementStrategy(st);
}
public void deleteAll() throws SQLException {
StatementStrategy strategy = c -> {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
};
this.jdbcContext.workWithStatementStrategy(strategy);
}
}
//---------------------------------
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao(jdbcContext());
return userDao;
}
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
return dataSource;
}
@Bean
public JdbcContext jdbcContext() {
return new JdbcContext(dataSource());
}
}
- JdbcContext : 인터페이스를 구현하지 않고 구체 클래스로 바로 활용.
- UserDao - JdbcContext 사이 강결합 발생.
- JdbcContext는 변경될 일 없고, UserDao 는 항상 JdbcContext를 사용해야하기 때문에 문제될 일이 없음.
- UserDao - JdbcContext 사이 강결합 발생.
- 장점 : 오브젝트 간 의존관계가 설정 파일에 드러남
- 단점 : 구체적인 클래스와의 의존관계가 있어서 DI의 근본적인 원칙 위배
변경되지 않는 JdbcContext를 내부로 감추기
public class UserDao {
private JdbcContext jdbcContext;
//DI를 위한 부가적인 코드
public UserDao(DataSource dataSource) {
jdbcContext = new JdbcContext(dataSource);
}
//...
}
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao(dataSource());
return userDao;
}
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
return dataSource;
}
}
- 장점 : JdbcContext가 UserDao 내부에서 만들어지고 사용되면서 둘 사이 관계가 외부에 드러나지 않음.
- 단점 : JdbcContext 를 싱글톤으로 만들 수 없고, DI 를 위한 부가적인 코드 필요.
템플릿/콜백 패턴
전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
전략 패턴의 컨텍스트 == 템플릿
- 고정된 작업 흐름을 가진 코드
익명 내부 클래스로 만들어지는 오브젝트 == 콜백
- 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트
전략 패턴과의 차이점
- 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달 받는다.
- 콜백 오브젝트가 내부 클래스로서, 자신을 생성한 클라이언트 메소드 내 정보를 직접 참조
- 클라이언트와 콜백이 강하게 결합
템플릿/콜백 응용 예시
public class Calculator {
public Integer calcSum(String filePath) throws IOException {
BufferedReaderCallback sumCallback =
new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
};
return fileReadTemplate(filePath, sumCallback);
}
public Integer calcMultiply(String filePath) throws IOException {
BufferedReaderCallback multiplyCallback =
new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer mul = 1;
String line = null;
while ((line = br.readLine()) != null) {
mul *= Integer.valueOf(line);
}
return mul;
}
};
return fileReadTemplate(filePath, multiplyCallback);
}
public Integer fileReadTemplate(String filePath, BufferedReaderCallback callback) throws IOException{
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
}
finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println("e.getMessage() = " + e.getMessage());
}
}
}
}
}
public class CalcSumTest {
Calculator calculator;
String numFilepath;
@BeforeEach
void setUp() {
this.calculator = new Calculator();
this.numFilepath = getClass().getResource("/static/numbers.txt").getPath();
}
@Test
void sumOfNumbers() throws IOException {
Assertions.assertThat(calculator.calcSum(this.numFilepath)).isEqualTo(10);
}
@Test
void multiplyOfNumbers() throws IOException {
Assertions.assertThat(calculator.calcMultiply(this.numFilepath)).isEqualTo(24);
}
}
- 초기 코드에서 변화가 자주 일어나는 부분 : sum 을 계산
- callback으로 분리
- 변하지 않는 부분(공통된 부분) : filePath 를 이용해서 BufferedReader를 만들고, 사칙연산 결과를 반환
- template 으로 만들기
추가 개선
- multiplyCallback, sumCallback 에서 중복되는 코드가 존재한다.
- 결과 저장을 위한 변수 초기화
- bufferedReader를 이용해 마지막 라인까지 순차적으로 읽는다.
- 라인 별 숫자와 결과 변수에 저장된 값을 이용해 계산한다.
- 위와 같이 중복된 로직을 템플릿에 녹여보자.
public interface LineCallback {
Integer doSomethingWithLine(String line, Integer value);
}
public class Calculator {
public Integer calcSum(String filePath) throws IOException {
LineCallback sumCallback =
new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filePath, sumCallback, 0);
}
public Integer calcMultiply(String filePath) throws IOException {
LineCallback multiplyCallback =
new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value * Integer.valueOf(line);
}
};
return lineReadTemplate(filePath, multiplyCallback, 1);
}
public Integer lineReadTemplate(String filePath, LineCallback callback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
Integer res = initVal;
String line = null;
while ((line = br.readLine()) != null) {
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println("e.getMessage() = " + e.getMessage());
}
}
}
}
}
스프링 속 템플릿/콜백 예시 : JdbcTemplate
public class UserDao {
private JdbcTemplate jdbcTemplate;
private RowMapper<User> userMapper =
new RowMapper<User>() {
@Override
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 UserDao(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
public void add(final User user) throws SQLException {
this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)",
user.getId(), user.getName(), user.getPassword());
}
public void deleteAll() throws SQLException {
//jdbcContext.executeSql("delete from users");
this.jdbcTemplate.update(
new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("delete from users");
}
}
);
}
public int getCount() {
// 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;
return this.jdbcTemplate.query(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("select count(*) from users");
}
},
new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
rs.next();
return rs.getInt(1);
}
});
}
public User get(String id) {
// 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;
return this.jdbcTemplate.queryForObject("select * from users where id = ?",
new Object[] {id}, userMapper);
}
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id", userMapper);
}
}
기존 jdbcContext -> 스프링에서 제공하는 jdbcTemplate
변경기존 StatementStrategy 인터페이스 -> PrepardStatementCreator 인터페이스
변경query()
는 callback을 두 개 이상 받을 수 있음.- e.g. getCount() 에서 query() 는 PreparedStatementCreator, ResultSetExtractor 두 개의 callback을 사용.
- 중복되는 RowMapper 인터페이스를 인스턴스 변수로 분리
개선된 userDao 특징
- UserDao 에는 User 정보를 DB에 넣거나 가져오거나 조작하는 방법에 대한 핵심적인 로직만 담겨 있음
- JDBC API 사용하는 방식, 예외처리, 리소스 반납, DB 연결 가져오기 등의 책임과 관심은 JdbcTemplate 에 담겨 있음.
- 위 사항에 대해 변경이 일어나더라도 UserDao 코드에는 영향을 주지 않음. (낮은 결합도)
- 스프링에는 JdbcTemplate 외에도 템플릿/콜백 패턴을 적용한 API가 다수 존재. 클래스 이름이 ’Template’ 으로 끝나거나 인터페이스 이름이 ‘Callback’ 으로 끝날 경우 템플릿/콜백이 적용된 것.
'SPRING' 카테고리의 다른 글
토비의 스프링 5. 서비스 추상화 (0) | 2022.05.27 |
---|---|
토비의 스프링 4. 예외 (0) | 2022.05.27 |
토비의 스프링 2. 테스트 (0) | 2022.05.27 |
토비의 스프링 1. 오브젝트와 의존관계 (0) | 2022.05.27 |
request 스코프(feat. Provider, Proxy) (0) | 2021.05.21 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 자바스터디
- 프로그래머스
- provider
- 카카오
- 김영한
- 데코레이터패턴
- c++
- ec2
- 자바
- 토비의스프링
- 코테
- 코딩테스트
- 템플릿콜백
- 프록시패턴
- OOP
- 객체지향
- AOP
- SOLID
- 프록시
- 토비
- 서비스추상화
- 토비의봄TV
- java
- 디자인패턴
- gracefulshutdown
- 스프링
- 예외처리
- 백기선
- BOJ
- 메서드레퍼런스
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함