From d44437c77ec28a1400aee2b971f824827b30f418 Mon Sep 17 00:00:00 2001 From: leevigong Date: Sat, 14 Feb 2026 11:05:26 +0900 Subject: [PATCH] docs: 3.3 (#28) --- ...0_\354\265\234\354\240\201\355\231\224.md" | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 "chapter03/3.3_JDBC_\354\240\204\353\236\265_\355\214\250\355\204\264\354\235\230_\354\265\234\354\240\201\355\231\224.md" diff --git "a/chapter03/3.3_JDBC_\354\240\204\353\236\265_\355\214\250\355\204\264\354\235\230_\354\265\234\354\240\201\355\231\224.md" "b/chapter03/3.3_JDBC_\354\240\204\353\236\265_\355\214\250\355\204\264\354\235\230_\354\265\234\354\240\201\355\231\224.md" new file mode 100644 index 0000000..1f29496 --- /dev/null +++ "b/chapter03/3.3_JDBC_\354\240\204\353\236\265_\355\214\250\355\204\264\354\235\230_\354\265\234\354\240\201\355\231\224.md" @@ -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`로 선언해야 하는 이유는 무엇인가? +- 익명 내부 클래스 방식과 독립된 전략 클래스 방식은 각각 어떤 경우에 적합한가?