[개체지향] 디자인 패턴
디자인패턴 배웠다고 곧바로 쓸 생각 말것.
- 내 코드가 어떻게 도는지 이해할때 까지
- 최소 경력 2년까지
올바른 공부방법
- 문제를 겪음
- 해결방법을 고민
- 비슷한 문제를 겪음
- 같은 해결방법을 적용
여태까지 접한 디자인 패턴의 몇몇 예
- Java String의 내부 구현 : flyweight
- C# File, Stream class : decorator
- C#, Java StringBuilder : builder
- 상속 vs 컴포지션에서 본 그래픽 개체 코드 : composite
- foreach : iterator
-
다형성에서 본 마법사 코드 : state
- 생성패턴 Creational
- class Factory Method
- 개체 :
- 추상메서드
- 빌더
- 프로토타입
- 싱글턴
- 구조패턴 Structural
- class : Adapter
- 개체 :
- 어댑터
- 브리지
- 컴포지트
- 데코레이터
- 퍼사드
- 플라이웨이트
- 프록시
- 행위패턴
- class
- 인터프리터
- 템플릿 메서드
- 개체
- 책임연쇄
- 커맨드
- 반복자
- 중재자
- 메멘토
- 옵저버
- 상태
- 전략
- 방문자
- class
팩토리 메서드
- 사용할 클래스를 몰라도 개체 생성을 가능하게 해주는 패턴
- 예시 : 자동주문기계
- 음료주문할때 컵 크기를 고름
- 고객은 스몰 미디엄 라지를 선택할 뿐 실제 컵 용량을 모름
public enum Cup Size{
SMALL,
MEDIUM,
LARGE
}
public final class Cup{
private int sizeMl;
private Cup(int sizeMl){
this.sizeMl = sizeMl;
}
public static Cup createOrNull(CupSize size){
switch(size){
case ...
return new Cup(255);
default:
assert(false) : ~;
return null;
}
}
}
대충 이런식으로 사용
미국의 미디움이 일본의 라지보다 큼
방법1 createOrNull의 매개변수에 나라도 넣어준다.
- 괜찮으나 다형적인 OO 사고방식은 아님.
방법 2 createOrNull을 다형적으로 만든다
- static 메서드를 다형적으로 만들 수 없음.
- 따라서 자식 클래스를 만들어야 함.
Menu 추상클래스
+createCupOrNull(CupSize) : Cup
----
KoreanMenu
+createCupOrNull(CupSize) : Cup
----
AmericanMenu
+createCupOrNull(CupSize) : Cup
Menu <-- KoreanMenu
Menu <-- AmericanMenu
Cup
+sizeMl : int
~Cup(int) : 패키지접근제어자
+getSize() : int
public final class Cup{
private int sizeMl;
Cup(int sizeMl){
this.sizeMl = sizeMl;
}
public int getSize(){
return this.sizeMl;
}
}
public abstract class Menu{
...
public abstract Cup createCupOrNull(CupSize size);
}
// 인터페이스로 안만든이유는 나중에 더 추가된다면 Menu에 데이터가 들어갈 확률이 높아서
//가상 생성자가 된 꼴
public final class AmericanMenu extends Menu{
...
@Override
public static Cup createOrNull(CupSize size){
switch(size){
//size만 커짐
case ...
return new Cup(473);
default:
assert(false) : ~;
return null;
}
}
}
public final class KoreanMenu extends Menu{
...
@Override
public static Cup createOrNull(CupSize size){
switch(size){
//size만 커짐
case ...
return new Cup(355);
default:
assert(false) : ~;
return null;
}
}
}
한단계 더
- Cup도 추상적으로…
- 나라의 법규에 따라 사용하는 컵 종류가 다를 수 잇음
- 어떤나라는 1회용
- 어떤나라는 유리컵
public abstract class Cup{
private int sizeMl;
protected Cup(int sizeMl){
this.sizeMl = sizeMl;
}
public int getSize(){
return this.sizeMl;
}
}
public final class GlassCup extends Cup{
GlassCup(int sizeMl){
super(sizeMl);
}
}
public final class PaperCup extends Cup{
private Lid lid;
PaperCup(int sizeMl, Lid lid){
super(sizeMl);
this.lid = lid;
}
}
public final class AmericanMenu extends Menu{
...
@Override
public static Cup createOrNull(CupSize size){
Lid lid = new Lid(size);
switch(size){
//size만 커짐
case ...
return new PaperCup(473,lid);
default:
assert(false) : ~;
return null;
}
}
}
public final class KoreanMenu extends Menu{
...
@Override
public static Cup createOrNull(CupSize size){
switch(size){
//size만 커짐
case ...
return new GlassCup(355);
default:
assert(false) : ~;
return null;
}
}
}
- 구체적인 것부터 시작하면 차라리 이해가 쉽다
- 처음부터 추상적인 패턴을 사용하지 말 것
- 구체적인 것에서 시작
- 필요한 만큼까지만 추상화
장점
- 클라이언트는 본인에게 익숙한 인자를 통해 개체 생성 가능
- 생성자에서오류 상황 감지 시 null반환 가능
- 다형적으로 개체 생성 가능
- 이 패턴을 가상 생성자 패턴이라고도 함
빌더 패턴
- 개체의 생성과정을 그 개체의 클래스로부터 분리하는 방법
- 개체의 부분부분을 만들어 나가다 준비되면 그제서야 개체를 생성
- 다형성이 없는 빌더는 이미 StringBuilder에서 봤음
그러나
builder.append(heading);
builder.append(newLine);
builder.append(newLine);
- 작성자의 의도 제목을 넣고 줄을 바꾸고 싶음
- 위코드에서 그 의도가 명확히 보이지 않음
- 실제 글을 쓰듯이 읽히지 않음
fluent interface
- 요즘은 빌더 패턴을 구현시 종종 플루언트 인터페이스도 지원
- 최근 내용이라 GoF엔 없음
- 빌더패턴의 모던화
builder.append(heading)
.append(newLine)
.append(newLine);
builder.append(leadingParagraph)
.append(newLine);
...
- append 메서드가 자기 자신을 반환
public StringBuilder append(String str){
...
return this;
}
잘못 사용하는 빌더 패턴
직원 정보 클래스
- 직원정보를 생성할때 형이 같은 매개변수가 여럿 있을 경우 순서를 잘 못넣는 경우가있음
- 이를 막기위해 빌더 클래스로 해결하는 방식이 있음
Employee robert = new EmployeeBuilder(1)
.withAge(31)
.withStartingYear(2020)
.withName("Robert", "Lee")
.build();
- 메서드 이름이 명확하니 잘못된 값을 전달할 확률이 적음
- 그러나 잘못된 해결법
Employee robert = new EmployeeBuilder(1)
.withAge(31)
.withName("Robert", "Lee")
.build();
- 다음의 경우 0년부터 일하기 시작한 초고인물 직원이 등장
- 개체는 생성부터 유효한 상태여야 한다는 우리의 원칙이 어긋남
해결법
public final class Emplyee{
private String firstName;
private String lastName;
private int id;
private int yearStarted;
private int age;
public Employee(CreateEmplyeeParams params){
this.firstName = params.firstName
this.lastName = params.lastName
this.id = params.id
this.yearStarted = params.yearStarted
this.age = params.age
}
}
public final class CreateEmplyeeParams{
public String firstName;
public String lastName;
public int id;
public int yearStarted;
public int age;
}
- 생성자에 인자 순서를 잘못 넣는 경우르 ㄹ해결
- 여전히 실수로 age를 안 넣는 등의 문제는 있음
- 빌더보단 나음
C#에서 가능한법
- named parameter
- 각 매개변수를 받는 일반적인 생성자로서 사용
Employee employee = new Employee(firstName: "Rober", lastName:"Lee", id: 1, yearStarted: 2020, age: 31);
- 언어를 고치는게 올바른 방향
다형적인 빌더 패턴
- csv파일을 HTML, markdown 포맷으로 바꾸고싶을때
-
html 포맷
- 부모 자식 관계가 있는 여러 태그로 이루어져있음
- 이정도는 알고있어야함
- 예 <table>, <tr> <td> </td> </tr> ``` TableBuilder +addHeadingRow() +addRow() +addColumm(String) — MarkdownTableBuilder +addHeadingRow() +addRow() +addColumn(String) +toMarkDownText():String —- HtmlTableBuilder +addHeadingRow() +addRow() +addColumn(String) +toHtmlDocument():HtmlDocument
TableBuilder <– MarkdownTableBuilder TableBuilder <– HtmlTableBuilder
CsvReader
- csvText: Stirng +CsvReader(String) +writeTo(TableBuilder) ```
CsvReader reader = new CsvReader(csvText);
HtmlTableBuilder builder = new HtmlTableBuilder();
reader.wirteTo(builder);
HtmlDocument html = builder.toHtmlDocument();
CsvReader reader = new CsvReader(csvText);
MarkdownTableBuilder builder = new MarkdownTableBuilder();
reader.wirteTo(builder);
String mdText = builder.toMarkDownText();
wirteTo() 메서드의 의사코드
- 첫줄을 읽음
- 빌더의 addHeadingRow()메서드를 호출
- 1번에서 읽은 줄을 쉼표에 따라 토큰화
- 토큰 배열을 foreach문을 돌면서 빌더의 addColumn(token)을 호출
- 나머지 줄을 한 줄씩 읽으면서 다음의 과정을 반복
- 빌더의 addRow() 메서드를 호출
- 각 토큰마다 빌더의 addColumn(token)을 호출
시퀀스 다이어그램
- 개체들이 서로 통신하는 모습을 보여주는 uml 다이어그램
- 동작을 시간 흐름에 따라서 보여줌
레퍼패턴 wrapper
- 주로 업계에서는 wrapper패턴이라함
- Gof 책에선 adapter 패턴이란 이름을 사용
- 어떤 클래스의 메서드 시그내처가 맘에 안 들 때 다른 걸로 바꾸는 방법
- 그 클래스의 메서드 시그내처를 직접 변경하지 않음
- 다른 소스코드가 사용하는경우
- 소스코드가 없는경우
- 그 대신 새로운 클래스를 만들어 기존 클래스를 감쌈
- A 클래스의 getA()를 B클래스로 감싼뒤 getB가 getA를 내부적으로 호출
메서드 시그내처를 바꾸는 여러가지 이유
- 추후 외부 라이브러리를 바꿀 때 클라이언트 코드를 변경하지 않기 위해
- 사용중인 메서드가 코딩 표준에 맞지 않아서
- 기존 클래스에 없는 기능을 추가하기 위해
- 확장된 용도 : 내부 개체를 클라이언트에게 노출시키지 않기 위해
- DTO(data transfer object) : 만들기
래퍼패턴과 그래픽 api
- 윈도우에서 사용 가능한 3d그래픽 api는 대표적으로 둘
- 두 api 모두 컴퓨터에 설치된 그래픽 칻를 이용
- 따라서 두 api에서 지원하는 기능이 비슷
OpenGL
+clearScreen(float, float, float, float)
---
DirectX
+clear(int,int,int,int)
- 둘다 화면을 어떤 색상으로 지우는 메서드
- 다른점
- 메서드 이름
- r g b a 매개변수의 형과 유효한 범위
- float은 0.0,1.0
- int는 0,255
this.graphics.clearScreen(1.f, 0.f, 0.f, 0.f);//argb
this.graphics.clear(0,0,0,255); // rgba
- OpenGL을 사용하다 DrectX로 바꿀시에 해당 코드 모두 바꿔야함
- 수정할 코드가 많을수록 실수할 가능성이 높아진다.
- 이럴때 래퍼 클래스를 사용하면 한 곳만 바꾸면 됨.
public final class Graphics{
private OpenGL gl;
...
public void clear(float r, float g, float b, float a){
this.gl.clearScreen(a,r,g,b);
}
}
- 내부에 OpenGL개체를 들고 있음
- Graphics 메서드는 OpenGL의 메서드 호출
클라이언트는 래퍼 클래스만 사용
- Graphics 개체만 만듦(속에 OpenGL 개체가 들어있음)
- Graphics 개체의 메서드만 호출
public final class Graphics{
private DirectX dx;
...
public void clear(float r, float g, float b, float a){
this.dx.clear((int) (r*255),(int) (g*255),(int) (b*255),(int) (a*255) );
}
}
DTO
- 시스템 규모가 크면 종종 이런 문제들을 겪음
- DB정보(PersonEntity)
- id
- 이름
- passwordHash
- phoneNumber
- balance
- createdDateTime
- modifedDateTime
- 이중 이메일,이름 가입일만 화면에 띄워준다고 했을때 필요 이상의 데이터를 반환
- 민감한 데이터가 있을 수 있음
해결방법
- 따라서 클라이언트가 필요로 하는 정보만(PersonDto) 반환하는게 더 좋다.
- 데이터 전송에만 사용하는 개체를 데이터 전송 개체(DTO)라 함
- 남은일 : PersonEntity를 PersonDto로 변환하는 메서드만 만들면 됨
- PersonEntity의 메서드에 toDto를 만들어 사용
- 엄밀히 말하면 어댑터 패턴은 아님 하지만 궁극적인 목표는 비슷
프록시 패턴 proxy
proxy 란
- 웹 브라우저 설정 등을 뒤지다 보면 프록시 서버란걸 볼 수 있음
- 프록시 서버란 실제 웹사이트와 사용자 사이에 위치하는 중간 서버
- 인터넷상의 캐시 메모리 처럼 작동함
- 사용자는 프록시 서버를 통해 원하는 문서를 읽으려 함
- 프록시 서버에 이미 그 문서가 저장되어 있다면 그걸 반환
- 없다면 실제 웹서버에서 문서를 읽어와 프록시 서버에 저장
프록시 패턴
- 프록시 패턴이 이루려는 목적도 비슷
- 클래스 안에서 어떤 상태를 유지하는게 여의치 않은 경우가 있음
- 데이터가 너무 커서 미리 읽어 두면 메모리 부족
- 개체 생성 시 데이터를 로딩하면 시간이 꽤 걸림
- 개체는 만들었으나 그 속의 데이터를 사용하지 않을 수도 있음
- 이럴 경우 다음과 같은 방법을 통해 불필요한 데이터 로딩을 방지
- 개체 생성 시에는 데이터 로딩에 필요한 정보만 기억해 둠
- 클라이언트가 실제로 데이터를 요철할 때 메모리에 로딩함
예 이미지 데이터
- 용량이 큼
- 저장장치에서 읽어와야 함
- 병목점, bottle neck이 걸린다고 함
Image class
public final class Image{
private ImageData image;
public Image(String filePath){
this.image = ImageLoader.getInstance().load(filePath);
}
public void draw(Canvas canvas, float x, float y){
canvas.draw(this.image, x, y);
}
}
- 생성자에서 무조건 이미지를 읽어 옴
- 메모리를 많이 사용
- 이미지를 읽어오는 데 시간도 걸림(디스크 읽는 속도는 좀 느리니까)
- 모든 image에 대해 draw()가 호출될지도 의심스러움
Proxy 패턴 적용
public final class Image{
private String filePath;
private ImageData image;
public Image(String filePath){
this.filePath = filePath;
}
public void draw(Canvas canvas, float x, float y){
if(this.image == null){
this.image = ImageLoader.getInstance().load(filePath);
}
canvas.draw(this.image, x, y);
}
}
- 이렇게 늦게 읽어오는 방식을 지연 로딩(lazy loading) 이라 함
- 반대는 즉시 로딩(eager loading)이라고 함
즉시로딩 vs 지연로딩
| 즉시로딩 | 지연로딩 + 캐시X | 지연로딩 + 캐시(프록시 패턴) | |
|---|---|---|---|
| 최신 데이터 | X | O | 세모 |
| 메모리사용량 | 최고 | 최소 | 그중간 어딘가 |
| 실행속도 병목점 | 생성시 | 이미지 사용할때마다 | 알기힘듦 |
- 지연 로딩은 예전에는 정말 유용했지만
-
요즘 세상은 장담점에따라 골라 씀
- 요즘 컴퓨터는 메모리를 많이 장착
- 미리 다 로딩해놔도 큰 문제가 아닌 경우도 많음
- 한번에 그리는 이미지 수가 많지 않다면
- 필요할때마다 디스크에서 읽을 수 있음
- 하지만 인터넷에서 그 이미지들을 로딩한다면
- 예전에 디스크에서 읽을 때보다 시간이 더 오래 걸림
- 그동안 프로그램이 멈춰 있다면 사용자가 좋아할까?
프록시 패턴 + 캡슐화의 문제
- 클라이언트는 언제 이 클래스가 느려지는 지 알 수가없다.
- 세 구현 방법 모두 Image 클래스 안에 캡슐화되어 있음
극단적인 OO 진영의 주장
- 클라이언트가 그걸 알 필요가 없다.
- 하지만 즉시 로딩 방법을 택해서 처음 프로그램 시작 중에 이미지 읽느라 2분의 딜레이가 있다면?
- 클라이언트가 내부 동작방법을 분명히 알고 그에 적합한 UI를 보여주는 방법이 더 사랑받는 요즘(loding 중, 몇 개중 몇개를 로딩하고 있습니다 등등)
- 따라서 요즘 세상에는 클래스가 남몰래 프록시 패턴을 사용하는 것보다 클라이언트에게 조작 권하을 주는게 좋을 수 있음
프록시 패턴의 현대화 예
public final class Image{
private String filePath;
private ImageData image;
public Image(String filePath){
this.filePath = filePath;
}
public boolean isLoaded() {return return this.image != null; }
public void load(){
if(this.image == null){
this.image = ImageLoader.getInstance().load(filePath);
}
}
public void unload( this.image = null; )
public void draw(Canvas canvas, float x, float y){
canvas.draw(this.image, x, y);
}
}
isLoaded load unload
- 이미지의 로딩 상태를 클라이언트가 명확히 알 수 있게 해줌
- 클아이언트가 로딩과 언로딩 시점을 직접 제어할 수 있게 해줌
- 이를 이용해 게임이나 앱에서 봤던 로딩 스크린을 보여줄 수 있음
- 모든 이미지를 다 읽어올 때까지
- 상태머신(state machine을 사용
LoadingScreen class
public class LoadingScreen extends Screen{
ArrayList<Image> requiredImages;
...
public void update(){
if(this.requiredImages.size() == 0){
StateManager.getInstance().pop(this);
return;
}
Image image = this.requieedImages.get(0);
if(image.isLoaded()){
this.requiredImages.remove(0);
}else{
image.load();
}
dreawScreen();
}
}
지연로딩이 무조건 나쁘다는게 아님
- 필요한 곳에 잘 선택해 사용할 것
- 사용자 경험(UX)도 고려해야 함
- 내부 동작이 명백하게 보이게 클래스를 작성하면 좋은 경우
- 단순히 결과를 말하는게 아님
- 거기에 걸리는 시간 등의 부수적인 요소도 중요하다면 더더욱
책임 연쇄
위키피디아 예시
public abstract class Logger{
private EnumSet<LogLevel> logLevels;
private Logger next;
public Logger(LogLevel[] levels){
this.logLevels = EnumSet.copyof(Arrays.asList(levels));
}
public Logger setNext(Logger next){
this.next = next;
return this.next;
}
public final void message(String msg, LogLevel severity){
if(logLevels.contains(severity)){
log(msg);
}
if(this.next != null){
this.next.message(msg, severity);
}
}
protected abstract void log(String msg);
}
public class ConsoleLogger extends Logger{
public ConsoleLogger(LogLevel[] levels){
super(levels);
}
@Override
protected void log(String msg){
System.err.println("Sriting to console: " + msg);
}
}
public class EmailLogger extends Logger{
public EmailLogger(LogLevel[] levels){
super(levels);
}
@Override
protected void log(String msg){
System.err.println("Sending via email: " + msg);
}
}
public class FileLogger extends Logger{
public FileLogger(LogLevel[] levels){
super(levels);
}
@Override
protected void log(String msg){
System.err.println("Writing to log file: " + msg);
}
}
public enum LogLevel{
INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;
public static LogLovel[] all(){
return values();
}
}
/// 실제 사용
Logger logger = new ConsoleLogger(LogLevel.all());
logger.setNext (new EmailLover(new LogLevel[]{LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR})).setNext(new FileLogger(new LogLevel[]{LogLevel.WARNING, LogLevel.ERROR}));
// consoleLogger에서 처리 (consoleLogger는 모든 로그 레벨을 처리)
logger.messtge("Entering function ProcessOrder().", LogLevel.DEBUG);
logger.messtge("Order record retrieved.", LogLevel. INFO);
// consoleLogger와 emailLogger에서 처리
// (emailLogger는 Functional_Error과 Functional_Message 로그 레벨을 처리)
logger.message("Unable to Process Order ORD1 Dated D1 For Customer C1.",LogLevel.FUNCTIONAL_ERROR );
logger.message("Oreder Dispatched.", LogLevel.FUNCTIONAL_MESSAGE);
//consoleLogger와 fileLogger에서 처리 (fileLogger는 Warning과 Error로그 레벨을 처리)
logger.message("Customer Address details missing in Branch DataBase.", LogLevel.WARNING);
logger.message("Customer Address details missing in Organization DataBase.", LogLevel.ERROR);
- logger를 한번만 호출
훨씬 직관적인 방법 LogManager class
public final class LogManager{
private static LogManager instance;
private ArrayList<Logger> loggers = new ArrayList<Logger>();
public static LogManager getInstance(){
if(instance == null){
instance = new LogManager();
}
return instance;
}
public void addHandler(Logger logger){
this.loggers.add(logger);
}
public void message(String msg, LogLevel severity){
for(Logger logger : this.loggers){
logger.message(msg, severity);
}
}
}
public abstract class Logger{
private EnumSet<LogLevel> logLevels;
public Logger(LogLevel[] levels){
this.logLevels = EnumSet.copyof(Arrays.asList(levels));
}
public final void message(String msg, LogLevel severity){
if(logLevels.contains(severity)){
log(msg);
}
}
protected abstract void log(String msg);
}
// ConsolerLogger
// EmailLogger
// FileLogger class는 변경사항 없음
/// 실제 사용
LogManager logManager = LogManager.getInstance();
logManager.addHandler(new ConsoleLogger(LogLevel.all()));
logManager.addHandler(new EmailLover(new LogLevel[]{LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR}));
logManager.addHandler(new FileLogger(new LogLevel[]{LogLevel.WARNING, LogLevel.ERROR}));
logManager.message("Entering function ProcessOrder().", LogLevel.DEBUG);
logManager.message("Order record retrieved.", LogLevel. INFO);
logManager.message("Unable to Process Order ORD1 Dated D1 For Customer C1.",LogLevel.FUNCTIONAL_ERROR );
logManager.message("Oreder Dispatched.", LogLevel.FUNCTIONAL_MESSAGE);
logManager.message("Customer Address details missing in Branch DataBase.", LogLevel.WARNING);
logManager.message("Customer Address details missing in Organization DataBase.", LogLevel.ERROR);
어떤 극단적인 OO추종자들은 이리 말할 수도 있습니다.
- for 문은 구조적인 프로그래밍이니까 올바르지 않다.
위키피디아에 있는 예 자체가 잘못된 예
올바른 책임 연쇄 패턴
- 어떤 메시지를 처리할 수 있는 여러 개체가 있음
- 이 개체들은 차례대로 메시지를 처리할 수 있는 기회를 받음
- 만약 그중 한 개체가 메시지를 처리하면 그거에 대한 책임을 짐
- 즉, 다음 개체는 메시지를 처리할 기회를 받지 못함
- 이래서 책임 연쇄란 이름이 붙은것.
올바른 책임연쇄 패턴
// 원래 코드
public final void message(String msg, LogLevel severity){
if(logLevels.contains(severity)){
log(msg);
}
if(this.next != null){
this.next.message(msg, severity);
}
}
// 올바르게 바꾼 코드
public final void message(String msg, LogLevel severity){
if(logLevels.contains(severity)){
log(msg);
} else if(this.next != null){
this.next.message(msg, severity);
}
}
옵저버 observer
- A개체를 감시하는 개체
- 감시하는 개체는 한개가 아니고 여러개 일수있음
- 감시당하는 개체는 하나
- A가 변하면 나도 바꾸고 싶다 이럴때 사용
옵저버 보다 더 자주 사용하는 패턴
- pub sub 패턴(발행, 구독)
- publisher subscriber
- 옵저버와 비슷하지만 엄밀히 말하면 다른 패턴
- 이루려는 목적은 비슷함
LogManager가 방금전에 봤던 pub sub 패턴
- Logger등(구독자)을 LogManager에 추가
- 프로그램(발행자)에서 LogManager에 로그메시지를 보냄
- 그 로그 메시지를 처리하게 등록된 구독자들에게 전부 메시지가 감
- 여기서 LogManager를 빼면 옵저버 패턴
옵저버 패턴 예 : 크라우드 펀딩
- 돈이 들어올 때마다 두 개체를 업데이트 하고 싶음
- 장부를 업데이트 (상태는 금액만 필요)
- 모파일 폰에서 노티를 받음(상태는 이름과 금액이 필요)
- 이벤트 주도 아키텍처라고도 함(event - driven)
public interface IFundingCallback{
void onMoneyRaised(String backer, int amount);
}
public final class BookkeepingApp implements IFundingCallback{
//멤버 변수와 메서드는 모두 생략
@Override
public void onMoneyRaised(String backer, int amount){
// 장부에 새 내역 추가
// amount만 사용
}
}
public final class MobileApp implements IFundingCallback{
//멤버 변수와 메서드는 모두 생략
@Override
public void onMoneyRaised(String backer, int amount){
// 모바일 앱에 알림을 보여준다
// backer와 amount 모두 사용
}
}
public final class CrowdFundingAccount{
private int balance;
private ArrayList<IFundingCallback> subscribers;
public CrowdFundingAccount(){
this.subscribers = new ArrayList<IFundingCallback>();
}
public void subscribe(IFundingCallback sub){
subscribers.add(sub);
}
public void support(String backer, int amount){
this.balance += amount;
for(IFundingCallback sub : subscribers){
sub.onMoneyRaised(backer, amount);
}
}
}
- C의 함수포인터를 통한 콜백함수와 유사
-
옵저버 패턴은 결국 콜백 함수의 목록이다
BookkeepingApp book = new ...
funding.subscribe(bood);
....
book = null;
- 근데 이런 방식이 매니지드 언어에서 메모리 누수를 만드는 주범
- book 의 원래 개체는 사라지지 않음
- ArrayList subscribers 에서 참조를 하고있음 따라서 직접 지워줘야 함
CrowdFundingAccount class
...
public void unsubscribe(IFundingCallback sub){
subscribers.remove(sub);
}
//사용
BookkeepingApp book = new ...
funding.subscribe(bood);
....
funding.unsubscribe(book);
book = null;
정리
- 팩토리 메서드
- 빌더
- 랩퍼
- 프록시
- 책임연쇄
- 옵저버