프로젝트

중복된 채팅방을 찾는 코드

미뿌감 2024. 8. 4. 19:29
728x90

1번의 경우 아래와 같이 코드를 작성했다.

    @Transactional
    public boolean isCreatedChat(Long userId, Long tutorId) {
        User user = findByIdAndCheckPresent(userId, false);
        List<ChatParticipant> chatParticipants = user.getChatParticipants();

        for (ChatParticipant chatParticipant : chatParticipants) {
            List<ChatParticipant> roomParticipants = chatParticipantRepository.findByChatRoomId(chatParticipant.getChatRoom().getId());
            for (ChatParticipant roomParticipant : roomParticipants) {
                if (roomParticipant.getIsTutor() && Objects.equals(roomParticipant.getUser().getUserId(), tutorId)) {
                    return true;
                }
            }
        }
        return false;
    }

 

3번의 경우 효율 측면에서 더 좋다고 생각이 들지만, 하드코딩의 문제 때문에 적용하지 못하였다.

view 폴더에 chat_room_participants_view 라는 테이블을 만들고, 

해당 테이블을 중복된 채팅방이 있는지 찾을 때마다 update 해준다면, 하드 코딩 문제를 해결할 수 있지 않을까 하여 우선 sql 문을 작성해 보았다.

 

view란 무엇인가?

하나 이상의 기존 테이블로 부터 생성된 '가상 테이블'을 의미한다.

저장 장치 내, 물리적으로 존재하지 않지만, 사용자에게 있는 것처럼 간주된다고 한다.

또한 읽기 전용으로 사용되어 보안에 도움이 된다.

 

해당 경우 하나 이상의 기존 테이블로 부터 생성된 가상 테이블이 맞긴 하지만,

저장 장치 내 물리적으로 존재하는 것이 아닌가? 라는 의구심이 들었다.

 

그래서 알아보니, sql문에서 VIEW 문으로 테이블을 생성하게 되면 Mysql의 가상의 테이블로 물리적으로 데이터가 저장되지 않으며 데이터 조회 시마다 쿼리가 실행되어 최신 데이터를 반환한다.

 

실제로 mysql을 까서 확인한 결과는 다음과 같다.

show full tables;를 하게 되면, Table_type 또한 확인해 볼 수 있는데, chat_room_participants_view의 경우 type이 view 임을 확인할 수 있다.

 

view를 이용한 sql문은 아래와 같다. VIEW를 사용한 것을 확인할 수 있다.

CREATE OR REPLACE VIEW chat_room_participants_view AS
SELECT
    cr.id AS chat_room_id,
    CONCAT(
        COALESCE(
            GROUP_CONCAT(CASE WHEN cp.is_tutor = TRUE THEN u.user_id END ORDER BY u.user_id SEPARATOR '&'),
            ''
        ),
        CASE 
            WHEN COUNT(CASE WHEN cp.is_tutor = TRUE THEN 1 END) > 0 
                AND COUNT(CASE WHEN cp.is_tutor = FALSE THEN 1 END) > 0 
            THEN '-' 
            ELSE '' 
        END,
        COALESCE(
            GROUP_CONCAT(CASE WHEN cp.is_tutor = FALSE THEN u.user_id END ORDER BY u.user_id SEPARATOR '&'),
            ''
        )
    ) AS participants
FROM
    chat_room cr
JOIN
    chat_participant cp ON cr.id = cp.chat_room_id
JOIN
    users u ON cp.user_id = u.user_id
GROUP BY
    cr.id;

 

이 코드를 실행하게 되면 views라는 폴더에 table이 생긴 것을 확인할 수 있다.

 

 

그렇다면 이제, 코드는 어떤 식으로 작성하였는지 알아보자.

 

우선 파일은 3개로 구성된다.

  1. ChatRoomViewRepsitory - interface
  2. ChatRoomViewRepositoryCustom - interface
  3. ChatRoomViewRepositoryCustomImpl - class

ChatRoomViewRepository의 경우엔 기존의 repository와 같이 선언해주면 된다. 

package org.aper.web.domain.chat.repository;

import org.aper.web.domain.chat.entity.ChatRoomView;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ChatRoomViewRepository extends JpaRepository<ChatRoomView, Long>, ChatRoomViewRepositoryCustom {
    List<ChatRoomView> findByParticipants(String participants);
}

 

여기서 interface ChatRoomViewRepositoryCustom을 상속 받아주면 된다. (인터페이스는 인터페이스만 상속 받아줄 수 있다.)

 

