객체 지향과 디자인 패턴
객체 지향과 디자인 패턴
객체 지향 특성(캡슐화, 상속, 추상화, 다형성) : 요리도구
객체 지향 설계 5원칙 (SOLID) : 요리도구를 올바르게 사용하는 방법
디자인 패턴 : 레시피
요리도구를 사용하여 요리를 만들때, 하나의 요리에도 표준화된 요리법이 있듯이 프로그래밍에도 표준화된 해결책이 있다.
디자인(설계) 패턴
- 프로그램을 작성하다보면 비슷 비슷한 상황에 직면하게 되는 경우가 많은데, 그러한 상황에서 이전에 많은 개발자들이 고민하고 정제한 사실 상의 표준 설계 패턴이다.
- 실제 개발 현장에서 비즈니스 요구사항을 프로그래밍으로 처리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 best practice이다.
- 객체 지향 특성과 설계 원칙을 기반으로 구현한다.
스프링 프레임워크
- 공식적인 정의 : java enterprise 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크
- 또다른 정의 : OOP 프레임워크
- 스프링은 객체 지향 특성과 설계 원칙을 극한까지 적용한 프레임워크이기에 스프링 공부시 자연스럽게 객체 지향 설계의 best practice인 디자인 패턴을 만나게 된다.
디자인 패턴과 객체 지향 특성
디자인 패턴은 객체 지향의 특성 중 상속(extends), 인터페이스(implements), 합성(객체를 속성으로 사용)을 이용한다.
위 3 가지 특성만 사용하므로 여러 디자인 패턴이 비슷해 보일 수 있다.
합성
class Car {
private Engine engine;
private Wheel[] wheel;
}
한 객체의 인스턴스가 다른 객체의 인스턴스를 소유하거나 포함하는 것
자동차 객체는 엔진 객체와 바퀴 객체를 합성(Compositon)한다.
어댑터 패턴(Adapter Pattern)
어댑터 = 변환기 : 서로 다른 두 인터페이스 사이에 통신이 가능하게 하는 것
ODBC/JDBC는 어댑터 패턴을 이용해 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있다.
플랫폼별 JRE도 어댑터 패턴이라고 할 수 있다.
OCP(개방 폐쇄 원칙)을 활용한 설계 패턴이다. JDBC와 JRE가 어댑터 역할을 수행한다.
어댑터 패턴 적용 X
public class ServiceA{
void runServiceA(){
System.out.println("서비스 A");
}
}
public class ServiceB{
void runServiceB(){
System.out.println("서비스 B");
}
}
public class Client{
public static void main(String[] args){
ServiceA sa1 = new ServiceA();
ServiceB sb1 = new ServiceB();
sa1.runServiceA();
sb1.runServiceB();
}
}
Client에서 바로 서비스를 호출한다.
- 각 서비스에 대해 다른 이름의 메서드를 각각 호출해주어야한다.
- 같은 역할을 하는 메서드는 같은 이름으로 호출할 수 있도록 어댑터 패턴을 적용한다.
어댑터 패턴 적용
serviceA와 serviceB에 대한 각각의 어댑터를 생성한다.
public class AdapterServiceA{
ServiceA sa1 = new ServiceA(); // 호출할 객체를 속성으로 만든다.
void runService(){
sa1.runServiceA(); // 어댑터에서 ServiceA 호출
}
}
호출할 객체를 속성으로 만든 후 호출한다.
public class AdapterServiceB{
ServiceB sb1 = new ServiceB(); // 호출할 객체를 속성으로 만든다
void runService(){
sb1.runServiceB(); // 어댑터에서 ServiceB 호출
}
}
어댑터를 사용하자.
public class ClientWithAdapter{
public static void main(String[] args){
AdapterServiceA asa1 = new AdapterServiceA();
AdapterServiceB asb1 = new AdapterServiceB();
asa1.runService();
asb1.runService();
}
}
어댑터를 이용하면 동일한 이름의 메서드로 서로 다른 타입의 객체를 호출할 수 있다.
여기서 더 나아가서 변환기들이 인터페이스를 구현하게 해서 개선할 수도 있다.
+ 개선 : 인터페이스 도입
public interface AdapterInterface{
void runService(); // 추상 메서드
}
public class AdapterServiceA implements AdapterInterface{
ServiceA sa1 = new ServiceA();
void runService(){
sa1.runServiceA(); // 어댑터에서 ServiceA 호출
}
}
public class AdapterServiceB implements AdapterInterface{
ServiceB sb1 = new ServiceB();
void runService(){
sb1.runServiceB(); // 어댑터에서 ServiceB 호출
}
}
public class ClientWithAdapter{
public static void main(String[] args){
ArrayList<AdapterInterface> adapterList = new ArrayList<>();
adapterList.add(new AdapterServiceA());
adapterList.add(new AdapterServiceB());
for (AdapterInferface adapter: adapterList){
adapter.runService();
}
}
}
같은 인터페이스 참조 타입이며 같은 이름의 메서드를 호출한다.
(실제 객체는 다르므로 객체에 따라 서비스가 실행된다. - 다형성)
어댑터를 도입함으로써 서로 다른 인터페이스를 가진 클래스들을 호환 가능하게 만들어준다.
인터페이스 도입시 장점
- 유연성 : 객체들이 동일한 인터페이스(계약)을 구현하므로 객체들의 교체가 용이하다.
- 확장성 : 새로운 어댑터를 추가하더라도 기존의 인터페이스를 구현하면 되므로 다른 어댑터를 수정할 필요가 없다.
- 유지보수성 : 개발자는 내부 로직(구현 클래스의 내용)을 모르더라도 사용 클래스(인터페이스)만 알면 사용이 가능하다.
OCP와 DIP를 준수할 수 있다.
결론 : 어댑터 패턴이란?
어댑터 패턴은 합성, 즉 객체를 속성으로 만들어서 참조하는 디자인 패턴이다.
호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴이다.
runServiceA(), runSerivceB()를 main()에 대응하도록 중간에 AdapterServiceA(), AdapterServiceB()를 통해 호출하는 패턴이다.
프록시 패턴(Proxy Pattern)
프록시 = 대리자, 대변인
프록시(대리자)를 사용하지 않고 직접 호출
public class Service{
public String runSomething(){
return "run~";
}
}
public class Client{
public static void main(String[] args){
Service service = new Service();
System.out.println(service.runSomething()); // 직접 서비스 호출
}
}
프록시 패턴 적용
프록시 패턴의 경우 실제 서비스 객체가 가진 메서드와 같은 이름의 메서드를 사용하는데, 이를 위해 인터페이스를 사용한다.
인터페이스를 이용하면 호출하는 쪽(클라이언트)에서 직접 서비스를 호출하는지 대리자를 통해 서비스를 호출하는지 모르게 처리할 수 있다.
1. 인터페이스 생성 - 호출할 메서드를 정의한다.
public interface IService {
String runSomething();
}
2. 실제 서비스 코드에 인터페이스를 구현한다.
public class Service implements IService{
public String runSomething(){
return "run~";
}
}
서비스 코드에 인터페이스를 구현한다. (기존 코드에 implements IService 부분만 추가됨)
3. 프록시 생성 : 인터페이스를 구현한 프록시를 생성한다.
public class Proxy implements IService{
IService service1;
public String runSomething(){
service1 = new Service();
return service1.runSomething(); // 서비스 메서드를 그대로 호출한다.
}
}
- 인터페이스를 참조 타입으로 하여 서비스 객체를 만들고, 서비스의 메서드를 호출한다.
- 프록시에서는 서비스를 그대로 호출하여 결과를 반환한다.
이 코드에서 실제 서비스 객체 생성시 인터페이스 타입으로 참조한 이유는 프록시 클래스에서 구체적인 서비스 객체에 의존하지 않고 인터페이스에 의존하기 위해서이다.(DIP)
후에 다른 서비스를 생성해야할 경우에 인터페이스를 구현하기만 한다면 프록시 코드를 바꿀 부분이 적어진다. (OCP)
4. 클라이언트 : 프록시 호출
public class ClientWithProxy {
public static void main(String[] args){
IService proxy = new Proxy();
System.out.println(proxy.runSomething());
}
}
클라이언트는 프록시를 통해 서비스 메서드를 호출할 수 있다.
프록시는 서비스 메서드 결과를 그대로 반환한다.
이 코드에서 Proxy 생성시 인터페이스로 참조한 이유는 인터페이스를 통해 프록시 객체와 실제 서비스 객체를 동일한 타입으로 취급할 수 있고 동일한 메서드 호출 방식을 유지할 수 있다.
클라이언트 코드를 변경하지 않더라도 실제 객체 대신 프록시 객체를 사용할 수 있다.
프록시 패턴 구현 포인트
- 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
- 대리자는 실제 서비스에 대한 참조 변수를 갖는다(합성)
- 대리자는 실제 서비스와 같은 이름의 메서드를 호출하고 그 값을 클라이언트에게 돌려준다.
- 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다.
프록시 패턴 사용이유
프록시 패턴은 실제 서비스 메서드의 반환값에 가감하는 것을 목적으로 하지 않는다.
제어 흐름을 변경하거나 다른 로직을 수행하기 위해 사용한다.
제어 흐름을 변경한다는 것은 실제 객체에 대한 접근을 제어하는 것이다.
ex ) 메서드 호출 전 가로채어 접근 권한 확인, 캐싱, 로깅 등의 작업을 수행할 수 있다.
ex ) 실제 객체의 생성 시점을 지연시키거나, 객체 생성 방식을 변경할 수 있다.
=> 프록시를 통해 실제 객체에 대한 요청의 흐름을 제어할 수 있다.
public class Proxy implements IService{
IService service1;
public String runSomething(){
System.out.println("메서드 호출 전 로그 출력");
// 서비스 호출
service1 = new Service();
String result = service1.runSomething();
System.out.println("메서드 호출 후 로그 출력");
return result;
}
}
결론 : 프록시 패턴이란?
프록시 패턴은 제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴이다.
프록시 패턴은 DIP와 OCP 원칙이 적용된 패턴이다.
DIP : 인터페이스에 의존
OCP : 클라이언트는 인터페이스에 대해서만 열려있으므로 다양한 인터페이스 구현 객체로 교체할 수 있고, 실제 구현에 대해서는 닫혀 있다. (구현한 내용에 대해 알필요가 없다.)
데코레이터 패턴(Decorator Pattern)
데코레이터 : 장식
데코레이터 패턴 : 원본에 장식을 더하는 패턴
데코레이터 패턴과 프록시 패턴
데코레이터는 프록시 패턴과 구현 방법이 같지만 차이점이 있다.
- 프록시 패턴 : 클라이언트가 최종적으로 돌려 받는 반환값을 조작하지 않고 그대로 반환한다.
- 데코레이터 패턴 : 클라이언트가 받는 반환값에 장식(decorator)를 덧입힌다. (변환값에 가감한다.)
데코레이터 패턴 적용
프록시 패턴과 구현 방식은 같다.
1. 인터페이스 생성 - 호출할 메서드를 정의한다.
public interface IService {
String runSomething();
}
2. 실제 서비스 코드에 인터페이스를 구현한다.
public class Service implements IService{
public String runSomething(){
return "run~";
}
}
서비스 코드에 인터페이스를 구현한다. (기존 코드에 implements IService 부분만 추가됨)
3. 데코레이터 생성 : 서비스의 반환 결과에 장식을 더한다.
public class Decorator implements IService{
IService service1;
public String runSomething(){
service1 = new Service();
return service1.runSomething() + "fast" ;
}
}
서비스의 반환결과 run에 추가적인 장식 fast를 붙인다.
4. 클라이언트 : 데코레이터 호출
public class ClientWithProxy {
public static void main(String[] args){
IService decorator = new Decorator();
System.out.println(decorator.runSomething());
}
}
데코레이터 패턴 구현 포인트
- 장식자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다. - 프록시와 동일
- 장식자는 실제 서비스에 대한 참조 변수를 갖는다(합성) - 프록시와 동일
- 장식자는 실제 서비스와 같은 이름을 가진 메서드를 호출하고, 그 반환값에 장식을 더해 클라이언트에게 돌려준다.
- 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수 있다. - 프록시와 동일
결론 : 데코레이터 패턴이란?
메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴
프록시 패턴과 동일하게 OCP와 DIP 패턴이 적용된 설계 패턴이다.
DIP : 인터페이스에 의존
OCP : 클라이언트는 인터페이스에 대해서만 열려있으므로 다양한 인터페이스 구현 객체로 교체할 수 있고, 실제 구현에 대해서는 닫혀 있다. (구현한 내용에 대해 알필요가 없다.)
싱글턴 패턴(Singleton Pattern)
인스턴스를 하나만 만들어 사용하기 위한 패턴
커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 경우 인스턴스를 여러개 만들게 되면 불필요한 자원을 사용하게 되고, 예상치 못한 결과를 낳을 수 있다.
싱글턴 패턴은 오직 인스턴스를 하나만 만들고 그것을 계속해서 재사용한다.
싱글턴 패턴을 적용할 경우 의미상 두 개의 객체가 존재할 수 없다.
싱글턴 패턴 구현 포인트
- new를 실행할 수 없도록 생성자에 private 접근제어자를 지정한다.
- 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.
- 유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.
정적 참조 변수(static 변수)와 정적 메서드를 사용하는 이유는 클래스 당 유일한 인스턴스를 보장하기 위해서이다.
정적 참조 변수 : 클래스 로딩시 메모리에 할당되며 모든 인스턴스가 공유된다. 모든 인스턴스가 동일한 참조 변수를 공유하므로 유일한 인스턴스를 보장할 수 있다.
정적 메서드 : 정적 메서드는 정적 참조 변수를 반환한다. 정적 메서드를 통해 반환하므로 new 연산자를 사용하지 않아도 된다.
싱글톤 구현 코드
public class Singleton {
static Singleton singletonObject; // 정적 참조 변수, 유일
private Singleton(){}; // private 생성자, 생성자를 통해 인스턴스가 생성되지 못하도록 막음
public static Singleton getInstance(){
if (singletonObject == null){
singletonObject = new Singleton(); // 한번만 생성
}
return singletonObject; // 한번 생성 후에는 해당 객체만 반환
}
}
- 객체가 할당되지 않는 경우에만 new를 통해 객체를 만들고 정적 참조 변수에 할당한다.
- 정적 참조 변수에 할당되어있는 유일한 객체의 참조를 반환한다.
- 서로 다른 객체의 참조변수가 하나의 단일 객체를 참조한다.
싱글톤 객체의 속성
단일 객체의 경우 결국 공유 객체로 사용되므로 속성을 갖지 않는 것이 정석이다.
단일 객체가 속성을 갖게 되면 하나의 참조 변수가 변경한 단일 객체의 속성이 다른 참조 변수에 영향을 미치기 때문이다.
다만 읽기 전용 속성을 갖는 것은 문제가 되지 않는다.
또한 단일 객체가 다른 단일 객체에 대한 참조를 속성으로 가진 것도 문제가 되지 않는다.
스프링 빈이 가져야 할 제약조건이기도 하다.
읽기 전용 속성을 가진 싱글톤 객체
public class Singleton {
private static final Singleton instance = new Singleton();
private final String readOnlyField;
private Singleton() {
readOnlyField = "초기값";
}
public static getInstance(){
if (instance == null){
instance = new Singleton(); // 한번만 생성
}
return instance;
}
public String getReadOnlyField(){ // public getter
return readOnlyField;
}
}
readOnlyField를 static final로 선언할 수도 있지만 그렇게 되면 객체지향 원칙(캡슐화, 정보 은닉)을 따르기 어려우므로 private 필드와 public getter를 사용하는 것이 좋다.
싱글턴 패턴의 특징
- private 생성자를 갖는다.
- 단일 객체 참조 변수를 정적 속성으로 갖는다.
- 단일 객체 참조 변수가 참조하는 단일 객체를 반환하는 getInstance() 정적 메서드를 갖는다.
- 단일 객체는 쓰기 가능한 속성을 갖지 않는 것이 정석이다.
결론 : 싱글턴 패턴이란?
클래스의 인스턴스, 즉 객체를 하나만 만들어 사용하는 패턴이다.
템플릿 메서드 패턴(Template Method Pattern)
템플릿 메서드 패턴 적용 X
public class Dog{
public void playWithOwner(){
System.out.println("놀자");
System.out.println("멍멍!!");
System.out.println("꼬리를 흔든다");
System.out.println("잘했어");
}
}
public class Cat{
public void playWithOwner(){
System.out.println("놀자");
System.out.println("야옹~");
System.out.println("꼬리를 흔든다");
System.out.println("잘했어");
}
}
상속을 이용해 코드를 개선해보자.
템플릿 메서드 패턴 적용
public abstract class Animal{
// 템플릿 메서드 => 공통 로직 수행
public void playWithOwner(){
System.out.println("놀자");
play();
wave();
System.out.println("잘했어");
}
// 추상 메서드 => 하위 클래스에서 구현 강제
abstract void play();
// Hook(갈고리) 메서드 => 하위 클래스에서 선택적으로 오버라이딩
void wave(){
System.out.println("꼬리를 흔든다");
}
}
템플릿 메서드 패턴은 템플릿 메서드, 추상 메서드, 훅 메서드로 구성된다.
- 템플릿 메서드 : 상위 클래스에 공통 로직을 수행한다. 오버라이딩한 추상 메서드/훅 메서드를 호출한다.
- 추상 메서드 : 하위 클래스에 오버라이딩을 강제한다.
- 갈고리(Hook) 메서드 : 하위 클래스에서 선택적으로 오버라이딩한다.
public class Dog extends Animal{
void play(){
System.out.println("멍멍!!");
}
// 오버라이딩 가능
void wave(){
System.out.println("꼬리를 흔든다 멍!");
}
}
public class Cat extends Animal{
void play(){
System.out.println("야옹!!");
}
// 오버라이딩 가능
void wave(){
System.out.println("꼬리를 흔든다 야옹!");
}
}
public class Driver{
public static void main(String[] args){
Animal bolt = new Dog();
Animal kitty = new Cat();
bolt.playWithOwner();
kitty.playWithOwner();
}
}
클래스 다이어그램
시퀀스 다이어그램
템플릿 메서드를 사용하는 경우
- 알고리즘 구조는 동일하지만 특정 단계의 구현이 다른 경우
- 상속 관계의 클래스에 공통적인 기능이 있을 경우
템플릿 메서드의 장점
- 공통 로직을 상위 클래스의 템플릿 메서드에 모아두기 때문에 코드 재사용이 높아진다.
- 상위 클래스의 로직을 수정하지 않고 하위 클래스에서 특정 단계만 오버라이딩할 수 있다.
결론 : 템플릿 메서드 패턴이란?
상위 클래스의 견본(template) 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴
템플릿 메서드 패턴은 클래스 다이어그램에서 보다시피 DIP 원칙을 활용한다.
구현에 의존하지 않고 추상화된 인터페이스에 의존한다.
팩토리 메서드 패턴(Factory Method Pattern)
팩터리 : 객체 생성
팩터리 메서드 : 객체를 생성하여 반환하는 메서드
팩터리 메서드 패턴 : 하위 클래스에서 팩터리 메서드를 오버라이딩하여 객체를 반환하게 하는 것
팩토리 메서드 패턴 구현 예시
public abstract class Animal{
abstract AnimalToy getToy(); // 객체 반환 메서드
}
getToy()는 객체를 반환하는 메서드이고, 추상메서드이므로 오버라이딩을 강제한다.
public abstract class AnimalToy{
abstract void identify();
}
AnimalToy는 추상 클래스로 팩토리 메서드가 생성할 객체의 상위 클래스이다.
Animal을 상속한 클래스들은 서로 다른 getToy() 객체를 반환할 것이므로 인터페이스를 사용한다.
public class Dog extends Animal{
AnimalToy getToy(){ // 객체 반환 메서드 구현
return new DogToy();
}
}
추상 클래스를 구현한 클래스(DogToy)의 인스턴스를 반환한다.
public class DogToy extends Toy{
public void identify(){
System.out.println("강아지가 좋아하는 장난감, 테니스공입니다.");
}
}
public class Cat extends Animal{
AnimalToy getToy(){ // 객체 반환 메서드 구현
return new CatToy();
}
}
public class CatToy extends Toy{
public void identify(){
System.out.println("고양이가 좋아하는 장난감, 캣타워입니다.");
}
}
public class Driver{
public static void main(String[] args){
Animal bolt = new Dog();
Animal kitty = new Cat();
AnimalToy boltBall = bolt.getToy();
AnimalToy kittyTower = kitty.getToy();
}
}
클래스 다이어그램
시퀀스 다이어그램
결론 : 팩터리 메서드 패턴이란?
오버라이드된 메서드가 객체를 반환하는 패턴
전략 패턴(Strategy Pattern)
다자인 패턴의 꽃이라고 할 수 있다.
전략 패턴 구성요소 3가지
- 전략 객체 : 전략 메서드를 가진다.
- 컨텍스트 : 전략 객체를 사용한다. 전략 객체의 사용자/소비자이다.
- 클라이언트 : 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트이다. 제 3자, 전략 객체의 공급자이다.
예시
군인이 있고, 군인은 무기를 사용한다.
보급 장교가 무기를 군인에게 지급해주면, 군인은 주어진 무기에 따라 전투를 수행하게 된다.
여기서 무기는 전략이고, 군인은 컨텍스트, 보급 장교는 제 3자, 클라이언트가 된다.
전략 패턴 적용 코드
전략 인터페이스
public interface Strategy{
public abstract void runStrategy();
}
전략1 - 총
public class StrategyGun implements Strategy{
public void runStrategy(){
System.out.println("탕");
}
}
전략2 - 검
public class StrategySword implements Strategy{
public void runStrategy(){
System.out.println("챙");
}
}
전략3 - 활
public class StrategyBow implements Strategy{
public void runStrategy(){
System.out.println("슝");
}
}
컨텍스트 - 군인
public class Soldier{
void runContext(Strategy strategy){
System.out.println("전투 시작");
strategy.runStrategy(); // 주입받은 전략을 사용한다.
System.out.println("전투 종료");
}
}
클라이언트 - 보급 장교
public class Client{
public static void main(String[] args){
Strategy strategy = null;
Soldier rambo = new Soldier();
strategy = new StrategyGun();
rambo.runStrategy(strategy); // 전략 주입
strategy = new StrategyBow();
rambo.runStrategy(strategy); // 전략 주입
}
}
전략을 다양하게 변경하면서 컨텍스트를 실행할 수 있다.
전략 패턴은 다양한 곳에서 다양한 문제 상황의 해결책으로 사용된다.
전략 패턴과 템플릿 메서드 패턴
전략 패턴과 템플릿 메서드 패턴은 같은 문제를 해결한다.
- 알고리즘의 구조는 같지만, 일부 단계의 구현이 다른 경우
- 상속을 이용하는 템플릿 메서드 패턴*과 객체 주입을 통한 전략 패턴 중에 선택/적용할 수 있다.
java는 단일 상속만 가능하므로 상속이라는 제한이 있는 템플릿 메서드 패턴보다는 전략 패턴이 더 많이 활용된다.
클래스 다이어그램
시퀀스 다이어그램
결론 : 전략 패턴이란?
클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴
OCP와 DIP가 적용된다.
OCP : 새로운 전략이 생성될때 기존 코드에 영향을 주지 않고(closed) 새로운 기능 추가에 대해서만 열려 있다.(open)
DIP : 컨텍스트는 추상화된 인터페이스에 의존한다.
템플릿 콜백 패턴(Template Callback Pattern - 견본/회신 패턴)
전략 패턴의 변형으로 스프링의 3대 프로그래밍 중 하나인 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴이다.
템플릿 콜백 패턴은 전략 패턴과 모든 것이 동일한데, 전략을 익명 내부 클래스로 정의하여 사용하는 것이 특징이다.
전략 패턴 코드를 템플릿 콜백 패턴으로 바꾸기
익명 내부 클래스를 사용하므로 따로 StrategyGun, StrategyBow 등의 전략 인터페이스를 구현한 클래스가 필요 없다.
전략 인터페이스
public interface Strategy{
public abstract void runStrategy();
}
컨텍스트 - 군인
public class Soldier{
void runContext(Strategy strategy){
System.out.println("전투 시작");
strategy.runStrategy(); // 주입받은 전략을 사용한다.
System.out.println("전투 종료");
}
}
클라이언트 - 보급 장교
public class Client{
public static void main(String[] args){
Soldier rambo = new Soldier();
rambo.runStrategy(new Strategy(){
public void runStrategy(){
System.out.println("총!");
}
}); // 익명 클래스로 전략을 생성하여 주입
rambo.runStrategy(new Strategy(){
public void runStrategy(){
System.out.println("슝!");
}
}); // 익명 클래스로 전략을 생성하여 주입
}
}
익명 클래스로 생성시 전략 생성하는 부분의 코드에서 중복되는 부분이 많으므로 리팩토링하자.
리팩토링 : 컨텍스트에 전략 생성 코드를 이관한다
컨텍스트 - 군인
public class Soldier{
void runContext(String weaponSound){
System.out.println("전투 시작");
executeWeapon(weaponSound).runStrategy();
System.out.println("전투 종료");
}
private Strategy executeWeapon(String weaponSound){
return new Strategy(){
public void runStrategy(){
System.out.println(weaponSound);
}
}
}
}
전략을 생성하는데 필요한 재료를 클라이언트로부터 주입받아 전략을 생성한다.
클라이언트 - 보급 장교
public class Client{
public static void main(String[] args){
Soldier rambo = new Soldier();
rambo.runStrategy("총!");
rambo.runStrategy("슝!");
}
}
중복되는 부분을 컨텍스트로 이관했다.
스프링은 이런 형식으로 리팩터링된 템플릿 콜백 페턴을 DI에 적극 활용한다.
스프링을 이해하기 위해서는 전략 패턴과 템플릿 콜백 패턴, 리팩토링된 템플릿 콜백 페턴을 잘 기억하자.
결론: 템플릿 콜백 패턴이란?
전략을 익명 내부 클래스로 구현한 전략 패턴
전략 패턴의 일종이므로 당연히 OCP와 DIP이 적용된 패턴이다.
스프링이 사용한 다른 패턴들
스프링은 8가지 패턴 말고도 여러 디자인 패턴을 사용하고 있다.
스프링 MVC의 경우 프론트 컨트롤러 패턴, MVC 패턴(Model - Vew - Controller)를 활용하고 있다.
프론트 컨트롤러 패턴
클라이언트의 모든 요청을 단일 진입점으로 받아들이는 패턴이다.
스프링에서느 DispatcherServlet이 프론트 컨트롤러 역할을 하여, 클라이언트의 모든 요청을 받아 적절한 핸들러(컨트롤러)에 전달한다.
이를 통해 모든 요청을 중앙에서 처리할 수 있어 보안, 인증, 로깅 등의 처리를 일관되게 할 수 있다.
MVC 아키텍처 패턴
애플리케이션 계층을 세가지 역할로 나누는 아키텍처 패턴이다.
- Model : 애플리케이션의 데이터와 비즈니스 로직을 캡슐화한다.
- 서비스 객체와 도메인 객체로 구현된다.
- View : 클라이언트에 응답할 UI 화면을 렌더링한다.
- JSP, Thymeleaf 등의 뷰 템플릿 엔진 사용
- Controller : 클라이언트의 요청을 처리하고, Model과 View를 연결하는 역할을 한다.
- @Controller 어노테이션 클래스
스프링 작동방식
스프링 MVC는 프론트 컨트롤러 패턴과 MVC 아키텍처 패턴을 결합하여 구현된다.
클라이언트의 요청은 DispatcherServlet에 전달되고, 이 서블릿이 적절한 컨트롤러를 호출한다. 컨트롤러는 모델을 업데이트하고 뷰를 선택하여 클라이언트에 응답한다.
Reference
https://m.yes24.com/Goods/Detail/17350624
+ 책 내용 기반으로 추가적으로 내용을 작성했는데 오개념이 있다면 지적 부탁드립니다~!!