Posts 학원 #56일차: PMS 프로젝트: Observer 패턴의 활용
Post
Cancel

학원 #56일차: PMS 프로젝트: Observer 패턴의 활용

복습

ApplicationContextListener는 다른 패키지에서도 공통으로 사용해야 하기 때문에 (특정 프로그램에 종속되지 않기 때문에) 회사 도메인 바로 아래에 둔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.eomcs.pms.listener;

import com.eomcs.context.ApplicationContextListener;

public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized() {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
  }
  
  @Override
  public void contextDestroyed() {
    System.out.println("프로젝트 관리 시스템(PMS)을 종료합니다!");
  }
}
  • 기존에 만들어진 클래스에서 이 클래스의 메서드들을 호출할 것이다.
  • 디자인 패턴을 적용할 때는 복잡하더라도 차후 기능을 변경하고 삭제하는 것이 편해진다.

만약 프로젝트에 옵저버 패턴을 적용하지 않는다면, 다음과 같이 직접 service 메서드를 수정할 것이다.

1
2
3
4
5
public void service () {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
    //...
    System.out.println("프로젝트 관리 시스템(PMS)을 종료합니다!");
}

옵저버를 만들었으면 메인 메서드에서 옵저버를 등록해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class App {

  // 옵저버를 보관할 컬렉션 객체
  List<ApplicationContextListener> listeners = new ArrayList<>();

  // 옵저버를 등록하는 메서드
  public void addApplicationContextListener(ApplicationContextListener listener) {
    listeners.add(listener);
  }

  // 옵저버를 제거하는 메서드
  public void removeApplicationContextListener(ApplicationContextListener listener) {
    listeners.remove(listener);
  }

  // service() 실행 전에 옵저버에게 통지한다.
  private void notifyApplicationContextListenerOnServiceStarted() {
    for (ApplicationContextListener listener : listeners) {
      // 곧 서비스를 시작할테니 준비하라고,
      // 서비스 시작에 관심있는 각 옵저버에게 통지한다.
      listener.contextInitialized();
    }
  }

  // service() 실행 후에 옵저버에게 통지한다.
  private void notifyApplicationContextListenerOnServiceStopped() {
    for (ApplicationContextListener listener : listeners) {
      // 서비스가 종료되었으니 마무리 작업하라고, 
      // 마무리 작업에 관심있는 각 옵저버에게 통지한다.
      listener.contextDestroyed();
    }
  }


  public static void main(String[] args) throws Exception {
    App app = new App();

    // **옵저버 등록**
    app.addApplicationContextListener(new AppInitListener());

    app.service();
  }

  public void service() throws Exception {

    notifyApplicationContextListenerOnServiceStarted();
    //..생략
      
    notifyApplicationContextListenerOnServiceStopped();
      
  }
}

옵저버 패턴은 새 기능을 추가할 때, 특히 특정 상태에 특정 작업을 하고 싶을때 사용한다. 그러나 실제로 우리가 옵저버 패턴을 적용할 일은 없다. 이미 리스트에 들어 있는 객체의 메서드를 호출하도록 프로그램 되어 있다. 따라서 실제로 우리는 규칙에 따라 클래스만 작성하고 메인 메서드에 등록하면 끝난다.

서블릿 프로그램을 만들 때 구조는 옵저버 패턴과 매우 유사(동일)하다. 단 웹 서버를 만들 때는 직접 등록하지 않고 클래스 선언 위에 @Webservlet(주소)을 쓴다. 그러면 웹 서버에 등록한다. 그럼 브라우저에서 주소를 입력하면 프로그램이 자동으로 service() 메서드를 호출한다.

33-b: 데이터 로딩과 저장을 Observer 객체로 옮기기

  • Observer 디자인 패턴 구조에서 기능을 추가하는 방법을 연습한다.
  • publisher와 subscriber 간의 객체를 공유함으로써 데이터 공유하는 방법을 연습한다.
  • App클래스가 하던 파일 입출력 기능을 옵저버로 옮긴다.

