[나만무] Lock이 필요한 상황이였을까?

Mando·2024년 8월 18일
0

정글

목록 보기
3/3

Lock을 적용시킨 API 설명

1:1 대화를 시작하려고 할 때 아래 API를 호출하게 된다.

  • 만약 둘 사이에 1:1 채팅방이 존재한다면 해당 채팅방을 return 받는다.
  • 둘 사이에 1:1 채팅방이 존재하지 않는다면 둘 사이의 채팅방을 생성하여 return 한다.
	@Override
	@PostMapping("/chat-room/opposite/{oppositeId}")
	public ApiResponse<SuccessBody<RoomResponse>> joinDirectRoom(
			@Member MemberInfo memberInfo, @PathVariable("oppositeId") Long oppositeId) {
		RoomResponse response = roomService.createDirectRoom(memberInfo.getMemberId(), oppositeId);

		return ApiResponseGenerator.success(
				response, HttpStatus.OK, RoomSuccessMessageCode.CREATE_ROOM);
	}

문제는 왜 발생했는가?

두 유저 사이에는 채팅방이 존재하지 않기 때문에

두 유저가 각각 채팅방을 생성하였고

그 결과 서로가 다른 채팅방을 구독하고 있는 상황이 발생하였다.
(서로에게 메시지를 보내도 메시지를 받지 못 하는 상황...)

이를 위한 해결방법은 무엇이 있을까?

1. 두 유저 사이에 채팅방을 미리 생성해두자

문제점은 만약 유저 1, 유저 2 사이에 채팅이 이루어지지 않는다면

채팅방을 생성할 필요가 없지만 이 방식대로 하면 채팅방이 생성이 된다.

즉 불필요한 채팅방까지 생성하게 되는 문제가 생긴다.

만약 유저가 n명 있을 때, 각 유저 쌍 사이에 채팅방을 생성한다고 가정하면, 총 생성되는 채팅방의 개수는 n^2개가 된다.

지금 우리 프로젝트에서는 유저의 수가 적지만 더 많은 유저의 수를 가지는 더 큰 규모의 프로젝트에서는 적용하지 못 할 것 같다는 생각이 든다.

2. 비관적 락을 사용하여 방 생성을 1개로 막자

그럼 아예 비관적 락을 사용해서 한 유저만 채팅방을 생성하도록 하자! 라는 생각을 했다.

즉 여기서는 findCommonChatRoom에서 공통된 채팅방이 있는지 조회 할 때 비관적 락을 사용하였다.

	@Transactional
	public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
		ChatRoom commonChatRoom =
				chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);

		if (commonChatRoom == null) {
			Long chatRoomId = createNewChatRoom(senderId, oppositeId);
			return RoomResponse.of(chatRoomId);
		}

		return RoomResponse.of(commonChatRoom.getId());
	}

(테스트 코드 실행 시간 첨부)

3. named lock을 사용하자

  • Request1 : 유저1-유저2 사이의 채팅방 조회(없으면 생성)
  • Request2 : 유저1-유저3 사이의 채팅방 조회(없으면 생성)

두 요청이 들어왔다고 해보자

이때 비관적 락의 경우 Request2는 Request1이 다 처리가 될 때까지 block되고 기다려야 한다.

사실상 여기서 lock을 걸었던 이유는 두 유저 사이의 채팅방이 2개 이상이 생성되지 않도록 막기 위함이였는데

다른 유저 사이의 채팅방을 조회하는 케이스도 block된다.

따라서 두 유저의 정보를 바탕으로 named lock을 사용하면

  • Request1 : 유저1-유저2 사이의 채팅방 조회(없으면 생성)
  • Request2 : 유저1-유저3 사이의 채팅방 조회(없으면 생성)

의 경우에는 Request1, Request2 모두 동시에 처리가 가능하고

  • Request1 : 유저1-유저2 사이의 채팅방 조회(없으면 생성)
  • Request2 : 유저1-유저2 사이의 채팅방 조회(없으면 생성)

