본문 바로가기

IT 기술/개념정리

유사 채팅방 구현하기(2)

개발목표

1. 채팅 방 접속 시 웹소켓 연결 정보를 찾아 채팅방 메모리에 변수에 저장한다.

2. 커넥션이 끊겼을 때 입장했던 방 사람들에게만 사실을 브로딩 캐스팅한다. 물론 메모리의 데이터는 remove한다.

3. 채팅방 입장 시 방 사람들에게만 사실을 브로딩 캐스팅한다.

4. 유요하지 않은 Text(none Json String)을 보냈더라도 커넥션을 유지한다.

 

 

1. 채팅 방 접속 시 웹소켓 연결 정보를 찾아 채팅방 메모리에 변수에 저장한다.

  • 채팅방 정보는 WAS메모리에 기록한다. 
    1. 커넥션 성공 시 Map<유저아이디, Session 객체> 로 저장된다.
    2. 추후 이 Map은 Map<채팅방 String, List<Map>> 형태로 2중 저장된다.(채팅방 입장 시)
    3. Map으로 선택한 이유는 HashMap을 사용해 빠른 탐색을 하기 위해서이다. (List의 경우 for문을 돌릴 수 밖에 없어서)
public void createOrJoinGrpNoByUserSession(Map<String, Object> inParam) throws Exception {
		if(BzUtils.isEmpty(WebSocketConfig.getSessionMap().get(inParam.get(DBMeta.uid.name())))) {
			inParam.put(ReturnMeta.message.name(), "연결된 소켓 통신이 없습니다.");
			throw new Exception();
		}
		
		
		if (BzUtils.isNotEmpty(inParam.get(DBMeta.uid.name()))
				&& BzUtils.isNotEmpty(inParam.get(DBMeta.grpNo.name()))) {
			
			Map<String, List<Map<String, WebSocketSession>>> sessionListIncludeMap = WebSocketConfig
					.getGrpNoSessionListIncludeMap();
			List<Map<String, WebSocketSession>> sessionList = new ArrayList<Map<String, WebSocketSession>>();
			// Step1 그룹 맵 중 grpNo가 있으면 해당 리스트에 접속정보 넣기
			if (BzUtils.isNotEmpty(sessionListIncludeMap.get(inParam.get(DBMeta.grpNo.name())))) {
				sessionList = sessionListIncludeMap.get(inParam.get(DBMeta.grpNo.name()));
				
				removeSessionElementByUid(sessionList,(String)inParam.get(DBMeta.uid.name()));

				sessionList.add(Map.of((String) inParam.get(DBMeta.uid.name()),
						WebSocketConfig.getSessionMap().get(inParam.get(DBMeta.uid.name()))));

			}
			// Step1_1 그룹맵이 없다면 grpNo를 키로 그룹리스트를 생성한다.
			else {
				sessionList.add(Map.of((String) inParam.get(DBMeta.uid.name()),
						WebSocketConfig.getSessionMap().get(inParam.get(DBMeta.uid.name()))));
				sessionListIncludeMap.put((String) inParam.get(DBMeta.grpNo.name()), sessionList);

			}

		}

	}
  • 웹소켓 연결시 인터셉터가 연결가능 여부를 판단한다. 
    • 개발 편의 상 at의 payload 대신 url query String으로 받아서 Key를 만든다.
String atValue = UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams()
						.getFirst(ProcessMeta.at.name());
				if (BzUtils.isNotEmpty(atValue)) {
					sessionUid=atValue;
				}
				httpSession.put(ProcessMeta.clientSession.name(),sessionUid);

 

2. 커넥션이 끊겼을 때 입장했던 방 사람들에게만 사실을 브로딩 캐스팅한다. 물론 메모리의 데이터는 remove한다

 

  • 채팅 방 입장 시 입장 성공 정보는 DB에 기록한다.
  • 입장 시 저장했던 메모리 변수에서 유저 커넥션 정보를 찾는다.
