---
# System prepended metadata

title: 마이 룸 서버 - 동시성 문제 트러블 슈팅

---

# 마이 룸 서버 - 동시성 문제 트러블 슈팅
### 발견하게된 계기
- 하나의 서버로만 개발하다가 Jmeter로 테스트를 해보고 싶어 4명의 유저를 동시에 접속시키고 테스트해봄
- 하나의 서버로, 하나의 계정으로만 테스트할 때는 문제가 없었는데, 비어있는 자료에 접근하거나, 데이터가 불일치하는 문제가 발생
  - 이를 통해 동시성 처리 문제를 인지
### 멀티 스레드 static 인스턴스 접근 동시성 문제
- 서버 내에서 유저 위치, 세션, 방 정보 등을 Map 자료구조를 이용해 저장해놓고 사용합니다.
   - ``` java
     // sessionId : userDetails
     private static final Map<String, UserDetails> userData = new  HashMap<>();
     // sessionId : roomId
     private static final Map<String, String> session_room = new HashMap<>();
     // roomId : room
     private static final Map<String, Room> lobby = new HashMap<>();
        ```
- 여러 유저가 접속한다고 했을 때, 스레드 여러 개가 동시에 Map 자료구조를 참조하고 수정하게 됩니다.
- 이 과정에서 정보가 누락되거나, 이미 사라진 정보를 참조하게 되는 문제가 발생했습니다.
  - ![](https://i.imgur.com/peZtimk.png)
- 따라서 동시성 문제를 해결할 방법을 찾았습니다.
  - Synchronized
  - Concurrent 패키지의 ConcurrentHashMap 자료구조
  - [Synchronized 원리 참고자료](https://qqqqqq.tistory.com/entry/Synchronized%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC)
  - [Concurrent 패키지 참고자료](https://devlog-wjdrbs96.tistory.com/269)
  - 위 자료를 통해 Concurrent 패키지로 HashMap 자료형에 대해 부분적인 락 처리를 활용하는 것이 빠를 것이라는 생각이었습니다.
- 두 방식 중 어떤 것이 더 좋을지 확인해보고자 특정 요청에 대해 Jmeter를 이용해 테스트해봤습니다.
  - 기본 메소드
    - ![](https://i.imgur.com/d69uTMN.png)
  - Synchronized
    - ![](https://i.imgur.com/uX2KRL3.png)
  - ConcurrentHashMap
    - ![](https://i.imgur.com/kR2l5gX.png)
- 위와 같은 결과를 통해 인스턴스에 대한 동기화 처리는 ConcurrentMap 자료구조를 이용하고 메소드 범위에서 동기화 처리는 Synchronized를 이용하기로 했습니다.
### 멀티 서버 Redis 접근 동시성 문제
- 서버 내에서 멀티스레드에 대해 동기화처리를 하고 나서 여러 서버가 동시에 Redis에 접근할 때, 동시성 오류가 난다는 것을 발견했습니다.
  - Queue형태로 명령을 받아서 처리하여 문제가 없다고 생각했지만, 조회 시점, 업데이트 시점을 
- 이후 조언을 얻어 상태관리 서버와 소켓 연결을 해 Redis에 대한 동기화 처리를 해줄 수 있다는 것을 알게 됐습니다.
- 하지만 시간이 부족하다고 판단하여 다른 방법을 찾았고, 그 중 Redisson을 이용한 분산 락 처리에 대해 알게 됐습니다.
  - [Redisson 참고자료](https://kkambi.tistory.com/196)
- 이후 동기화가 진행됐지만, 여러 데이터를 변경하는 작업이 트랜잭션으로 묶이지 않아서 따로 `redisTemplate.execute()`를 이용해 한번에 명령을 수행하도록 했습니다.
  - ``` java
    RLock roomLock = redissonClient.getLock(PREFIX_OF_LOCK + roomId);
    try {
        roomLock.lockInterruptibly(EXPIRE_TIME_OF_LOCK, TIME_UNIT);
        if(Boolean.TRUE.equals(redisTemplate.opsForHash().hasKey(mainKey, hashKey + "nickname"))) {
            redisTemplate.execute(new SessionCallback<Object>() {
                @Override
                public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                    operations.multi();
                    HashOperations<K, String, Object> hash = operations.opsForHash();
                        
                    // 트랜잭션 코드
                        
                    operations.exec();
                    return null;
                }
            });
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        roomLock.unlock();
    }
      ```