image

  • 옵저버의 메서드를 호출할 때 파라미터로 옵저버와 발행자 간의 공유할 객체를 넘긴다.

객체 간에 데이터를 주고받기 위해서는 어떻게 해야 할까? 이때까지 배운 방법으로는 호출자가 호출하는 메서드의 파라미터로 데이터를 넘기고, 호출당한 타 클래스 메서드의 작업 결과는 리턴값으로 받아 사용하게 하면 될 것 같다.

그러나 작업한 결과가 많을 때는 어떻게 해야 할까? 리턴값은 하나밖에 없지 않은가! 리턴값을 Map 객체로 받을 수도 있겠지만, publisher가 공급할 작업재료도 많다면? 작업자가 필요한 작업하는 데 필요한 데이터와 작업자가 작업한 결과를 어떻게 주고받을까? Map객체를 공유함으로서 이를 이룰 수 있다. 호출하는 쪽이 메서드의 파라미터로 Map객체를 넘기고, callee가 작업한 결과도 Map 객체에 저장하도록 한다. 즉 Map 객체를 함게 공유하는 것이다.

이게 가능한 이유는 자바에서 객체는 call by reference이기 때문이다. 파라미터로 받은 값이 원시형이면 그 자체가 복사되어 메서드 안에서 변경하더라도 호출한 쪽에 반영되지 않는다. 그러나 파라미터로 받은 값이 객체면 객체의 주소가 복사된다. 따라서 메서드 안에서 객체를 수정하면 결국 호출한 쪽에서도 수정된 객체 주소를 가지고 접근하게 되는 것이다.

명령을 내릴 때 말로 표현한다. 프로그램에서는 메서드 호출을 하는 것이 명령을 내리는 것이. 그렇기 때문에 메서드 이름을 항상 동사(명령어)로 하는 것이다.

1단계: 발행자와 옵저버 간의 데이터를 공유할 수 있도록 규칙에 파라미터 추가

ApplicationContextListener 인터페이스의contextInitialized(). contextDestroyed() 메서드가 Map 파라미터를 받도록 선언한다.

javax.servlet: ServletContextListener 인터페이스의 메서드명도 동일하다 (contextDestroyed), contextDestroyed): 상태변경이 발생했을 때 호출되는 규칙을 정의

1
2
3
4
5
6
7
public interface ApplicationContextListener {
  // 발행자(애플리케이션)가 애플리케이션 시작을 알리기 위해 호출하는 메서드
  void contextInitialized(Map<String, Object> context);

  // 발행자(애플리케이션)가 애플리케이션 종료를 알리기 위해 호출하는 메서드
  void contextDestroyed(Map<String, Object> context);
}

따라서 이를 구현한 AppInitListener의 메서드도 인터페이스의 메서드와 선언부를 같게 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized(Map<String, Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
  }

  @Override
  public void contextDestroyed(Map<String, Object> context) {
    System.out.println("프로젝트 관리 시스템(PMS)을 종료합니다!");
  }
}

App클래스(발행자)에서 발행자와 옵저버 간에 데이터를 공유하기 위해 Map 필드를 생성한다. 그리고 옵저버를 호출할 때 맵 객체를 넘겨주도록 한다. 호출한 쪽에서 객체를 만들어 넘긴다는 것이 중요하다! 맵 파라미터 추가한다. 주방장이 주방보조에게 음식을 만들 재료와 음식을 담을 그릇을 넘겨준다고 생각하면 된다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class App {
  Map<String, Object> context = new HashMap<>();
  //...
    // service() 실행 전에 옵저버에게 통지한다.
  private void notifyApplicationContextListenerOnServiceStarted() {
    for (ApplicationContextListener listener : listeners) {
      // 서비스가 종료되었으니 마무리 작업하라고, 
      // 마무리 작업에 관심있는 각 옵저버에게 통지한다.
      // => 옵저버에게 맵 객체를 넘겨준다.
      // => 옵저버는 작업 결과를 파라미터로 넘겨준 Map객체에 담아줄 것이다.
      // 빈 그릇 줄 테니까, 작업 결과 담아줘!!
      listener.contextInitialized(context);
    }
  }

  // service() 실행 후에 옵저버에게 통지한다.
  private void notifyApplicationContextListenerOnServiceStopped() {
    for (ApplicationContextListener listener : listeners) {
      // 서비스가 종료되었으니 마무리 작업하라고, 
      // 마무리 작업에 관심있는 각 옵저버에게 통지한다.
      // => 옵저버에게 맵 객체를 넘겨준다.
      // => 옵저버는 작업 결과를 파라미터로 넘겨준 Map객체에 담아줄 것이다.
      listener.contextDestroyed(context);
    }
  }
//..
}

