[자바 객체지향 원리] 6장 - 스프링이 사랑한 디자인 패턴
캡! 상추다 객체 지향 특성 => 도구
SOLID 설계 원칙 => 도구를 올바르게 사용하는 방법
디자인 패턴 => 요리법, 레시피
이전의 많은 개발자들이 고민하고 정제한 사실 상의 표준 설계 패턴
실제 개발 현장에서 비즈니스 요구 사항을 프로그래밍으로 정리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 best practice
= 디자인 패턴
스프링을 이해하는데 도움이 될 디자인 패턴!
스프링 프레임 워크 = "자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크" = "OOP 프레임워크"
나는 지금 객체 지향에서 시작해 스프링으로 향하는 Bottom-Up 학습 중!
디자인 패턴은 객체 지향의 특성 중 상속(extends), 인퍼테이스(interface/implements), 합성(객체를 속성으로 사용)을 이용
이 세 가지 방식 외에 다른 방식은 없다. 따라서 여러 디자인 패턴이 비슷해 보일 수 있음
어댑터 패턴(Adapter Pattern)
어댑터 = 변환기(converter)
서로 다른 두 인터페이스 사이에 통신이 가능하게 하는 것
ex) 충전기(휴대폰, 전원 콘센트 사이에서 둘을 연결해줌)
ex) ODBC/JBDC (어댑터 패턴을 이용해 다양한 데이터베이스 시스템을 단일한 인터페이스로 조작할 수 있게 해줌)
ex) JRE (플랫폼과 상관 없이 자바 프로그램 실행가능하게 해줌)
어댑터는 개방 폐쇄 원칙(OCP)을 활용한 설계 패턴
시퀀스(sequence, 순차, 순서) 다이아그램
sa1 참조 변수와 sb1 참조변수를 통해 호출하는 각 메서드가 비슷한 일을 하지만, (불필요하게) 메서드명이 다름!
클라이언트(ClientWithAdapter)가 변환기를 통해 runService()라는 동일한 메서드 명으로 두 객체의 메서드를 호출
(변환기들이 인터페이스를 구현하게 해서 더 개선할 수 있음)
어댑터 패턴은 합성, 즉 객체를 속성으로 만들어서 참조하는 디자인 패턴으로, 한 문장을 정리하면,
"호출 당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴"
프록시 패턴(Proxy Pattern)
프록시 = 대리자, 대변인
다른 누군가를 대신해 그 역할을 수행하는 존재
대리자 사용하지 않고 직접 호출하는 구조
ClientWithNoProxy 가 Service 객체의 runSomething() 메서드를 직접 호출하고 있음
프록시 패턴이 적용된 경우
프록시 패턴의 경우, 실제 서비스 객체가 가진 메서드와 같은 메서드 사용하기 위해 인터페이스 사용
-> 서비스 객체가 들어갈 자리에 대리자 객체를 대신 투입
-> 클라이언트 쪽에서는 실제 서비스 객체를 통해 메서드를 호출하고 반환값을 받는지, 대리자 객체를 통해 받는지 전혀 모르게 처리
public interface IService {
String runSomething();
}
public class Service implements IService {
public String runSomething() {
return "서비스 짱!!!";
}
}
public class Proxy implements IService {
IService service1;
public String runSomething() {
System.out.println("호출에 대한 흐름 제어가 주 목적, 반환 결과를 그대로 전달");
service1 = new Service();
return service1.runSomething();
}
}
public class ClientWithProxy {
public static void main(String[] args) {
// 프록시를 이용한 호출
IService proxy = new Proxy();
System.out.println(proxy.runSomething());
}
}
프록시 패턴의 중요 포인트
- 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이 때 인터페이스를 사용한다.
- 대리자는 실제 서비스에 대한 참조 변수를 갖는다(합성)
- 대리자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 값을 클라이언트에게 돌려준다
- 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.
대변인은 입장을 대변할 뿐, 자신의 입장을 가감하지 않음
프록시 패턴은 실제 서비스 메서드의 반환값에 가감하지 않음!!
-> 제어의 흐름을 변경하거나, 다른 로직을 수행하기 위해 사용
"제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴"
Service, Proxy, IService 와의 관계는
-> 마티즈, 쏘나타, 자동차의 관계(운전) = OCP
-> 인터페이스를 중간에 두고 타이어를 서로 교체해 주어도 영향받지 않았던 자동차 = DIP
데코레이터 패턴(Decorator Pattern)
데코레이터 = 도장/도배업자 = 장식자
원본에 장식을 더하는 패턴
- 프록시 패턴
클라이언트가 최종적으로 돌려 받는 반환값을 조작하지 않고 그대로 전달(특별한 경우가 아니면)
제어의 흐름을 변경하거나 별도의 로직 처리를 목적으로 함 - 데코레이터 패턴
클라이언트가 받는 반환값에 장식을 덧입힘
프록시 패턴과 데코레이터 패턴은 클래스 다이어그램과 시퀀스 다이어그램이 같음! 코드만 살펴보자
package abstraction01;
public interface IService {
String runSomething();
}
public class Service implements IService {
public String runSomething() {
return "서비스 짱!!!";
}
}
public class Decorator implements IService {
IService service;
public String runSomething() {
System.out.println("호출에 대한 장식 주목적, 클라이언트에게 반환 결과에 장식을 더하여 전달");
service = new Service();
return "정말" + service.runSomething();
}
}
public class ClientWithProxy {
public static void main(String[] args) {
IService decorator = new Decorator();
System.out.println(decorator.runSomething());
}
}
- 장식자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이 때 인터페이스를 사용한다.
- 장식자는 실제 서비스에 대한 참조 변수를 갖는다(합성).
- 장식자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 반환값에 장식을 더해 클라이언트에게 돌려준다.
- 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.
"메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 주는 패턴"
프록시 패턴과 동일하게 OCP, DIP이 적용된 패턴
싱글턴 패턴(Singleton Pattern)
인스턴스를 하나만 만들어 사용(계속해서 재사용)하기 위한 패턴
ex) 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등
인스턴스를 여러개 만들게 되면 불필요한 자원 사용, 프로그램의 예상치 못한 결과
의미상 두 개의 객체가 존재할 수 없음
-> 객체 생성을 위한 new에 제약을 걸어야 함 + 만들어진 단일 객체를 반환할 수 있는 메서드 필요
- new를 실행할 수 없도록 생성자에 private 접근 제어자를 지정
- 유일한 단일 객체를 반환할 수 있는 정적(static) 메서드가 필요
- 유일한 단일 객체를 참조할 정적 참조 변수 필요
package singletonPattern;
public class Singletom {
static Singleton singletonObject; // 정적 참조 변수
private Singleton() { }; // private 생성자
// 객체 반환 정적 메서드
public static Singleton getInstance() {
if (singletonObject == null) {
singletonObject = new Singletone();
}
return singletonObject;
}
}
정적 참조 변수에 객체가 할당돼있지 않은 경우에만 new를 통해 객체를 만들고 정적 참조 변수에 할당
그리고 정적 참조 변수에 할당돼 있는 유일한 객체의 참조 반환
package singletonPattern;
public class Client {
public static void main(String[] args) {
// private 생성자임으로 new 할 수 없다.
// Singleton s = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
s1 = null;
s2 = null;
s3 = null;
}
}
private 생성자 -> Singleton 외부에서 new를 이용해 객체 생성할 수 없음
The constructor Singleton() is not visible
4개의 참조 변수(singletonObject, s1, s2, s3)가 하나의 단일 객체를 참조하는 것을 볼 수 있음
1. 단일 객체인 경우 결국 공유 객체로 사용되지 때문에 속성을 갖지 않게 하는것이 정석
단일 객체가 속성을 갖게 되면, 하나의 참조 변수가 변경한 단일 객체의 속성이 다른 참조 변수에 영향을 미치기 때문
= 전역/공유 변수를 가능한 한 사용하지 말라
2. 읽기 전용 속성을 갖는 것은 문제 X
3. 단일 객체가 다른 단일 객체에 대한 참조를 속성으로 가진 것 또한 문제 X
-> 스프링의 싱글턴 빈이 가져야할 제약 조건
객체 참조 변수 자체를 System.out.println을 통해 출력하면, 참조하고 있는 객체의 toString() 메서드가 호출됨
toString() 메서드를 별도로 오버라이딩하지 않았다면 객체의 고유 값인 hashcode를 반환
싱글턴 패턴의 특징
- private 생성자를 갖는다
- 단일 객체 참조 변수를 정적 속성으로 갖는다
- 단일 객체 참조 변수가 참조하는 단일 객체를 반환하는 getInstance() 정적 메소드를 갖는다.
- 단일 객체는 쓰기 가능한 속성을 갖지 않는 것이 정석이다.
"클래스의 인스턴스, 즉 객체를 하나만 만들어 사용하는 패턴"
템플릿 메서드 패턴(Template Method Pattern)
템플릿 = 견본
상위 클래스에 공통 로직을 수행하는 템플릿 메서드와,
하위 클래스에 오버라이딩을 강제하는 추상 메서드 or
선택적으로 오버라이딩 할 수 있는 훅(Hook) 메서드를 두는 패턴
package templateMethodPattern;
public abstract class Animal {
// 템플릿 메서드
public void playWithOwner() {
System.out.println("귀염둥이 이리온...");
play();
runSomething();
System.out.println("잘했어");
}
// 추상 메서드
abstract void play();
// Hook(갈고리) 메서드
void runSomething() {
System.out.println("꼬리 살랑 살랑~");
}
}
객체 지향의 4대 특성 가운데 상속을 통해 동일한 부분(중복)은 상위 클래스로, 달라지는 부분만 하위 클래스로 분할
상위 클래스인 Animal에는 템플릿(견본)을 제공하는 playWithOwner() 메서드
하위 클래스에게 구현을 강제하는 play() 추상 메서드,
하위 클래스가 선택적으로 오버라이딩할 수 있는 runSomething() 메서드
package templateMethodPattern;
public class Driver {
public static void main(String[] args) {
Animal bolt = new Dog();
Animal kitty = new Cat();
bolt.playWithOwner();
System.out.println();
System.out.println();
kitty.playWithOwner();
}
}
"상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴"
의존 역전 원칙(DIP) 활용
팩토리 메서드 패턴(Factory Method Pattern)
팩토리 = 공장 = 객체 생성
객체를 생성 반환하는 메서드
-> 하위 클래스에서 팩토리 메서드를 오버라이딩해서 객체를 반환하게 함
package factoryMethodPattern;
public abstract class Animal {
// 추상 팩토리 메서드
abstract AnimalToy getToy();
}
// 팩토리 메서드가 생성할 객체의 상위 클래스
public abstract class AnimalToy {
abstract void identify();
}
public class Dog extends Animal {
// 추상 팩토리 메서드 오버라이딩
@Override
AnimalToy getToy() {/
return new DogToy();
}
}
// 팩토리 메서드가 생성할 객체
public class DogToy extends AnimalToy {
public void identify() {
System.out.println("나는 테니스공! 강아지의 친구!");
}
}
public class Cat extends Animal {
// 추상 팩토리 메서드 오버라이딩
@Override
AnimalToy getToy() {
return new CatToy();
}
}
// 팩토리 메서드가 생성할 객체
public class CatToy extends AnimalToy {
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();
// 팩터리 메서드가 반환한 객체들을 사용
boltBall.identify();
kittyTower.identify();
}
}
"오버라이드된 메서드가 객체를 반환하는 패턴"
의존 역전 원칙(DIP) 활용
전략 패턴(Strategy Pattern)
디자인 패턴의 꽃
- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자/소비자)
- 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자, 전략 객체의 공급자)
클라이언트는 다양한 전략 중 하나를 선택해 생성한 후, 컨텍스트에 주입한다.
ex) 군인, 군인이 사용할 무기, 보급 장교가 무기를 군인에게 지급해주면 군인은 주어진 무기에 따라 전투 수행
무기 = 전략
군인 = 컨텍스트
보급 장교 = 제 3자, 클라이언트
- 다양한 전략을 공통된 방식으로 사용하기 위한 인터페이스
package strategyPattern;
public interface Strategy {
public abstract void runStrategy();
}
- 다양한 전략, 즉 무기 구현(총, 검, 활)
package strategyPattern;
public class StrategyGun implements Strategy {
@Override
public void runStrategy() {
System.oout.println("탕, 타당, 타다당");
}
}
public class StrategySword implements Strategy {
@Override
public void runStrategy() {
System.oout.println("챙.. 채쟁챙 챙챙");
}
}
public class StrategBow implements Strategy {
@Override
public void runStrategy() {
System.oout.println("슝...쐐액...쉑, 최종병기");
}
}
- 무기(전략)를 사용할 군인(컨텍스트) 구현
package strategyPattern;
public class Soldier {
void runContext(Strategy strategy) {
System.out.println("전투 시작");
strategy.runStrategy();
System.out.println("전투 종료");
}
}
- 무기(전략)를 조달(생성)해서 군인(컨텍스트)에게 지급(주입)해 줄 보급 장교(클라이언트, 제 3자) 구현
package strategyPattern;
public class Client {
public static void main(String[] args) {
Strategy strategy = null;
Soldier rambo = new Soldier();
// 총을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategyGun();
rambo.runContext(strategy);
System.out.println();
// 검을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategySword();
rambo.runContext(strategy);
System.out.println();
// 활을 람보에게 전달해서 전투를 수행하게 한다.
strategy = new StrategyBow();
rambo.runContext(strategy);
}
}
// 결과
// 전투 시작
// 탕, 타당, 타다당
// 전투 종료
// 전투 시작
// 챙.. 채쟁챙 챙챙
// 전투 종료
// 전투 시작
// 슝... 쐐액.. 쉑, 최종병기
// 전투 종료
전략을 다양하게 변경하면서 컨텍스트 실행 가능!!
다양한 곳에서 다양한 문제 상황의 해결책으로 사용됨
템플릿(견본) 메서드 패턴과 유사!
같은 문제의 해결책으로, 상속을 이용하는 템플릿 메서드 패턴 vs 객체 주입을 통한 전략 패턴
단일 상속만이 가능한 자바 언어에서는,
상속 제한이라는 제한이 있는 템플릿 메서드 패턴 << 전략 패턴
"클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴"
-> OCP, DIP
템플릿 콜백 패턴(Template Callback Pattern - 견본/회신 패턴)
전략 패턴의 변형
스프링의 3대 프로그래밍 모델 중 하나인 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴
(7장에서 더 자세히 학습)
전략 패턴과 모든 것이 동일
+ 전략을 익명 내부 클래스로 정의해서 사용
위 예제를 템플릿 콜백 패턴으로 바꿔보자
package templateCallbackPattern;
public class Client {
public static void main(String[] args) {
Soldier rambo = new Soldier();
/*
* Strategy strategy = new StrategyGun();
* rambo.runContext(strategy);
*/
/*
* Strategy strategy = new IStrategy() {
*
* @Override public void doStrategy() {
* System.out.println("총! 총초종총 총! 총!"); } };
*
* rambo.runContext(strategy);
*/
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("총! 총초종총 총! 총!");
}
});
System.out.println();
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("칼! 카가갈 칼! 칼!");
}
});
System.out.println();
rambo.runContext(new Strategy() {
@Override
public void runStrategy() {
System.out.println("도끼! 독독..도도독 독끼!");
}
});
}
}
많은 중복 코드 -> 리팩토링!
package templateCallbackPatternRefactoring;
public class Soldier {
void runContext(String weaponSound) {
System.out.println("전투 시작");
executeWeapon(weaponSound).runStrategy();
System.out.println("전투 종료");
}
private Strategy executeWeapon(final String weaponSound) {
return new Strategy() {
@Override
public void runStrategy() {
System.out.println(weaponSound);
}
};
}
}
전략을 생성하는 코드가 컨텍스트, 즉 군인 내부로 들어옴!
package templateCallbackPatternRefactoring;
public class Client {
public static void main(String[] args) {
Soldier rambo = new Soldier();
rambo.runContext("총! 총초종총 총! 총!");
System.out.println();
rambo.runContext("칼! 카가갈 칼! 칼!");
System.out.println();
rambo.runContext("도끼! 독독..도도독 독끼!");
}
}
스프링은 이런 형식으로 리팩토링된 템플릿 콜백 패턴을 DI에 적극 활용 중!!
스프링을 이해하고 활용하기 위해서는 템플릿 콜백 패턴, 리팩토링된 템플릿 콜백 패턴을 잘 기억해두자!
"전략을 익명 내부 클래스로 구현한 전략 패턴"
-> OCP, DIP
스프링이 사랑한 다른 패턴들
스프링 MVC = >
프론트 컨트롤러 패턴(Front Controller Pattern; 최전선 제어자 패턴)
MVC(Model - View - Controller) 활용
추후 부록을 통해 살펴볼 예정