실습 - 발행자와 구독자의 데이터 공유
git/eomcs-java-project/mini-pms-33-b
옵저퍼 패턴과 자바에서 제공하는 Servlet 클래스들의 관계는 유사하므로 옵저버 패턴을 확실히 익혀야 한다.
ServletContextListener
웹 어플리케이션을 실행하는 데 필요한 초기화 작업이나 웹 어플리케이션이 종료된 후 사용된 자원을 반환하는 등의 작업을 수행하는데 사용된다. 이를 통해 웹 어플리케이션이 시작되고 종료될 때 특정한 기능을 실행할 수 있다.
출처 : ServletContextListener 이벤트 처리| 작성자 원종천
구체적으로는 Servlet의 context의 변경 사항이 생길 때마다 그에 따른 수행 작업을 하는 클래스이다.
이번 실습에서는 발행자에서 옵저버에게 파라미터로 데이터를 넘겨 서로가 같은 데이터를 공유할 수 있도록 해볼 것이다.
발행자에서 옵저버의 메서드를 호출할 때 파라미터로 옵저버와 발행자 간의 공유할 객체를 넘긴다. 주로 실무에서는 key와 value의 형태로 데이터를 보관할 수 있는 map 객체를 넘긴다. 옵저버는 넘겨받은 map 객체을 이용해서 작업을 수행하고, map 객체에 결과를 저장하여 리턴한다.
발행자와 옵저버가 함께 공유할 데이터는 App의 context이므로 ApplicationContextListener 인터페이스들의 메서드에서 Context 정보를 담은 Map 객체를 파라미터를 추가할 것이다.
import java.util.Map;
public interface ApplicationContextListener {
void contextInitialized(Map<String, Object> context);
void contextDestroyed(Map<String, Object> context);
}
또한 인터페이스를 구현하고 있는 구현체들의 메서드에서도 똑같이 파라미터를 추가한다.
import java.util.Map;
import com.eomcs.context.ApplicationContextListener;
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 클래스에서는 옵저버와 공유할 App의 context 정보를 담을 Map 객체를 필드로 저장한다. 생성하는 객체는 HashTable로 지정한다.(HashMap으로 해도 상관없다.)
Map<String, Object> context = new HashTable<>();
HashMap과 HashTable의 차이
- HashMap은 key나 value에 null을 저장할 수 있지만 HashTable에서는 null을 어디서도 저장할 수 없다.
- HashMap은 concurrent 멀티 스레딩 환경에서 threadsafe하지 않다. synchronize해서 적절히 lock을 걸어 조치를 취하지 않는다.
HashTable은 synchronize하여 스레드가 들어올 때마다 lock을 걸기 때문에 threadsafe하다.
또한 App에서 정의된 notify 메서드들에서 contextInitialized와 contextDestroyed를 호출하고 있기 때문에 파라미터로 context 를 넘겨준다.
private void notifyApplicationContextListenerOnServiceStarted() {
for (ApplicationContextListener listener : listeners)
listener.contextInitialized(context);
}
private void notifyApplicationContextListenerOnServiceStopped() {
for (ApplicationContextListener listener : listeners)
listener.contextDestroyed(context);
}
데이터 파일에서 데이터를 로딩하고 저장하는 기능을 수행하는 DataHandlerListener 옵저버 클래스를 정의하고 App에서 데이터 저장, 로드 메서드와 메서드 호출 코드를 이 클래스 안으로 옮긴다.
public class DataHandlerListener implements ApplicationContextListener {
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);
}
@Override
public void contextDestroyed(Map<String, Object> context) {
saveData(boardList, boardFile);
saveData(memberList, memberFile);
saveData(projectList, projectFile);
saveData(taskList, taskFile);
}
private <T> void loadData(
Collection<T> list,
File file,
Class<T[]> clazz
) {
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
list.addAll(Arrays.asList(new Gson().fromJson(in, clazz)));
System.out.printf("'%s' 파일에서 총 %d 개의 객체를 로딩했습니다.\n",
file.getName(), list.size());
} catch (Exception e) {
System.out.printf("'%s' 파일 읽기 중 오류 발생! - %s\n",
file.getName(), e.getMessage());
} finally {
try {
in.close();
} catch (Exception e) {
}
}
}
private void saveData(Collection<?> list, File file) {
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(file));
Gson gson = new Gson();
String jsonStr = gson.toJson(list);
out.write(jsonStr);
out.flush();
System.out.printf("총 %d 개의 객체를 '%s' 파일에 저장했습니다.\n",
list.size(), file.getName());
} catch (IOException e) {
System.out.printf("객체를 '%s' 파일에 쓰는 중 오류 발생! - %s\n",
file.getName(), e.getMessage());
} finally {
try {
out.close();
} catch (IOException e) {
}
}
}
}
이렇게 하면 contextInitialized 메서드에서 불러들어온 데이터를 App에서 받지 못하므로 파라미터로 받은 Map 객체에 담아줘야한다.
@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);
context.put("boardList", boardList);
context.put("memberList", memberList);
context.put("projectList", projectList);
context.put("taskList", taskList);
}
이제 App에서 notifyApplicationContextListenerOnServiceStarted()를 호출한 후 context 안에 담긴 value값들을 꺼내서 사용할 수 있도록 List 변수에 저장한다.
App에서 service 메서드를 실행하면서 List들에 생기는 변경사항은 실시간으로 DataHandlerListener의 필드에서도 반영되고 있기 때문에 굳이 다시 Map 객체에 담아줄 필요는 없다. 마찬가지로 contextDestroyed 메서드 몸체에서도 굳이 파라미터로 받은 Map 객체에서 value를 꺼낼 필요는 없다. 이미 DataHandlerListener의 필드들을 사용하면 되는 것이다.
@SuppressWarnings("unchecked")
public void service() throws Exception {
notifyApplicationContextListenerOnServiceStarted();
List<Board> boardList = (List<Board>)context.get("boardList");
List<Member> memberList = (List<Member>)context.get("memberList");
List<Project> projectList = (List<Project>)context.get("projectList");
List<Task> taskList = (List<Task>)context.get("taskList");
.
.
.
notifyApplicationContextListenerOnServiceStopped();
}
@Override
public void contextDestroyed(Map<String, Object> context) {
saveData(boardList, boardFile);
saveData(memberList, memberFile);
saveData(projectList, projectFile);
saveData(taskList, taskFile);
}
따라서 각 List 객체들은 처음에 DataHandlerListener에서 생성되었지만 그것들의 주소를 App도 알고, 각 xxxHandler들도 모두 알고 있는 셈이므로 변경사항을 매번 다른 클래스에게 알려줄 필요가 없다.
지금 App에서는 DataHandlerListener 옵저버 객체 메서드의 파라미터로 Map 객체를 넘기고 있다. 반면, ServletContextListener 클래스는 contextInitialized 메서드와 contextDestroyed 메서드에서 파라미터로 ServletContextEvent 클래스 타입이 지정되어있다. 이 클래스는 ServletContext 객체를 리턴하는 getServletContext라는 메서드를 갖는다.
실습 - 네트워크
git/eomcs-java-project/mini-pms-34-a
git/eomcs-java-project/mini-pms-client-34-a
git/eomcs-java-project/mini-pms-server-34-a
git/eomcs-java-project/mini-pms-34-b
git/eomcs-java-project/mini-pms-client-34-b
git/eomcs-java-project/mini-pms-server-34-b
네트워크 API를 활용하여 stateful 통신 방식으로 클라이언트와 서버 애플리케이션 사이에 데이터를 주고 받으려고 한다.
일단 클라이언트 프로그램과 서버 프로그램을 만들 수 있도록 gradle을 통해 자바 프로그램 두 개를 빌드한다. 또, gradle의 eclipse 플러그인을 장착한 후, gradle eclipse 명령어를 실행하여 각 프로그램을 eclipse IDE 용 프로그램으로 전환한다.
이제 두 프로그램에 Socket 클래스와 입출력 스트림을 이용하여 간단한 메시지를 주고 받을 것이다.
클라이언트 프로그램의 ClientApp 클래스에서는 다음과 같이 코드를 작성한다.
- 서버와 연결된 소켓 생성
- 소켓에 대한 입출력 도구 생성
- 서버에게 인삿말 건네기
- 서버에게 응답 받기
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ClientApp {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 8888);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());) {
out.println("안녕하세요?");
out.flush();
System.out.println(in.readLine());
} catch (Exception e) {
e.printStackTrace();
}
}
}
서버 프로그램의 ServerApp 클래스에서는 다음과 같이 코드를 작성한다.
- 서버 소켓을 생성
- 클라이언트와 연결된 소켓 리턴
- 리턴받은 소켓에 대한 입출력 도구 생성
- 클라이언트가 보낸 메시지 입력받기
- 입력받은 메시지를 다시 클라이언트에게 보내기
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerApp {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("서버 실행 중....");
try (Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream())) {
out.println(in.readLine());
out.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
'국비 교육' 카테고리의 다른 글
2020.10.20 일자 수업 : 스레드 풀, ExecutorService, DBMS (0) | 2020.10.20 |
---|---|
2020.10.12 일자 수업 : Observer 디자인 패턴 (0) | 2020.10.12 |
2020.10.7 일자 수업 : 웹 서버, 채팅 프로그램, 스레드 (0) | 2020.10.09 |