채팅방의 나가기 기능과 찐막

김채원·2025년 6월 13일

1. 생각정리

정말 한 주간 채팅기능 만들기에 매달려 있던것 같은데
드디어 끝이 보이는 느낌이다

이틀 전에 코드를 갈아엎고
구현을 거진 끝내가는 중 고민이 생겼다

채팅방의 나가기 기능은 어떻게 만드는거지??

지금껏 작성해봤던 코드는 대부분 무언가가 있거나 없거나여서
소프트딜리트 하드딜리트 중에서만 고민하면 됐는데 ..

채팅방은 내가 나가더라도 상대방은 가지고 있을테니
삭제를 하면 안되잖아 ?? 라는 생각이 파칭하고 들었다

내가 채팅방을 나가도 상대방은 채팅방이 있어야할테고
내가 채팅방 목록을 조회했을 때 해당 채팅방이 나와서는 안된다

유저는 MySQL, 채팅방은 MongoDB로 설계되었으므로
연관관계는 맺을 수 없다

해당 조건을 토대로 내가 생각한 방법은 두 가지가 있었는데

1. 채팅방에 누가 있는지 상태를 볼 수 있는 필드 만들기

	private boolean sellerExist;

	private boolean customerExist;

이런 느낌으로 필드를 갖고
누군가 나간다면 해당 값을 false로 처리해
조회 시 조건을 걸면 되지 않을까?가 첫 번째였고

2. 중간 테이블을 만들어 해당 값을 처리하기

유저와 채팅방 사이에 MySQL 테이블을 하나 더 만들어 유저와 연관관계를 갖고
해당 테이블은 채팅방의 id를 리스트로 가지고 있다

채팅방을 조회할 때 해당 테이블의 값을 불러와
리스트에 있는 id의 채팅방만 조회하는 방식이 두 번째였다

둘 중 어느 방법이 나을까를 결정하기 위해
각각의 단점이 뭐가 있을까 생각해봤는데

우선 첫 번째 방법의 단점을 생각해봤을 때
쿼리문이 조금 복잡해질거 같다는 생각을 했다

로그인 한 회원이 판매자인지 구매자인지 검증을 해야했고
해당 필드가 true라면 조회하지 않도록 설계해야했다

두 번째 방법의 단점을 생각해봤을 때의 문제는
만약 둘 다 채팅방을 자주 나가지 않는 사람이라고 가정했을 때
이미 해당 테이블에 데이터가 2개가 쌓이게 된다

단지 중간역할을 하는 테이블이 너무 커질지도 모른다는 생각이 들어서
조금 망설여졌다

결론적으로 말하면 첫 번째 방법을 선택했다

구현

controller

	@DeleteMapping("/{chatRoomId}")
	public ResponseEntity<CommonResponse<Void>> leaveChatRoom(
		@AuthenticationPrincipal CustomPrincipal currentUser,
		@PathVariable String chatRoomId
	) {
		chatService.leaveChatRoomById(currentUser.getId(), chatRoomId);

		return CommonResponse.of(SuccessCode.DELETED);
	}

service

@Override
	public void leaveChatRoomById(Long userId, String chatRoomId) {

		ChatRoom chatRoom = findChatRoom(chatRoomId);

		if (chatRoom.isMember(userId)) {
			throw new CustomException(ErrorCode.FORBIDDEN);
		}

		chatRoom.leaveChatRoom(userId);
		chatRepository.save(chatRoom);
	}

repository

@Override
	public List<ChatRoom> findChatRoomsByUserId(Long userId, Pageable pageable) {

		//조회 기준 등록 QueryDSL BooleanBuilder와 유사
		Criteria criteria = new Criteria().orOperator(
			Criteria.where("sellerId").is(userId).and("sellerExist").is(true),
			Criteria.where("customerId").is(userId).and("customerExist").is(true)
		);

		Aggregation aggregation = Aggregation.newAggregation(
			Aggregation.match(criteria),
			Aggregation.sort(Sort.by(Sort.Direction.DESC, "lastMessageDate")),
			Aggregation.skip(pageable.getOffset()),
			Aggregation.limit(pageable.getPageSize())
		);

		return mongoTemplate.aggregate(aggregation, "chatroom", ChatRoom.class).getMappedResults();
	}

첫 번째 방법을 선택한 이유는 ..
구현이 어렵지 않기 때문이었다 ㅎ

만약 MySQL에서 구현한다고 생각하면
where절을 통해 조건을 설정해주어야겠지만
나는 어그리게이트를 통해 조건을 걸어주고 있었으니 ..
and를 통해 내가 원하는걸 넣어주기만 하면 됐기 때문이다

사실 성능을 생각해도 둘 다 비슷비슷 할거같았다

둘 다 구현하고 테스트를 해보면 좋았긴하겠지만
시간이 빠듯하기도 했고
내 지식으로 생각해 봤을때 사실 큰 차이가 없을 것 같았다

트러블슈팅

문제 상황

코드를 구현하는중에 문제가 생겼다

분명 포스트맨으로 테스트 할 때 200OK와 함께 삭제 됐다는 글이 떴는데

조회를 해보니 여전히 해당 채팅방이 보이는 것이었다 ..
코드를 아무리 다시봐도 문제는 없는거 같아서
실제로 db에 반영이 됐는지 먼저 확인해보기로 했다

문제 원인

우선 데이터 베이스를 확인해보니

내가 바보같이 판매자와 구매자의 아이디를 동일하게 만들었다

코드가 동작하지 않는것이 아닌
애초에 조건이 잘못되어있었다

	@Override
	public ChatRoomCreateResponseDto saveChatRoom(Long userId, Long tradeBoardId) {

		TradeBoard tradeBoard = tradeBoardRepository.findById(tradeBoardId)
			.orElseThrow(() -> new CustomException(ErrorCode.TRADE_BOARD_NOT_FOUND));

		User user = findUser(userId);

		if (tradeBoard.isOwner(userId)) {
			throw new CustomException(ErrorCode.CANNOT_SEND_MESSAGE_TO_SELF);
		}

		ChatRoom chatRoom = ChatRoom.builder()
			.tradeBoardId(tradeBoardId)
			.sellerId(tradeBoard.getUserId())
			.customerId(userId)
			.build();

		chatRepository.save(chatRoom);

		return new ChatRoomCreateResponseDto(chatRoom);
	}

오히려 좋아.
채팅방 생성 메서드에 자신인지 검증하는 코드를 하나 추가해줬다

문제해결

이후 값을 다르게 설정해 확인해보니



값이 깔끔하게 삭제되는걸 확인할 수 있다

마무리

글이 꽤나 싱겁게 끝난거같아 아쉽지만 재밌는? 고민을 한거같아서 뿌듯하다. 가끔 이렇게 생각을 요하는 일들이 나는 잘맞는것같다. 앞으로 개발을 하면서 채팅방 나가기처럼 기존에 내가 사용하던 방식과는 다른것들을 마주할 일이 많이 생길거같은데 그때도 오늘처럼 즐겁게 임할 수 있으면 좋겠다.

profile
김채원 판교간다

0개의 댓글