3장 템플릿
템플릿 메소드 / 전략 패턴 / 중첩 클래스 / 템플릿 콜백 패턴로 중복 코드 효율적으로 재사용하기
앞서 1장에서는 중복 코드를 메소드로 추출하여 재사용하였다. 이때 중복 코드 중에 변하는 부분/변하지 않는 부분이 섞여 있을 수 있었다. 따라서 변하는 부분은 슈퍼클래스의 추상 메소드로 정의화고 하위 클래스에서 상속을 통해 오버라이딩하였다. 이를 템플릿 메소드 패턴이라고 하고, 이를 통해 복잡한 중복 코드도 효율적으로 재사용할 수 있었다. 여기서는 템플릿 메소드에서 나아가 중복 코드를 효율적으로 개선하는 방법들을 소개한다. 우선 안 좋은 예시부터 개선해나가는 방식으로 각각의 디자인 패턴을 알아보았다.
Bad Case
중복되는 코드가 변하지 않는 코드뿐이라면 메소드로 추출하는게 효율적이다. 그러나 아래의 코드(JDBC
)에서는 메소드로 추출된 부분이 매번 변하는 부분이다. 다음 JDBC를 통해 DB에 접근하는 과정에서 중복되는 코드를 메소드화하였다. 그러나 메소드화한 부분이 쿼리 내용마다 변하기 때문에 재사용이 어렵다.
JDBC로 DB 사용하는 과정
Connection 생성
쿼리 생성
→ 중복되는 코드이지만, 쿼리 내용이 매번 변한다. 아래 코드에서 이 부분을 메소드 추출하였다.
쿼리 실행
Connection 해제
이처럼 변하는 부분을 메소드로 추출한 경우, 재사용하기 어려워 메소드화한 의미가 없다.
이를 개선하여 변하는 부분/변하지 않는 부분을 분리하여 상속을 통해 재사용 + 확장하는 방식이 템플릿 메소드 패턴이다.
1. 템플릿 메소드 패턴
우선 변하지 않는 부분과 변하는 부분을 구분한다. 변하지 않는 부분은 슈퍼 클래스에 정의한다. 그리고 상속을 통해 변하는 부분(슈퍼 클래스의 추상 메소드로 정의)은 하위 클래스에서 재정의한다. 이를 통해 기본 구조는 바뀌지 않으면서 기능 확장을 할 수 있다.
슈퍼 클래스에서 변하는 부분은 추상 메소드로 정의한다. (변하지 않는 부분은 메소드로 구현한다)
하위 클래스에서 각자에 맞게 추상 메소드를 구현한다.
이처럼 템플릿 메소드 패턴은 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분은 하위 클래스에서 오버라이딩해준다. 이를 통해 기능을 확장하고 싶을 때, 기존 구조는 변하지 않으면서 상속을 통해 기능을 확장할 수 있다. (OCP) 예를 들어, 새로운 Dao 로직을 만들고 싶으면 UserDaoDeleteAll
처럼 UserDao
을 상속받아 오버라이딩해주면 재사용하면서도 변경/확장할 수 있다.
그러나 여기에도 문제가 있다. Dao 로직마다 새로운 클래스를 만들어서 상속받아야 한다. 상속 관계가 설계 시점에 정해져 유연성이 떨어지게 된다.(다중 상속이 안되므로, 상속 구조가 하나로 고정되면 유연성/확장성이 떨어진다!) 이를 개선하여 오브젝트를 분리하고, 인터페이스에만 의존하는 방식이 전략 패턴이다.
2. 전략 패턴
전략 패턴이란
오브젝트를 둘로 분리한다.
컨텍스트 : 변하지 않는 부분
전략 인터페이스 : 변하는 부분
컨텍스트는 전략 인터페이스에 의존한다.
전략 패턴은 컨택스트가 추상화된 전략 인터페이스에 의존하는 방식이다. 컨택스트를 바꾸지 않고 필요한 전략을 바꿔가면서 쓸 수 있다.
전략 패턴 예시
아래 그림/코드는 전략 패턴에 클라이언트의 DI 방식을 적용했다. 즉, 클라이언트 코드에서 전략 구현체를 선택하여 컨택스트에 주입한다.
전략 인터페이스
컨텍스트가 만든 연결을 받아서 PreparedStatement를 만들고 만들어진PreparedStatement 오브젝트를 돌려준다.
전략 구현체
전략 인터페이스를 상속받아 구체적으로 어떤 PreparedStatement 오브젝트를 생성할지 구현한다.
컨텍스트 : 변하지 않는 맥락
StatementStrategy
인터페이스를 매개변수로 정의하여, StatementStrategy
를 상속받은 구현체들을 모두 받을 수 있다. (ex. DeleteAllStatement
)
클라이언트 코드
컨택스트에 전략 구현체를 주입한다.
이렇게 전략 패턴을 사용하면, 전략 구현체를 바꾸더라도 기존 컨택스트 코드는 바뀌지 않는다.
단, 이 방법에도 단점이 몇가지 있다.
우선 클라이언트단(UserDaoDeleteAll
)에서 메소드(deleteAll
)마다 새로운 전략 구현체를 매번 만들어야 한다. (클래스 객체를 만드는 과정에 리소스가 든다) 뿐만 아니라, 만약 메소드에서 전략 구현체 생성 시에 전달할 정보들(인자들)이 있다면, 이를 저장해둘 변수를 따로 만들어두어야 한다. 이를 개선하기 위해 중첩 클래스를 활용할 수 있다.
3. 중첩 클래스로 전략 패턴 최적화
중첩 클래스로 전략 패턴을 개선시켜 매번 클래스를 생성하지 고, 변수 사용에 부담을 덜 수 있다. 우선 중첩 클래스의 종류는 다음과 같다.
중첩 클래스(Nested class) : 다른 클래스 내부에 정의되는 클래스
Static class : 독립적인 오브젝트로 생성
Inner class : 자신이 정의된 클래스의 오브젝트 안에서 생성
Local Class : 메소드 내부에 정의, 메소드 실행 시 생성
Anonymous Class : 이름이 없는, 선언 시 생성
Inner Class : 클래스 내부에 정의, 해당 객체 생성 시 생성 (멤버 필드)
로컬 클래스로 최적화
로컬 클래스는 자신이 정의된 메소드 실행 시 생성된다. 메소드마다 클래스 파일을 생성하지 않아도 된다는 장점이 있다. 또한 해당 메소드의 로컬 변수에 바로 접근할 수 있다. (로컬 클래스가 외부 변수를 사용하려면 final로 선언되어야 한다)
익명 내부 클래스
익명 내부 클래스는 선언과 동시에 생성된다. 딱 한 번만 사용하므로 따로 변수에 담지 않고 바로 생성하여 파라미터로 넘겨준다.
4. 템플릿 콜백 패턴
앞서 전략 패턴은 변하는 부분과 변하지 않는 부분을 분리하여, 상속을 통해 변하는 부분을 하위 클래스에서 오버라이딩하였다. 이를 통해 복잡한 코드도 재사용 + 확장을 할 수 있었다.
템플릿 콜백 패턴은 전략 패턴에서 상속 대신 익명 내부 클래스를 활용한 방식이다.
템플릿 : 전략 패턴의 컨텍스트 (바뀌지 않는 코드 부분)
콜백 : 익명 내부 클래스로 만들어지는 오브젝트 (상황에 따라 바뀌는 코드 부분)
콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿은 바뀌지 않는 컨텍스트이고, 콜백은 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 볼 수 있다.
여기서 한 번 더 개선하면 변하는 부분만 인자로 전달하도록 하는 메소드로 추출할 수 있다.
또 이 메소드를 여러 클라이언트(UserDao
외의 클래스들)에서 사용할 경우, 컨텍스트(템플릿 클래스) 안으로 옮겨 재사용할 수 있다.
Last updated