Skip to content
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
162 changes: 162 additions & 0 deletions chapter03/3.3_JDBC_전략_패턴의_최적화.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# 3.3 JDBC 전략 패턴의 최적화

> 3.2에서 전략 패턴을 적용하여 변하지 않는 부분(컨텍스트)과 변하는 부분(전략)을 분리했다.
>
> `deleteAll()` 메소드에서 변하지 않는 JDBC 작업 흐름은 `jdbcContextWithStatementStrategy()` 메소드로, 변하는 PreparedStatement 생성 로직은 `StatementStrategy` 인터페이스를 구현한 `DeleteAllStatement` 클래스로 분리했다.

## 3.3.1 전략 클래스의 추가 정보

`deleteAll()`과 달리 `add()` 메소드에는 user라는 **부가적인 정보**가 필요하다. 클라이언트가 `AddStatement`에 user 정보를 전달해줘야 하므로, 생성자를 통해 User를 제공받도록 만든다.

### AddStatement 클래스

```java
public class AddStatement implements StatementStrategy {
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(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}
```

### add() 메소드 수정

이제 `add()` 메소드에서 `AddStatement`를 생성할 때 User 정보를 전달한다.

```java
public void add(User user) throws SQLException {
StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
```

이렇게 `deleteAll()`과 `add()` 두 군데에 전략 패턴이 적용되었다. 변하지 않는 JDBC 작업 흐름은 컨텍스트 메소드에서 공유하고, 바뀌는 부분(PreparedStatement를 만드는 전략)만 각각의 전략 클래스에서 담당한다.

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

현재 구조에도 두 가지 문제점이 있다.

1. **DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다.** 기능이 늘어날 때마다 클래스 파일이 계속 증가한다.
2. **StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다.**

### 해결법 1. 로컬 클래스

전략 클래스를 매번 독립된 파일로 만들지 않고, UserDao의 메소드 안에 **로컬 클래스**로 정의할 수 있다. `AddStatement`는 `add()` 메소드에서만 사용되므로, 메소드 레벨에 정의해도 문제없다.

```java
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy {
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(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}

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

로컬 클래스의 장점은 클래스 파일이 줄어들 뿐 아니라, 메소드 안에서 정의하기 때문에 **자신이 선언된 메소드의 로컬 변수에 직접 접근**할 수 있다는 것이다. `add()` 메소드의 파라미터인 `user` 변수에 직접 접근할 수 있으므로, 생성자를 통해 User를 전달해줄 필요가 없다. 다만 내부 클래스에서 외부의 변수를 사용할 때는 `final`로 선언해줘야 한다.

```java
public void add(final User user) throws SQLException {
class AddStatement implements 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;
}
}

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

> [!NOTE]
> **로컬 클래스**는 선언된 메소드 안에서만 사용할 수 있다. 클래스가 내부에 정의되어 있어 코드의 응집도가 높아지고, 해당 메소드의 로컬 변수에 직접 접근할 수 있어 생성자를 통한 데이터 전달이 필요 없어진다.

### 해결법 2. 익명 내부 클래스

`AddStatement`는 `add()` 메소드에서만 사용할 목적으로 만들어졌다. 좀 더 간결하게 클래스 이름도 제거할 수 있다.

> 익명 내부 클래스는 선언과 동시에 오브젝트를 생성한다. 이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다.

**add() 메소드의 익명 내부 클래스 적용:**

```java
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());
ps.setString(3, user.getPassword());

return ps;
}
}
);
}
```

**deleteAll() 메소드의 익명 내부 클래스 적용:**

```java
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
);
}
```

`DeleteAllStatement`와 `AddStatement` 두 개의 클래스를 별도로 만들 필요 없이, 각 메소드 안에서 익명 내부 클래스로 전략을 직접 구현하여 사용한다. 코드가 간결해지고, 메소드의 로컬 변수를 직접 사용할 수 있어 별도의 생성자도 필요 없다.

### 전략 패턴 최적화 과정 정리

- 독립된 전략 클래스 (DeleteAllStatement, AddStatement)
↓ 클래스 파일이 많아지는 문제
- 로컬 클래스 (메소드 안에 클래스 정의)
↓ 로컬 변수 직접 접근 가능, 생성자 불필요
- 익명 내부 클래스 (이름 없는 클래스, 선언과 동시에 생성)
↓ 가장 간결한 형태


> 이 과정에서 **변하지 않는 것**(JDBC 작업 흐름, try/catch/finally)과 **변하는 것**(PreparedStatement 생성)의 분리라는 원칙은 계속 유지된다. 달라지는 것은 전략을 정의하고 전달하는 방식이 점점 간결해진다는 것이다. 이 구조는 이후 **템플릿/콜백 패턴**의 기초가 된다.

## 질문
- 로컬 클래스에서 외부 변수를 참조할 때 `final`로 선언해야 하는 이유는 무엇인가?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q. 로컬 클래스에서 외부 변수를 참조할 때 final로 선언해야 하는 이유는 무엇인가?

  • 내부 클래스는 바깥 메서드의 지역 변수를 직접 참조하는 것이 아니라 복사된 값을 사용하기 때문에 변경 가능한 변수는 허용하지 않습니다. Java 7 까지는 final 선언을 명시적으로 해줘야 했으나, Java 8 부터는 선언하지 않아도 되지만 사실상 final 인 값이어야만 합니다.

- 익명 내부 클래스 방식과 독립된 전략 클래스 방식은 각각 어떤 경우에 적합한가?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q. 익명 내부 클래스 방식과 독립된 전략 클래스 방식은 각각 어떤 경우에 적합한가?

  • 전략이 한 번만 사용되고 마는 경우에는 익명 내부 클래스 방식이 적합하지만, 전략이 여러 곳에서 재사용되는 경우에는 독립된 전략 클래스 방식이 더 적합합니다.