Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[최유정] 2023 GDSC Spring Advanced Study - 3주차 #24

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions 최유정/3장_템플릿.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# 제목 없음

# 템플릿

## 기존 DAO의 문제

- 예외 상황 처리 문제

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

PreparedStatement ps = c.preparedStatement("delete from users");
ps.executeUpdate(); // 여기서 예외가 발생하면 바로 메소드 실행이 중단되면서 DB 커넥션이 반환되지 못한다.

ps.close();
c.close();
}
```

DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그런데 이런 식으로 오류가 날 때마다 미처 반환되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있다.

- try/catch/final 적용

```jsx
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;

try {
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users");
ps.executeUpdate(); // 예외가 발생할 수 있는 코드를 모두 try 블록으로 묶어준다.
} catch (SQLException e) {
throw e; // 예외가 발생했을 때 부가적인 작업을 해줄 수 있도록 catch 블록을 둔다. 아직은 예외를 메소드 밖으로 던지는 것 밖에 없다.
} finally { // finally이므로 try 블록에서 예외가 발생했을 떄나 안 했을 때나 모두 실행된다.
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {} // ps.close() 메소드에서도 SQLException이 밣생할 수 있기 때문에 잡아줘야한다.
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {}
}
}
}
```

어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 c와 ps가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다.문제는 이 close()도 SQLException이 발생할 수 있는 메소드라는 점이다.****


## 디자인 패턴을 적용하여 분리 및 재사용

### 1. 변하는 부분을 메소드로 추출

```jsx
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnectin();
ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출한다.
ps.executeUpdate();
} catch (SQLException e) {...}
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps;
ps = c.preparedStatement("delete from users");
return ps;
}
```

### 2. 템플릿 메소드 패턴의 적용

```jsx
public class UserDaoDeleteAll extends UserDao {
protected PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.preparedStatement("delete from users");
return ps;
}
}
```

변하지 않는 부분을 슈퍼클래스에 두고 변하는 부분을 추상 메소드로 정의해서 서브클래스에서 새롭게 정의해 쓰는 것

⇒ 문제

1) 모든 DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 된다.

2) 컴파일 시점에 클래스 간(슈퍼-서브) 관계가 결정되어 있어서 유연하지 못하다.

### 3. 전략 패턴 적용

```jsx
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();

StatementStrategy strategy = new DeleteAllStatement(); // 전략 클래스가 DeleteAllStatement로 고정됨으로써 OCP 개방 원칙에 맞지 않게 된다.
ps = starategy.makePreparedStatement(c);

ps.executeUpdate();
} catch (SQLException e) {...}
}
```

전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있다는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다

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

```jsx
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) {}
}
}
```

컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시킨다

## JDBC 전략 패턴의 최적화

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

```jsx
public void add(final User user) throws SQLException {
class AddStatement implements StatementStrategy { // add() 메소드 내부에 선언된 로컬 클래
User user;

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

public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
...
}

StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
}
```

- 이전의 개선된 코드의 문제점
- DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점
- DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 전달하고 저장해 둘 생성자와 인스턴스 변수를 번거롭게 만들어야 한다.

### 로컬 클래스

```jsx
public void add(final User user) throws SQLException {
class AddStatement implements StatementStrategy { // add() 메소드 내부에 선언된 로컬 클래
User user;

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

public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
...
}

StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
}
```

StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리면 클래스 파일이 많아지는 문제는 해결할 수 있다.

→ 장점

- AddStatement는 복잡한 클래스가 아니므로 메소드 안에서 정의해도 그다지 복잡해 보이지 않는다.
- 메소드마다 추가해야 했던 클래스 파일을 하나 줄일 수 있다
- 내부 클래스의 특징을 이용해 로컬 변수를 바로 가져다 사용할 수 있다.

### 익명 내부 클래스

```jsx
public void add(final User user) throws SQLException {
jdbcContextWithStatementStrategy(
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();
...
return ps;
}
}
);
}
```

선언과 동시에 오브젝트 생성

## 컨텍스트와 DI

### 컨텍스트 분리하고 DI

JDBC 컨텍스트를 UserDao와 dI 구조로

```jsx
public class JdbcContext {
private DataSource dataSource;

public void setDataSource(DataSource dataSource) { // DataSource 타입 빈을 DI 받을 수 있게 준비
this.dataSource = dataSource;
}

public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;

try {...}
catch (SQLException e) {...}
finally {...}
}
}
```

```jsx
public class UserDao {
...
private JdbcContext jdbcContext;

public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext; // jdbcContext를 Di받도록 만든다.
}

public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy( // DI 받은 JdbcContext의 컨텍스트 메소드를 사용하도록 변경한다.
new StatementStrategy() {...}
);
}
}
```

→ 장점

- JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 되기 때문이다.
- JdbcContext가 DI를 통해 다른 빈에 의존하고 있디 때문이다.

## 템플릿과 콜백

전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

- 템플릿 : 전략 패턴의 컨텍스트
- 콜백: 익명 내부 클래스로 만들어지는 오브젝트

### 콜백의 재활용

- 익명 내부 클래스를 사용

```jsx
public void deleteAll() throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { // 변하지 않는 콜백 클래스 정의와 오브젝트 생성
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.preparedStatement("delete from users"); // 변하는 SQL 문장
}
}
);
}
```

- 변하지 않는 부분을 분리

```jsx
public void deleteAll() throws SQLException {
executeSql("delete from users");
}

private void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { // 변하지 않는 콜백 클래스 정의와 오브젝트 생성
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.preparedStatement(query);
}
}
);
}
```