public void removeConnectionMemoryByUid(String uid) throws IOException {
		Map<String, List<Map<String, WebSocketSession>>> sessionListIncludeMap = WebSocketConfig
				.getGrpNoSessionListIncludeMap();
		// 커넥션이 끊어졌을 때 접속이력(그룹번호)을 찾는다.
		Map<String, Object> inParam = new CamelHashMap();
		inParam.put(DBMeta.uid.name(), uid);
		List<CamelHashMap> grpList = groupOrderMapper.selectGrpOrdInfoByUid(inParam);
		if(BzUtils.isEmpty(grpList)) {
			return;
		}
		// 그룹 번호로 방을 찾고 그 방에서 제거한다.
		for (CamelHashMap grpMap : grpList) {
			List<Map<String, WebSocketSession>> sessionList = sessionListIncludeMap.get(grpMap.get(DBMeta.grpNo.name()));
			if(BzUtils.isEmpty(sessionList)) {
				return;
			}
			removeSessionElementByUid(sessionList,uid);
			
			//커넥션이 종료되었을 때 그룹멤버들에게 종료정보를 전달한다.
			inParam.put(DBMeta.grpNo.name(), grpMap.get(DBMeta.grpNo.name()));
			Map<String, Object> broadCastMap = new CamelHashMap();
			broadCastMap.put(DBMeta.grpNo.name(), grpMap.get(DBMeta.grpNo.name()));
			broadCastMap.put(ReturnMeta.refreshSid.name(), "GROUP0004");
			inParam.put(ReturnMeta.data.name(), broadCastMap);
			broadcastGrpNoMember(inParam);

		}
		

	}

 

3. 채팅방 입장 시 방 사람들에게만 사실을 브로딩 캐스팅한다.

  • 2. 내용과 비슷하다.
public void broadcastGrpNoMember(Map<String, Object> inParam) throws IOException {
		List<Map<String, WebSocketSession>> sessionList = new ArrayList<Map<String, WebSocketSession>>();
		Map<String, List<Map<String, WebSocketSession>>> sessionListIncludeMap = WebSocketConfig
				.getGrpNoSessionListIncludeMap();
		if (BzUtils.isNotEmpty(sessionListIncludeMap.get(inParam.get(DBMeta.grpNo.name())))) {
			sessionList = sessionListIncludeMap.get(inParam.get(DBMeta.grpNo.name()));
			Gson gson = new Gson();
			inParam.remove(DBMeta.usrPower.name());
			List<CamelHashMap> grpList = groupOrderMapper.selectGrpOrdInfoByGrpNo(inParam);
			for (CamelHashMap grpMap : grpList) {
				String uid = (String) grpMap.get(DBMeta.uid.name());
				for (Map<String, WebSocketSession> map : sessionList) {
					WebSocketSession wss = map.get(uid);
					if(BzUtils.isNotEmpty(wss)) {
						wss.sendMessage(new TextMessage(gson.toJson(inParam.get(ReturnMeta.data.name()))));
					}
					
				}
			}
		}
		
	}

 

4. 유요하지 않은 Text(none Json String)을 보냈더라도 커넥션을 유지한다.

  • handleTextMessage 안에 try catch문을 사용해서 커넥션 익셉션으로 인한 종료를 방지했다.
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
		Map<String, Object> servieRet = new CamelHashMap();
		Gson gson = new Gson();
		servieRet.put(ReturnMeta.code.name(), CommonConstants.ERROR);
		servieRet.put(ReturnMeta.message.name(), "미지정오류");
		try {
			
			...
		} catch (Exception e) {
			
			...
			session.sendMessage(new TextMessage(gson.toJson(servieRet)));
		}

	}

 

앞으로 체크할 일

  • 위의 로직은 TextWebSocketHandler afterConnectionClosed 메서드가 잘 작동하길 바라면서 작성했다. 
  • 로직이 잘 작동하지 않으면 static 변수의 크기가 너무 커질 수 있다.
  • WAS가 이중화 되어있으면 커넥션을 담는 static 변수 공유가 안된다. 그래서 메모리에 채팅방 정보를 없으면 insert하게 설계했다.
  • 브로드캐스팅 할 트리거가 이중화된 WAS에서 어떻게 구성할지 고민된다.(카프카 or 레디스?)

'IT 기술 > 개념정리' 카테고리의 다른 글

Debezium 커넥터 복구 방법  (1) 2024.03.28
유사 채팅방 구현하기(1)  (0) 2024.02.02
Debezium 개념정리  (0) 2024.01.23