chatRoomViewRepositoryCustom 인터페이스는 함수 updateChatRoomParticipantsView(); 를 선언해준다. 

package org.aper.web.domain.chat.repository;

public interface ChatRoomViewRepositoryCustom {
    void updateChatRoomParticipantsView();
}

 

이제 chatRoomViewRepositoryCustom 인터페이스를 실제 구현해주는 class를 작성해 준다.

 

package org.aper.web.domain.chat.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class ChatRoomViewRepositoryCustomImpl implements ChatRoomViewRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public void updateChatRoomParticipantsView() {
        String sql = "CREATE OR REPLACE VIEW chat_room_participants_view AS " +
                "SELECT " +
                "    cr.id AS chat_room_id, " +
                "    CONCAT( " +
                "        COALESCE( " +
                "            GROUP_CONCAT(CASE WHEN cp.is_tutor = TRUE THEN u.user_id END ORDER BY u.user_id SEPARATOR '&'), " +
                "            '' " +
                "        ), " +
                "        CASE " +
                "            WHEN COUNT(CASE WHEN cp.is_tutor = TRUE THEN 1 END) > 0 " +
                "                AND COUNT(CASE WHEN cp.is_tutor = FALSE THEN 1 END) > 0 " +
                "            THEN '-' " +
                "            ELSE '' " +
                "        END, " +
                "        COALESCE( " +
                "            GROUP_CONCAT(CASE WHEN cp.is_tutor = FALSE THEN u.user_id END ORDER BY u.user_id SEPARATOR '&'), " +
                "            '' " +
                "        ) " +
                "    ) AS participants " +
                "FROM " +
                "    chat_room cr " +
                "JOIN " +
                "    chat_participant cp ON cr.id = cp.chat_room_id " +
                "JOIN " +
                "    users u ON cp.user_id = u.user_id " +
                "GROUP BY " +
                "    cr.id;";

        entityManager.createNativeQuery(sql).executeUpdate();
    }
}

 

@Override를 통해서 인터페이스에 선언되어 있던 함수를 구현해 준다.

 

sql문의 내용의 경우 다음과 같다.

  • chatRoomId에 따라 @Id를 구성하게 된다.
  • participants라는 string을 통해서 태그와 같이 생성하게 된다.
  • 태그의 앞에는 tutor의 아이디가 뒤에는 학생의 아이디가 오며 -로 구분하게 된다.
  • 만약 학생이 한명이 아닐 경우엔 오름차순으로 정렬되며 &로 구분하게 된다.

현재는 1대1 대화만을 목적으로 하고 있지만, 기능 확장의 가능성을 고려하여 위와 같이 sql문을 생성하게 되었다.

위와 같이 테이블이 생성되게 된다.

( 원래 1-2가 하나만 있어야 하지만, 중복성 검사 코드를 작성하기 전에 여러 번 채팅 생성 요청을 보내서 여러 개다.)

 

해당 코드에서 아래 부분이 생소해서 더 자세히 알아보았다. 

@PersistenceContext
private EntityManager entityManager;

@PersistenceContext 어노테이션은 entityManager를 주입하는 데 사용된다.

주입된 entityManager를 통해 데이터베이스에 생성된 sql문을 실행시킬 수 있게 된다.

 

마지막으로, 생성된 테이블의 entity를 만들어 주어, 해당 repository에서 findByParticipants를 할 수 있도록 하였다.

package org.aper.web.domain.chat.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;

@Entity
@Table(name = "chat_room_participants_view")
@Getter
@org.hibernate.annotations.Immutable
public class ChatRoomView {

    @Id
    private Long chatRoomId;
    private String participants;

    public ChatRoomView() {

    }
}

 

읽기만 가능하도록 하기 위해 아래 어노테이션을 추가해 주었다.

@org.hibernate.annotations.Immutable

 

이제 중복된 채팅방이 있는지 확인하는 코드를 확인해 보도록 하자.

@Transactional
public boolean isCreatedChat(Long userId, Long tutorId) {
    String tag = tutorId + "-" + userId;
    viewRepository.updateChatRoomParticipantsView();
    List<ChatRoomView> existingChatRoom = viewRepository.findByParticipants(tag);

    return !existingChatRoom.isEmpty();
}

 

이와 같이 구성해 주었으며, 확실히 1번으로 코드를 구성하였을 때보다 더 명료해진 것을 확인할 수 있다.

728x90