HashMap과 HashTable의 차이점

  • HashMap은 key나 value에 null을 허용한다. hashTable은 key나 value에 null을 허용하지 않는다.

  • HashMap은 멀티 스레드 환경에서 사용되는 것을 보장하지 않는다. 즉 여러 스레드가 동시에 해시맵에 접근하는 것을 막지 않는다. 멀티 스레딩 환경에서 HashMap은 스레드세이프하지 않다. 동시에 여러 스레드가 들어올 때 적절하게 lock을 걸어놓는 조치를 취하지 않기 때문에 여러 스레드가 동시에 들어올 때 처리하지 못한다. (synchronized 하지 않는다) HashTable은 여러 스레드가 put을 하면 다른 스레드는 put을 못하고 기다리도록 lock을 건다. 따라서 멀티 스레드 환경에서는 HashMap보다 HashTable을 쓰는 게 좋다.

2단계: 파일에서 데이터를 로딩하고 저장하는 기능의 옵저버를 추가한다.

image

  • com.eomcs.pms.listener.DataHandlerListener 클래스 생성
    • 옵저버의 규칙인 ApplicationContextListener를 구현한다.
    • contextInitialized() 데이터를 파일에서 로딩한다.
    • contextDestroyed()에서 그 데이터를 파일에 Json형식으로 저장한다.
    • App 클래스에서 파일 데이터를 로딩하고 저장하는 코드를 이 클래스로 옮긴다.
      • 왜? 파일을 읽고 쓰는 것은 프로그램을 시작하고 종료할 때, 즉 특정 상황에서 호출하는 기능이다. 따라서 옵저버 패턴을 적용하기 딱 좋다.
  • App에 DataHandlerListener 옵저버를 등록한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.eomcs.pms.listener;


// 게시물, 회원, 프로젝트, 작업 데이터를 파일에서 로딩하고 파일로 저장하는 일을 한다.
public class DataHandlerListener implements ApplicationContextListener {
  
  //(1) List, File을 인스턴스 필드로 선언한다.
  // 여기서 List 객체는 App에서 모른다. 따라서 App에서 사용하기 위해
  // 파라미터로 받은 Map 객체에 넣어줘야 한다.
  List<Board> boardList = new ArrayList<>();
  File boardFile = new File("./board.json"); // 게시글을 저장할 파일 정보

  List<Member> memberList = new LinkedList<>();
  File memberFile = new File("./member.json"); // 회원을 저장할 파일 정보

  List<Project> projectList = new LinkedList<>();
  File projectFile = new File("./project.json"); // 프로젝트를 저장할 파일 정보

  List<Task> taskList = new ArrayList<>();
  File taskFile = new File("./task.json"); // 작업을 저장할 파일 정보

  
  @Override
  public void contextInitialized(Map<String, Object> context) {
    // 애플리케이션의 서비스가 시작되면 먼저 파일에서 데이터를 로딩한다.
    // 파일에서 데이터 로딩
    loadData(boardList, boardFile, Board[].class);
    loadData(memberList, memberFile, Member[].class);
    loadData(projectList, projectFile, Project[].class);
    loadData(taskList, taskFile, Task[].class);
    
    // 옵저버가 파일에서 데이터(게시물, 회원, 프로젝트, 작업 데이터)를 읽어 
    // List 컬렉션에 저장한 다음 
    // 발행자(App 객체)가 를 사용할 수 있도록  객체에 담아서 공유한다.
    context.put("boardList", boardList);
    context.put("memberList", memberList);
    context.put("projectList", projectList);
    context.put("taskList", taskList);
  }