의 경우에는 Request2는 Request1의 처리가 다 끝날 때까지 block이 되므로 원하는 비즈니스 로직을 만들 수 있을 뿐더러 서비스의 처리 속도 또한 좋아질 것이라고 생각했다.

@Service
@RequiredArgsConstructor
public class FacadeRoomService {
	private final RoomService roomService;
	private final ChatRoomRepository chatRoomRepository;

	public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
		Long chatRoomId = 0L;

		ChatRoom commonChatRoom =
				chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);
		if (commonChatRoom != null) {
			return RoomResponse.of(commonChatRoom.getId());
		}

		String lockName = getNamedLockName(senderId, oppositeId);
		try {
			chatRoomRepository.getLock(lockName, 60);
			ChatRoom exist = chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);
			if (exist != null) {
				return RoomResponse.of(exist.getId());
			}
			chatRoomId = roomService.createDirectRoom(senderId, oppositeId);
		} finally {
			chatRoomRepository.releaseLock(lockName);
		}
		return RoomResponse.of(chatRoomId);
	}

	private String getNamedLockName(Long senderId, Long oppositeId) {
		return "direct_room_lock_"
				+ Math.min(senderId, oppositeId)
				+ "_"
				+ Math.max(senderId, oppositeId);
	}
}

(테스트 코드 실행 속도 첨부하기)

빈번하게 일어나는 상황이 아닌 것 같다

사실 내가 고려한 상황은 정말 특수한 상황이다.

  • 이전에 유저1, 유저2 사이의 채팅 내역이 없어야 하며(두 유저 사이의 채팅방이 없다.)
  • 두 유저가 동시에 서로에게 채팅을 보내려고 해야한다

위 2가지 조건이 만족해야만 두 유저 사이의 채팅방이 2개가 생성되어 서로 다른 채팅방을 subscribe하는 문제가 발생한다.

사실 위 케이스가 프론트 팀원과 채팅 기능이 제대로 되는지 확인하는 과정에서 발생하기는 했다.

그러나 정말 특수한 상황이기에 해당 이슈를 발견한 시점에는 해결하지 않았고 팀 안에서 다른 기능을 구현할 필요가 없을 뿐더러 다른 이슈를 해결할 필요가 없었기에 해당 이슈를 해결했다.

그럼 lock을 사용했어야만 할까?

두 유저가 동시에 서로 사이의 채팅방을 동시에 생성하는 상황이 생기더라도 결국 두 유저의 목적은 '동일'하다.

각 방마다 고유의 UUID를 부여하자

두 유저의 값을 기반으로 채팅방에 고유한 값을 부여하였고
해당 값은 UNIQUE column으로 설정하였다.
그 결과 두 유저 사이의 채팅방이 2개 이상 생성되지 않았고 단 1개만 생성되는 요구사항을 만족하였다.

	@Transactional
	public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
		String roomUUID = getRoomUUID(senderId, oppositeId);
		ChatRoom commonChatRoom = chatRoomRepository.findByRoomUUID(roomUUID);

		if (commonChatRoom != null) {
			return RoomResponse.of(commonChatRoom.getId());
		}

		Long savedRoomId = saveChatRoom(senderId, oppositeId);
		return RoomResponse.of(savedRoomId);
	}
    
    	private String getRoomUUID(Long senderId, Long oppositeId) {
		String combinedString = Math.min(senderId, oppositeId) + "_" + Math.max(senderId, oppositeId);
		return UUID.nameUUIDFromBytes(combinedString.getBytes()).toString();
	}

기술에 집중하지 말자

나는 코딩을 이용해서 "세상의 문제를 해결하고자 하는 사람이다"
근데 이번 케이스에는 동시성 이슈 발생했네? lock 이용해야겠다 라는 사고흐름으로 이어졌다.
즉 기술에 너무 의존한 나머지 다른 케이스를 생각하지 못 한것이다.

기술에 의존하지 말자. 나는 문제를 해결하고자 하는 사람이지 기술을 사용하는 사람이 아니다.

0개의 댓글