스프링이 사랑한 디자인 패턴 #
디자인 패턴이란?
프로그램을 작성하다보면 비슷비슷한 상황에 직면하게 된다. 이러한 상황에서 이전의 많은 개발자들이 고민하고 정제한 (사실상의)표준 설계 패턴이다.
Design Pattern = 설계 패턴
개발을 하면서 사용된 다양한 설계 패턴 중 많은 사람들이 인정한 Best Practice 를 정리한 것이다.
‘객체 지향의 4대 특성’과 ‘객체 지향의 설계 5 원칙’이 기반이 된다.
디자인 패턴은 객체 지향의 특성 중 상속, 인터페이스, 합성(객체를 속성으로 사용하는 것)을 이용한다.
- 어댑터 패턴 (Adapter Pattern)
- 프록시 패턴 (Proxy Pattern)
- 데코레이터 패턴 (Decorator Pattern)
- 싱클톤 패턴 (Singleton Pattern)
- 템플릿 메서드 패턴 (Template Method Pattern)
- 팩토리 메서드 패턴 (Factory Method Pattern)
- 전략 패턴 (Strategy Pattern)
- 템플릿 콜백 패턴 (Template Callback Pattern)
- 이 외 다양한 패턴들
어댑터 패턴 (Adapter Pattern) #
어댑터(adapter) 는 변환기(converter) 라고 할 수 있다.
변환기는 서로 다른 두 인터페이스 사이에 통신(접근, 연결)이 가능하게 하는 것이다.
OCP (개방 폐쇄 원칙) 을 활용한 디자인 패턴이다.
어댑터 패턴은 합성(객체를 속성으로 만들어서 참조하는 것)을 이용한 디자인 패턴이다.
호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기(converter)를 통해 호출하는 패턴이다.
예시 1)
핸드폰을 전원 콘센트에 직접 연결할 수 없다. ‘충전기’ 라는 변환기를 통해 연결할 수 있다.
예시 2)
애플리케이션을 직접 데이터베이스로 연결하지 않고 ODBC, JDBC 라는 변환기를 통해 연결한다.
// 어댑터 패턴을 적용하지 않은 코드
public class ServiceA {
void runServiceA() {
System.out.println("ServiceA");
}
}
public class ServiceB {
void runServiceB() {
System.out.println("ServiceB");
}
}
public class Main {
public static void main(String[] args) {
ServiceA serviceA = new ServiceA();
ServiceB serviceB = new ServiceB();
serviceA.runServiceA();
serviceB.runServiceB();
}
}
// 어댑터 패턴을 적용한 코드
public class ServiceA {
void runServiceA() {
System.out.println("ServiceA");
}
}
public class ServiceB {
void runServiceB() {
System.out.println("ServiceB");
}
}
public class AdapterA {
Service serviceA = new ServiceA(); // " 어댑터 패턴은 합성(객체를 속성으로 만들어서 참조하는 것)을 이용한 디자인 패턴이다. "
void runService() {
serviceA.runServiceA();
}
}
public class AdapterB {
Service serviceB = new ServiceB(); // " 어댑터 패턴은 합성(객체를 속성으로 만들어서 참조하는 것)을 이용한 디자인 패턴이다. "
void runService() {
serviceB.runServiceB();
}
}
public class Main {
public static void main(String[] args) {
// 호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기(converter)를 통해 호출하는 패턴이다.
// ServiceA, ServiceB 의 코드(호출당하는 쪽)를 Main 의 코드(호출하는 쪽)에 맞춘다..?
AdapterA adapterA = new AdapterA();
AdapterB adapterB = new AdapterB();
adapterA.runService(); <-- 메소드명을 통일시켰기에 인터페이스를 구현하게 해서 한 단계 더 개선시킬 수 있다.
adapterB.runService(); <-- 메소드명을 통일시켰기에 인터페이스를 구현하게 해서 한 단계 더 개선시킬 수 있다.
}
}
비슷한 로직이지만 메소드명이 다른 경우가 있다. (처음부터 동일한 메소드명으로 작성이 되어 있고 인터페이스도 잘 설계되어 있다면 상관 없겠지만, 그렇지 않은 경우도 있을 것이다.)
이러한 경우에 어댑터 패턴을 통해 리팩토링을 진행할 수 있는 것 같다.
호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기(converter)를 통해 호출하는 패턴이다.
프록시 패턴 (Proxy Pattern) #
프록시는 대리자(대리인, 대변인) 의 의미를 갖는다.
대리자(대리인, 대변인)는 ‘다른 어떠한 것(누군가)이 그 역할을 대신 수행하는 것‘이다.
프록시 패턴은 실제 객체가 가진 메서드와 같은 이름의 메서드를 사용하고, 이것을 위해 인터페이스를 사용한다.
인터페이스를 사용하면 실제 객체 대신 대리자 객체(이하 가짜 객체)를 주입할 수 있다. 클라이언트 쪽에서는 실제 객체를 이용하는 지, 가짜 객체를 이용하는지 모를 것이다.
OCP(개방 폐쇄 원칙), DIP(의존 역전 원칙) 을 활용한 디자인 패턴이다.
// 프록시 패턴을 적용하지 않은 코드
public class Service {
public String run() {
return "Hello";
}
}
public class Main {
public static void main(String[] args) {
Service service = new Service();
System.out.println(service.run());
}
}
// 프록시 패턴을 적용한 코드
public interface ServiceInterface {
String run();
}
public class Service implements ServiceInterface {
public String run() {
return "Hello";
}
}
public class ProxyService implements ServiceInterface {
Service service; // 합성 (객체를 속성으로 이용한다.)
public String run() {
// 호출에 대한 흐름 제어가 주된 목적이다. 반환하는 값에 대한 변화/수정 없이 그대로 전달한다.
// 실제 메서드가 호출 되기 전/후에 별도의 로직을 수행할 수도 있다.
service = new Service();
return service.run(); // 실제 반환되는 결과값은 변환이나 수정 없이 그대로 전달한다.
}
}
public class Main {
public static void main(String[] args) {
ServiceInterface service = new ProxyService();
System.out.println(service.run());
}
}
" 백악관 ‘대변인’은 해당 기관의 입장을 대변할 뿐, 그 입장에 자신의 입장을 가감하지 않는다. 프록시 패턴도 실제 메서드의 반환값에 가감하지 않는다. 단, 제어의 흐름을 변경하거나 추가적인 로직을 수행하기 위해 사용한다 "
제어 흐름을 조정할 목적으로 중간에 대리자(프록시)를 두는 패턴이다.
데코레이터 패턴 (Decorator Pattern) #
데코레이터는 장식자/도장/도배업자를 의미한다.
원본(원본 값)에 장식을 더한다. 즉 원래 메서드의 반환 값에 어떠한 조작(장식)을 덧입힌다.
프록시 패턴과 구현 방법이 동일하다.
프록시 패턴은 (클라이언트가 받을)최종 반환 값에 어떠한 조작이 없지만, 데코레이터 패턴은 최종 반환 값에 어떠한 조작이 들어간다.
데코레이터 패턴에 장식이 없다면 프록시 패턴과 동일하다.
OCP(개방 폐쇄 원칙), DIP(의존 역전 원칙) 을 활용한 디자인 패턴이다.
// 데코레이터 패턴을 적용한 코드
// 프록시 패턴의 코드 예제와 동일 (클래스명과 return 값만 다르다.)
public interface ServiceInterface {
String run();
}
public class Service implements ServiceInterface {
public String run() {
return "Hello";
}
}
public class DecoratorService implements ServiceInterface {
Service service; // 합성 (객체를 속성으로 이용한다.)
public String run() {
// 실제 메서드에 대한 장식이 주된 목적이다.
// 실제 메서드가 호출 되기 전/후에 별도의 로직을 수행할 수도 있다.
service = new Service();
return "Decorator: " + service.run(); // 실제 반환되는 결과값에 장식이 추가되었다.
}
}
public class Main {
public static void main(String[] args) {
ServiceInterface service = new DecoratorService();
System.out.println(service.run());
}
}
메서드 호출의 반환 값에 변화를 주기 위해 중간에 장식자(데코레이터)를 두는 패턴이다.
싱글톤 패턴 (Singleton Pattern) #
인스턴스를 1개만 만들어 사용하는 패턴이다.
커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 인스턴스에 활용될 수 있다.
인스턴스를 1개만 만들고, 그것을 계속해서 재사용한다.
1개의 단일 객체로 공유되기 때문에 (쓰기 가능한)속성을 갖지 않는 것이 정석이다. 다만, 읽기 전용으로 갖는 것은 문제가 되지 않는다.
구현하기 위한 방법은 다음과 같다.
- new 생성자에 제약을 건다. (private 을 사용한다.)
- 만들어진 인스턴스를 반환해주는 메서드가 필요하다.
- 만들어진 인스턴스를 참조할 정적(static) 참조 변수가 필요하다.
public class Singleton {
static Singleton singletonObj;
private Singleton() { } // private
public static Singleton getInstance() {
if(singletonObj == null) {
singletonObj = new Singleton();
}
return singletonObj;
}
}
public class Main {
public static void main(String[] args) {
// private 생성자이므로 아래와 같이 생성할 수 없다.
// Singleton singleton = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
}
}
Class 의 인스턴스, 즉 객체를 1개만 만들어서 사용하는 패턴이다.
템플릿 메서드 패턴 (Template Method Pattern) #
상위 클래스에서 공통의 로직을 수행하는 템플릿 메서드와 하위 클래스에 오버라이딩을 강제하는 추상 메서드 or 선택적으로 오버라이딩 할 수 있는 Hook 메서드를 두는 패턴을 템플릿 메서드 패턴이라고 한다.
DIP 를 활용한 패턴이다.
// 템플릿 메서드 패턴을 적용하지 않는 코드
public class Dog {
public void playWithOwner() {
System.out.println("놀이 시작");
System.out.println("강아지"); <-- Cat 클래스의 playWithOwner 메서드와 이 부분만 다르다
System.out.println("놀이 휴식");
System.out.println("놀이 끝");
}
}
public class Cat {
public void playWithOwner() {
System.out.println("놀이 시작");
System.out.println("고양이"); <-- Dog 클래스의 playWithOwner 메서드와 이 부분만 다르다
System.out.println("놀이 휴식");
System.out.println("놀이 끝");
}
}
// 템플릿 메서드 패턴을 적용한 코드
public abstract class Animal {
// 템플릿 메서드 (템플릿을 제공한다.)
public void playWithOwner() {
System.out.println("놀이 시작");
play1();
play2();
System.out.println("놀이 끝");
}
abstract void play1(); // 추상 메서드
// Hook 메서드
void play2() {
System.out.println("놀이 휴식");
}
}
public class Dog extends Animal {
@Override
public void play1() {
System.out.println("강아지");
}
@Override
public void play2() {
~~
}
}
public class Cat extends Animal {
@Override
public void play1() {
System.out.println("고양이");
}
@Override
public void play2() {
~~
}
}
상위 클래스의 템플릿 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴이다.
팩토리 메서드 패턴 (Factory Mathod Pattern) #
팩토리는 공장을 의미한다. 즉, 객체를 생성하는 공장을 의미한다.
팩토리 메서드는 객체를 생성/반환하는 메서드를 의미한다.
팩토리 메서드 패턴은 하위 클래스에서 팩토리 메서드를 오버라이딩해서 객체를 반환하게 하는 것을 의미한다. (…?)
DIP 를 활용한 패턴이다.
// 팩토리 메서드 적용된 코드 예시
public abstract class AnimalToy { // 팩토리 메서드가 생성할 객체(DogToy, CatToy)의 상위 클래스
abstract void idenfity();
}
public class DogToy extends AnimalToy { // 팩터리 메서드가 생성할 객체
public void identify() {
~~
}
}
public class CatToy extends AnimalToy { // 팩터리 메서드가 생성할 객체
public void identify() {
~~
}
}
public abstract class Animal {
abstract AnimalToy getToy(); // (추상) 팩토리 메서드
}
public class Dog extends Animal {
@Override
AnimalToy getToy() { // 팩토리 메서드 오버라이딩
return new DogToy(); // 객체 반환
} // 설명과 동일하게 팩토리 메서드를 오버라이딩해서 객체를 반환하고 있다.
}
public class Cat extends Animal {
@Override
AnimalToy getToy() { // 팩토리 메서드 오버라이딩
return new CatToy(); // 객체 반환
} // 설명과 동일하게 팩토리 메서드를 오버라이딩해서 객체를 반환하고 있다.
}
public class Main {
public static void main(String[] args) {
// 팩토리 메서드를 보유한 객체(bolt, kitty)
Animal bolt = new Dog();
Animal kitty = new Cat();
// 팩토리 메서드가 객체를 반환
AnimalToy boltBall = bolt.getToy();
AnimalToy kittyTower = kitty.getToy();
}
}
오버라이드된 메서드가 객체를 반환하는 패턴이다.
전략 패턴 #
전략 패턴은 디자인 패턴의 꽃이라고 한다.
다음의 3 요소를 꼭 기억해야 한다.
- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트 (전략 객체의 사용자/소비자)
- 전략 객체를 생성해서 컨텍스트에 주입하는 클라이언트 (제 3자, 전략 객체의 공급자)
ㅡㅡㅡㅡㅡㅡ 1. 전략 객체 생성 ㅡㅡㅡㅡㅡㅡ> 전략들 (전략1, 전략2, ..., 전략n)
|
클라이언트
|
ㅡㅡㅡㅡㅡㅡ 2. 전략 객체 주입 ㅡㅡㅡㅡㅡㅡ> 컨텍스트
클라이언트는 다양한 전략들 중 한개를 선택해서 생성하고, 컨텍스트에 주입한다.
OCP, DIP 를 활용한 패턴이다.
public class Gun implements Strategy { // 전략 (총)
@Override
public void runStrategy() {
~~
}
}
public class Sword implements Strategy { // 전략 (검)
@Override
public void runStrategy() {
~~
}
}
public class Soldier { // 컨텍스트 (군인)
void runContext(Strategy strategy) {
// 이 부분이 템플릿 메서드 패턴과 유사하다.
System.out.println("전투 시작");
strategy.runStrategy();
System.out.println("전투 종료");
}
}
public class Client { // 클라이언트 (보급 장교, 제 3자)
public static void main(String[] args) {
Strategy strategy;
Soldier soldier = new Soldier();
strategy = new Gun(); // 전략 객체 생성
soldier.runContext(strategy); // 컨텍스트에 전략 주입
strategy = new Sword(); // 전략 객체 생성
soldier.runContext(strategy); // 컨텍스트에 전략 주입
}
}
" 템플릿 메서드 패턴과 유사하다는 느낌이 든다면, 정확히 본 것이다. "
같은 문제에 대해, 템플릿 메서드는 상속을 이용한 해결책을 제시하고 전략 패턴은 객체 주입을 통해 해결책을 제시한다.
클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴이다.
템플릿 콜백 패턴 (Template Callback Pattern) #
전략 패턴의 변형이다. 스프링의 3대 프로그래밍 모델 중 하나인 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴이다.
전략 패턴과 모든 것이 동일한데, 전략을 익명 내부 클래스로 정의해서 사용한다는 특징이 있따.
OCP 와 DIP 가 활용된 패턴이다.
// 위의 전략 패턴과 동일한 예시
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 soldier = new Soldier();
soldier.runContext(new Strategy() {
@Override
public void runStrategy() {
~~
}
});
soldier.runContext(new Strategy() {
@Override
public void runStrategy() {
~~
}
});
}
}
전략을 익명 내부 클래스로 구현한 전략 패턴이다.
스프링이 사랑한 다른 패턴들 #
위의 8가지 패턴 이 외에 스프링은 다양한 디자인 패턴을 활용한다.
스프링 MVC의 경우에는 프론트 컨트롤러 패턴(Front Controller Pattern), MVC 패턴 을 활용한다.