Skip to content

좋아요 기능 개선(비동기, 복합 인덱스)

이민수 edited this page Jan 21, 2025 · 4 revisions

👀 비동기 처리로 안정성 개선

게시글 좋아요 기능 개선중 개선해야 할 점을 발견 했는데, 좋아요를 누르는 비즈니스 로직은 하나의 트랜잭션에서 중복체크를 위한 조회 쿼리 그리고 좋아요를 누르는 삽입, 리로드 없이 좋아요 수를 확인 할 수 있는 게시글별 좋아요 조회 쿼리로 구성되어 있으며,

또한 좋아요를 누르면 게시글을 등록한 이용자가 실시간으로 알림을 받을 수 있게 SSE를 활용한 네트워크 통신까지 하나의 트랜잭션으로 구성 되어 있엇음.

그런데 만약 알림을 보내는 통신중에 문제가 발생 할 시 MySQL서버에서도 트랜잭션이 commit 되지않고 MySQL서버 까지 위험해지는 상황이 발생 할 수 있으며, 트랜잭션 경랑화를 위해 두개의 로직은 비동기 처리 해야 한다고 판단이 됌

📖 setting environment

MySQL 프로시저를 활용해 1000명의 유저, 10000개의 게시글, 50000개의 좋아요의 더미 데이터를 추가한 뒤 실습

⚡ 비동기 처리

Spring Boot Application 클래스에 @EnableAsync와 비동기 처리를 할 메서드에 @Async 애너테이션을 붙이면 간단하게 비동기 처리를 할 수 있는데, 그럴시 좋아요를 누를때 마다 늘 스레드가 생성이 되고 어느 순간 OOM이 발생할 가능성을 염두해서

Thread pool을 활용하기로 결정,

@Configuration
@EnableAsync
public class SpringAsyncConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(3);
        threadPoolTaskExecutor.setMaxPoolSize(30);
        threadPoolTaskExecutor.setQueueCapacity(100);
        threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
        return threadPoolTaskExecutor;
    }
}

Thread pool을 활용하는 방법으로 설정파일과 @Bean 애너테이션으로 빈을 등록해서 사용해 간단하게 구현 할 수 있엇는데,

쓰레드 풀 사이즈를 총 100개 활용 하게 했으며, 만약 쓰레드 풀에 남아 있는 쓰레드가 없을시 최대 30개의 쓰레드를 생성해

OOM문제도 줄이고 상황에 따라 쉽게 유지보수 할 수 있도록 선택함.

이렇게 설정 파일을 이용해 빈을 등록할시 비동기 처리를 원하는 메서드에

@Async("threadPoolTaskExecutor") 

이렇게 선언식으로 쓰레드 풀을 사용하는 비동기 처리가 가능해 유지 보수 등 장점이 있다고 생각

@Async("threadPoolTaskExecutor")
    public void sendLikeNotification(Long postId,Post post, User user){
        try{
            LikeAlarmDTO likeAlarmDTO = LikeAlarmDTO.builder()
                    .postId(postId)
                    .createdAt(LocalDateTime.now())
                    .boardId(post.getBoard().getId())
                    .build();
            alarmService.customAlarm(
                    post.getUser().getId(),
                    likeAlarmDTO,
                    "작성하신 글에 좋아요가 달렸습니다",
                    "like"
            );
        }catch (Exception e){
            log.error("Failed to send like notification for postId {} and userId {}", postId, user.getId(), e);
        }
    }

비동기 처리를 원하는 네트워크 관련 메서드를 따로 빼서 리팩터링

💌 결과

동기 처리시

복합 인덱스 x 동기처리 비즈니스 로직 걸린시간

비동기 처리시

복합 인덱스 x 비동기 처리 o

실행 환경마다 다르기도 하며, context switching 등 문제로 동기 처리와 비동기 처리의 실행 시간은 크게 차이가 나지 않으나

트랜잭션 경랑화 및 네트워크 통신시 문제점이 생길때 DBMS서버까지 영향을 받을 수 있어 비동기 처리가 적절하다고 생각.

⚡ 복합 인덱스 설정

현 프로젝트에서 좋아요를 누르는 로직은 총 3개의 쿼리가 포함 되어 있는 트랜잭션을 사용하는데

1, 중복 검사 쿼리

한명의 유저가 하나의 게시글에 좋아요를 여러번 누르면 안되기 때문에 중복 검사를 위한 쿼리

select id from likes where post_id = ? and user_id = ?

2, 좋아요 삽입 쿼리

insert into likes (post_id,user_id) values(?,?)

3, 좋아요를 누른 뒤 좋아요 수를 확인 할 수 있는 쿼리

select count(post_id) from likes where post_id = ? 

이렇게 insert 쿼리가 트랜잭션에 포함이 되나 조회 쿼리가 2개 이므로 복합 인덱스를 활용해 최적화가 가능하지 않을까 생각이 듬

MySQLFK Key 에 대하여 자동적으로 보조 인덱스를 생성 하지만

좋아요 중복검사하는 쿼리는

select id from likes where post_id = ? and user_id = ?

이렇게 2개의 FK를 이용해야 하므로 복합 인덱스를 활용 할 시 효율이 더 좋지 않을까 생각이 들어 복합 인덱스를 사용 하기로 결정

ALTER TABLE likes ADD INDEX idx_post_user (post_id, user_id);

현 프로젝트에선 post_id 속성을 이용해 조회하는 쿼리가 많아 post_id를 Prefix로 설정해두고 user_id만으로 likes테이블 을 검색 해야 하는 로직이 없기도 하며, 추후 user_idlikes테이블을 검색 해야할 상황이 MySQL의 보조인덱스로 활용 할 수 있기에 Postfix로 설계

🔢 결과

복합인덱스 걸고 중복확인 쿼리

실행 계획을 살펴 보니 중복 확인 쿼리 및 좋아요 수 조회 쿼리가 생성해둔 인덱스를 활용 하였고

복합 인덱스 사용 전

복합 인덱스 x 비동기 처리 o

복합 인덱스 사용 후

복합 인덱스 o 비동기 처리

약간의 성능 향상은 있엇지만 크게 차이가 나지 않는다.

아무래도 INSERT문이 있었으므로 크게 차이나지 않는다고 판단이 되고 또한 FK키는 자동적으로 보조인덱스를 생성하기 때문

실행계획을 보니

복합 인덱스 걸기전 중복 확인 쿼리

두개의 보조인덱스를 결합해 사용하므로 속도 차이가 크게 나지 않으나

2개의 인덱스를 결합해 사용할시 2개의 인덱스를 검색한뒤 교집합을 찾는 Index Merge 현상이 생기고 데이터 양이 많아질수록

성능이 저하 될 점을 고려해 복합인덱스 사용으로 결정 또한 복합 인덱스를 사용 하지 않아도 post_id 컬럼엔 자동으로 보조 인덱스가 생성되고

복합 인덱스를 선언 할시 post_id컬럼 보조인덱스는 자동으로 삭제되므로 INSERT문에서도 비용 차이가 크게 나지 않을 것으로 판단

결론

복합 인덱스 x 동기처리 비즈니스 로직 걸린시간

복합 인덱스 o 비동기 처리

비동기 처리와 복합 인덱스 설정으로 약간의 속도 개선과 트랜잭션 경랑화 및 트랜잭션 안정성을 보장 할 수 있게 됌