개발

Spring Redis 도입에 대한 고찰

물빠진떡 2024. 7. 22. 02:56

서론


마피아 투게더에서 Redis를 도입하였다. 도입 이유는 여기에 기록해 두었다. 이때 2가지를 고려하게 되었다.

  1. 동시성
  2. 조회 쿼리

문제점


1. 동시성

기존 vote의 객체는 다음과 같이 구현되어 있다

public class Vote {

    private final Map<Player, Player> playerVote;
    private Player votedPlayer;
}

이러한 구조라면 동시성 문제가 발생할 것이라고 생각했다.
Redis는 SingleThread의 구조이다.
또한 Redis는 key-value의 구조이다. 이 상태에서 vote를

@RedisHash("vote")
public class Vote {
    @Id
    private String code; // 방 코드
    private final Map<Player, Player> playerVote;
    private Player votedPlayer;
}

와 같이 사용할 경우 같은 방에서 동시에 투표를 하였을때를 고려해보자.
기존 방이 아무도 투표 상황이 없고
p1 -> p2, p2-> p1을 투표한다고 생각하자.
Spring은 멀티스레드이기 때문에 내부에서는 각각이 동시에 진행된다 이때 각각의 요청 상태는

key:code, Map.of(p1->p2, p2->null, p3->null)
key:code, Map.of(p1->null, p2->p1, p3->null)
(이는 그냥 상태를 표현한거지 진짜 명령어이거나 하지 않는다)

이런다면 Redis는 싱글스레드이기 때문에 먼저 들어온 요청을 처리하게 된다
하지만 저장 형태는 RadisHash이고 레디스의 경우 value 내부값에서 특정 필드만 변경이 불가능하다.
때문에 첫번째 요청인

key:code, Map.of(p1->p2, p2->null, p3->null)

이 되도 이후에

key:code, Map.of(p1->null, p2->p1, p3->null)

이 적용되어 후자만 적용되게 된다. 이러면 p1의 요청은 보장되지 않으므로 동시성 문제가 발생한다.
이러한 동시성 문제를 해결하기 위해서는 각 value가 하나의 player, target을 관리하게 바꿔야한다고 생각했다.

@RedisHash("vote")
public class Vote {

    @Id
    private String id;
    private String code;
    private String name;
    private String target;
}

물론 transactional 설정이나 격리 수준을 조절하는 방법등도 있는듯 하다. RedisTemplate에서 enableTransaction을 통해 묶고 나서 vote를 조회하고 save하는 것을 한 트랜섹션으로 묶으면 되지않을까라는 생각도 들었다. 하지만 Redis가 싱글스레드인 상황에서 여러개의 요청이 들어올때 서로 다른 요청이 한 스레드에 묶이면 처리과정에서 어떤 문제가 생길지는 아직 테스트해보지 않았다(dead lock이라던지 뭐 기타 등등). 나중에 해보면 괜춘할듯

2. 조회 쿼리

Redis에 대해 공부하면서 장점인 쿼리가 존재하지 않으며 간단한 명령어로 조회, 저장이 가능하다.
하지만 이 단순함이 문제가 되었다고 생각한다.
왜냐하면 쿼리가 없기 때문에 value를 기반으로 filtering이 되지 않는다.
하지만 좋은 기능이 있다. 바로 키 검색시 조건을 붙일수 있기 때문이다.
예를 들어 key가 code:name의 형태일 경우 code:*의 명령어는 code를 가진 모든 키를 검색하는 명령어가 된다.
이러한 방법을 Secondary Indexes라고 표현한다.

@RedisHash("vote")
public class Vote {

    private String code;
    private String name;
    private String target;

    @Id
    @JsonIgnore
    public String getId() {
        return code + ":" + name;
    }
}

이러한 형태라면 같은 code내에서는 개개인의 vote 정합성을 지키면서 원하는 대로 조회할 수 있다고 생각하게 되었다.

마지막

이게 맞는 방법인지는 잘모르겠다. 하지만 기존 방식은 확실히 문제가 있을것이라고 생각하여 이렇게 설계하게 되었다.
그리고 이 방식에 문제점이 있다면 Sql Injection과 같은 공격에 취약할 것이라는 생각이 든다. name에 Redis와 관련된 명령어나 ':'가 들어가 'code:name'의 id 구조를 해치는 경우라고 생각한다. 이를 대체하기 위해 name을 검증하거나 하는 방식이 필요할 것이다.

참고


Secondary Indexes

'개발' 카테고리의 다른 글

Spring 동시성 테스트  (0) 2024.10.25
Spring Redisson Lock 구현하기, 근데 AOP를 곁들인  (0) 2024.10.21
실시간 통신 리펙토링 SSE 교체  (1) 2024.10.14