오늘은 지난 시간에 만든 채팅 프로그램을 좀 더 발전시켜, 대화 목록을 DB에 저장하거나 채팅화면이 아닐지라도 채팅 목록을 수신받을 수 있도록 만들 예정이다. Show
그 전에! 지난 시간의 수업 내용을 전반적으로 살펴보자. 지난 시간 정리HTTP, HTTPS웹 애플리케이션을 개발하면 어쩔 수없이 ‘HTTP, HTTPS’라는 프로토콜, 곧 통신 규칙을 사용한다. 근데 이 규칙에는 크게 두가지 특성이 있다. 1) Stateless일단 상태 정보를 유지하지 않는다. 그래서 로그인 정보를 기억하지 않기 때문에 Session, cookie를 이용하여, 상태정보를 저장한다. 2) Request, Response통신 구조가 “요청이 있어야만 응답이 있다”라는 규칙을 따른다. 그래서 요청 없으면 서버는 능동적으로 응답으로 데이터를 전송할 수 없다.
WS / WSS우리의 서버인 Tomcat은 HTTP, WS 모두 지원한다. 그래서 프로토콜에 따라 HTTP는 DS로 WS-Endpoint로 연결이 된다, 웹 소켓 프로세스1) HandShaking - WS과 톰캣 사이에서 일어나는 과정
우리가 프론트에서 해당 코드를 통해 웹 소켓 객체를 만들어 연결을 시도하면 1차적으로 핸드쉐이킹 과정이 발생한다. 즉, TOMCAT과 WS 사이에 통신 규정을 정하는데 이를 ‘HandShake’(OSI 7 Layer)라고 한다. (*규정을 정함) 따라서 통신-연결이 아니라, 통신이 연결되면서 연결 신호, 데이터는 어떤 형태 등등 뭘 보낼지, 어떻게 보내고, 어떻게 응답할지 이런 과정을 내부적으로 신호를 주고받는다. 2) Modify HandShake기본적으로 세팅된 HandShake(규칙)을 사용하면, HTTP로 오는 데이터 정보를 WS의 EndPoint로 끌어올 수 없다. 그래서 규칙을 바꿔서 HTTP Session을 넣어서 EndPoint로 보내줘야 한다. EndPoint로 데이터를 보낼 땐, 톰캣에서 ServerEndpointConfig 객체에 넣어서 보낸다. 여기에는 설정정보 뿐만 아니라, 유저 속성을 넣을 수 있는 저장소가 존재한다. 그리고 저장소
안의 3) EndPoint(@ServerEndPoint)핸드쉐이킹 과정에서 EndPoint 어노테이션을 따라, 요청된 엔드포인트를 찾고 인스턴스를 생성한다. 해당 인스턴스 안에서 각 연결에 따른 작업이 실행되고, 개별 세션을 갖고 있다. 4) @OnOpen핸드쉐이킹이 끝나고 WS 세션이 연결되었을 때, 실행되는 메서드다. 주로 채팅에서는 각 인스턴스의 session 정보를 Set에 저장한다.
5) WS Session
모든 WS 연결자들의 세션을 공통으로 관리할 수 있도록, 쉽게 말하면 각 인스턴스가 메시지를 연결자 모두에게 보낼 수 있도록 ‘정적 변수’를 만들어 준다. 제대로 들어가면 Set 보다는 Map을 사용해 더 다양한 정보를 담아둔다. 6) @OnMessage클라이언트에서 WS 객체를 통해 서버로 메시지를 보내면, @OnMessage 어노테이션으로 연결된 메서드에서 매개변수로 데이터를 받는다. 이렇게 받은 데이터를 연결된 모든 사람에게 보낸다. 주로 이 상황에서도 멀티쓰레드에 의한 동시성 문제로 인해 세션을 저장한 Set의 길이가 줄어들면서 예외가 발생한다. 그래서 동기화 블록으로 작업을 고정처리 해준다.
7) @OnClose, @OnError해당 어노테이션으로 세션이 닫히고, 에러가 생겼을 때 세션 저장소에서 각 세션을 지운다. 8) 연결이 끊김결과적으로 우리가 방을 나가고, 다시 들어가면 기존 웹 소켓이 사라지고, 새로운 웹 소켓이 생성된다. 즉, 페이지 전환마다 웹 소켓은 새로 만들어진다. 그래서 처음 대화 내용은 다 사라진다.
채팅 내역 유지 구현먼저 대화 내용은 그 어디에도 저장되지 않고 HTML 안에 흔적으로 남을 뿐이다. 이에 대한 정책적인 부분은 개발자를 따른다. 하지만 우리는 유지하고 싶으니까, 그 방법을 알아보자. 일단 크게 2가지 방향성이 있다.
1. 로그아웃 전까지만 유지로그아웃 전까지만 유지한다는 것은 말 그대로 로그인 된 상태에선 페이지 전환이 일어나도, 채팅은 지속으로 진행된다는 것이다. 예를 들어 우리가 채팅 페이지에서 내 정보 확인 페이지를 다녀와도, 그 사이에 진행된 채팅 내역을 볼 수 있다는 것이다. 이 방법이 가장 간단한 방법인데, 주로 FIFO(First In First Out) 구조인 큐(Queue)를 사용해서 각 세션에서 보내는 메시지들을 저장하는 것이다. 그럼 FIFO(First In First Out)이 뭘까? 데이터를 다루는 자료구조 중 하나인데, 리스트의 형태를 갖고 있다. 다만 데이터가 들어오는 입구와 나가는 곳이 있는데, 일정 길이 안에서 데이터가 차면 먼저 들어온 데이터부터 나가는 형식이다. 이를 구현하기 위해서 구글에서 만든 EvictingQueue를 사용할 것이다. 그럼 차근 차근 사용법을 알아보자. Guava EvictingQueue대화 내역을 EvictingQueue을 저장한 다음, 이를 세션에 저장하여 RAM에다가 넣어두면, 큐가 꽉 찼다는 가정하에 오래 순서대로 자동 삭제가 된다. 1) Guava 라이브러리 저장먼저 Maven에서 Guava 라이브러리를 끌어와 pom.xml에 넣어준다.
EvictingQueue는 Guava 라이브러리 내부에 있기에, 톰캣이 실행되면서 메모리 상에 인터페이스가 존재하니, 이를 가져다 쓰면 된다. 2) 인스턴스 생성생성 방법은 간단하다. EvictingQueue의 클래스 메서드를 통해서 인자로 길이를 저장해주면 된다.
queue를 출력하면 {2, 3}이 출력된다. 3) 클래스 변수로 만들기이렇게 생성된 queue는 클래스 변수가 되어야 한다. 이건 앞서 WS 세션을 저장한 Set과 마찬가지인데, 나중에 저장된 대화 내역을 클라이언트로 뿌려줄 것이기 때문에 모두가 하나의 객체를 공유해야 한다.
참고로 제너릭은 [ ChatDTO ]
4) @onMessage이제 메시지를 보낼 때마다 큐 안에 데이터를 저장해둘 것이다.
5) @onOpen@onMessage에서 저장한 대화 내역을 이제, WS로 접속하는 시점에 클라이언트로 보내서 이를 HTML로 출력하게 할 것이다. 단, 형식은 사용하기 쉽도록 JSON으로 보낸다.
6) 클라이언트에서 출력하기돌아온 JSON을 반복문을 돌려서 출력해준다.
2. DB에 저장하기1) 개요이 방식은 지우지 않는 한 대화 내용이 사라지지 않는다. 이를 메신저형라고 하는데, 이를 위해선 DB에다가 채팅 내용을 전부 기록한다.
데이터를 넣는 과정는 @OnMessage 안에서 데이터를 사용자에게 뿌리기 전에 진행되며, 이 때문에 DB와 연결 과정을 Service 페이지로 사용하는게 좋다. 그래서 흔히들 @Autowired하거나 @Component로 Service 계층을 만들어서 해서 사용하려 한다. 물론 만들 수는 있고, 적용도 된다.
더욱이 스프링 어노테이션으로 설정 인스턴스는 그 당시, 스프링 풀이 실행되면서 딱 한 번만 가능하다. 그래서 DI로 미리 꽂아놓는 것도 의미가 없다. 그래서 DI가 아닌 DL로 내가 원할 때 메모리에 생성된 인스턴스를 찾아서 꺼내와서 사용한다. 즉, 스프링 요소가 아닌 클래스가 스프링 풀 안의 것을 가져다 쓰고 싶으면 DL을 하면 된다는 것이다.
결과적으로 DI로 서비스 레이어를 DI 해봤자 의미가 없게 된다. 따라서 스프링 풀 밖에서 스프링 풀 안에 있는 요소를 DL로 꺼내 쓰면 된다. 2) 문제일단, Spring Container라고 불리는 것은 코드 상에선 근데 DL을 할 건데, 중요한 건 EndPoint의 우리는 톰캣이 만들어 준 스프링 컨테이너의 주소를 모른다는 것이다. 3) implement ApplicationContextAware앞서 문제는 먼저
4) 오버라이딩인터페이스를 상속받은 클래스는 그 내용을 오버라이딩해줘야 한다.
보다시피 setApplicationContext는 매개변수로 ApplicationContext, 곧 스프링 컨테이너의 주소를 받는다. 그래서 우리는 이를 사용할 수 있는데, 인스턴스 해당 인스턴스 자체는 서버 구동 시, 만들어지니 이를 클래스 변수로 연결해서 사용하면 된다. (어차피 한 개만 만들어지고...) 이 과정을 쉽게 보면, 웹 애플리케이션 같은 스프링 컨테이너의 주소를 알 수 없는 상황에선 DL을 할 수 없으니까, 대안으로 아무 클래스를 만들고 ApplicationContextAware를 상속 받으라는 것이다. 그렇게 ApplicationContextAware를 구현하는 클래스가 Bean으로 생성되었다면, 그 클래스는 setApplicationContext() 추상메서드를 채웠을테니, 스프링이 스프링 컨테이너가 가동되는 시점에, 이 주소를 메서드의 매개변수로 넣어준다는 것이다. 5) Service 레이어 생성실험용으로 Service 레이어를 만들어준다. 이때 어노테이션 처리를 해줬기 때문에 해당 인스턴스도 메모리 상에 존재하게 된다.
6) .getBean(자료형.class)이제 엔드포인트에서 [ EndPoint ] |