디자인패턴 배웠다고 곧바로 쓸 생각 말것.

  • 내 코드가 어떻게 도는지 이해할때 까지
  • 최소 경력 2년까지

올바른 공부방법

  1. 문제를 겪음
  2. 해결방법을 고민
  3. 비슷한 문제를 겪음
  4. 같은 해결방법을 적용

여태까지 접한 디자인 패턴의 몇몇 예

  • 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
        • 인터프리터
        • 템플릿 메서드
      • 개체
        • 책임연쇄
        • 커맨드
        • 반복자
        • 중재자
        • 메멘토
        • 옵저버
        • 상태
        • 전략
        • 방문자

팩토리 메서드

  • 사용할 클래스를 몰라도 개체 생성을 가능하게 해주는 패턴
  • 예시 : 자동주문기계
    • 음료주문할때 컵 크기를 고름
    • 고객은 스몰 미디엄 라지를 선택할 뿐 실제 컵 용량을 모름
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() 메서드의 의사코드

  1. 첫줄을 읽음
  2. 빌더의 addHeadingRow()메서드를 호출
  3. 1번에서 읽은 줄을 쉼표에 따라 토큰화
  4. 토큰 배열을 foreach문을 돌면서 빌더의 addColumn(token)을 호출
  5. 나머지 줄을 한 줄씩 읽으면서 다음의 과정을 반복
    1. 빌더의 addRow() 메서드를 호출
    2. 각 토큰마다 빌더의 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
    • 이름
    • email
    • 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;

정리

  • 팩토리 메서드
  • 빌더
  • 랩퍼
  • 프록시
  • 책임연쇄
  • 옵저버