  @Override
  public void contextDestroyed(Map<String, Object> context) {
    // 애플리케이션 서비스가 종료되면 컬렉션 객체에 보관 데이터를 저장한다.
    // 데이터를 파일에 저장
    saveData(boardList, boardFile);
    saveData(memberList, memberFile);
    saveData(projectList, projectFile);
    saveData(taskList, taskFile);
  }
  

  // 파일에서 JSON 문자열을 읽어 지정한 타입의 객체를 생성한 후 컬렉션에 저장한다.
  private <T> void loadData(
      Collection<T> list, // 객체를 담을 컬렉션
      File file, // JSON 문자열이 저장된 파일
      Class<T[]> clazz // JSON 문자열을 어떤 타입의 배열로 만들 것인지 알려주는 클래스 정보
      ) {
      //.....생략
  }
  
  private void saveData(Collection<?> list, File file) {
    //.....생략
  }
}

publisher-subscriber는 비동기 메시지 처리(java async message queue)할 때 사용한다.

옵저버는 서비스가 시작할 때까지 기다리다가 메서드가 호출되면 작업을 수행한다. 지정된 파일(제이슨 문자열이 들어 있는 파일)에서 데이터를 읽어서 list에 저장한다. 이때 App이라는 객체는 list필드를 모른다. 그럼 어떻게 해야 할까? list를 파라미터로 받은 Map 객체에 담아주면 된다. 즉 작업 데이터를 return하는 것이 아니라 파라미터로 받은 객체의 주소로 객체에 접근해서 추가하면 publisher도 수정된 객체를 사용할 수 있다. 파라미터로 실제로 객체를 넘겨 받은 것이 아니라 객체의 주소를 받은 것이기 때문에 App과 DataHandlerListener는 Map객체(의 주소)를 공유하고 있다고 할 수 있다. out 전용 파라미터가 없는 자바에서는 이런 방식으로 in-out 파라미터를 구현할 수 있다. 호출자로부터 받은 객체의 값을 꺼낼 수도 있고 담아서 호출자에게 줄 수도 있는 기능을 하는 이런 기능의 파라미터를 in-out 파라미터라고 한다.

out 전용 파라미터? C#의 경우 out 전용 파라미터가 있다. (OutArgExample(out int number)). 호출자가 이 값을 꺼내 쓸 수 있다. 자바의 경우 out전용 파라미터는 없고 return값만 있다. 단, 자바의 파라미터(변수)는 객체 그 자체가 아니라 객체 주소를 담기 때문에 Map 객체를 파라미터로 넘겨줌으로써 in-out 파라미터를 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class App {
  public static void main(String[] args) throws Exception {
    App app = new App();

    // 옵저버 등록
    app.addApplicationContextListener(new AppInitListener());
    app.addApplicationContextListener(new DataHandlerListener());
    
	app.service();
  }
  @SuppressWarnings("unchecked")
  public void service() {

    notifyApplicationContextListenerOnServiceStarted();
    // 옵저버 실행이 끝난 다음에 결과가 Map에 담겨 있다고 가정하고 get하면 된다.
    Map<String,Command> commandMap = new HashMap<>();

    // 옵저버가 작업한 결과를 맵에서 꺼낸다.
    List<Board> boardList = (List<Board>) context.get((List<Board>) context.get("boardList"));
    List<Board> memberList = (List<Board>) context.get((List<Board>) context.get("memberList"));
    List<Board> projectList = (List<Board>) context.get((List<Board>) context.get("projectList"));
    List<Board> taskList = (List<Board>) context.get((List<Board>) context.get("taskList"));
    //...
    notifyApplicationContextListenerOnServiceStopped();
  }
  //....
}
This post is licensed under CC BY 4.0 by the author.

학원 #55일차: Observer 패턴

학원 #57일차: 네트워크 API를 활용한 C/S 아키텍처

Loading comments from Disqus ...