Post

동시성을 통한 다중 접속자 관리

동시성을 통한 다중 접속자 관리

동시성을 통한 다중 접속자 관리

동시성(Concurrency)이란 하나의 코어에서 여러 스레드가 번갈아 실행되면서 동시에 동작하는 것처럼 보이게 만드는 개념이다.

실제로는 한 시점에 하나의 스레드만 실행되지만 매우 빠른 전환이 일어나기 때문에 동시에 처리되는 것처럼 보인다.

이 과정에서 스레드 간 전환이 발생하는데 이를 Context Switching이라고 한다.




접속자 수 관리 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Map<String, Integer> connectUsers;

@PostConstruct
private void setUp() {
    this.connectUsers = new HashMap<>();
}

public void connectUser(String status, String roomId, ChatMessage chatMessage) {
    if (Objects.equals(status, "Connect")) {
        connectUsers.putIfAbsent(roomId, 0);
        int num = connectUsers.get(roomId);
        connectUsers.put(roomId, (num + 1));
        saveFile(chatMessage);
    } else if (Objects.equals(status, "Disconnect")) {
        int num = connectUsers.get(roomId);
        connectUsers.put(roomId, (num - 1));
    }
    log.info("현재 인원 : " + connectUsers.get(roomId));
}

여러 사용자가 동시에 채팅방에 접속하거나 종료할 때 접속자 수가 정확하게 증가하거나 감소하지 않는 문제가 발생했다.

원인은 여러 스레드가 동시에 connectUser()를 호출하면서 HashMap을 동시에 수정했기 때문이다.

HashMap은 동기화를 지원하지 않기 때문에 멀티 스레드 환경에서 동시에 수정하면 값이 꼬이거나 예상하지 못한 결과가 발생할 수 있다.

예를 들어 두 사용자가 동시에 접속하면 둘 다 기존 값을 0으로 읽고 각각 1을 저장하여

실제로는 2명이 접속했지만 값은 1이 되는 문제가 발생할 수 있다.




방법 1. ConcurrentHashMap 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Map<String, Integer> connectUsers;

@PostConstruct
private void setUp() {
    this.connectUsers = new ConcurrentHashMap<>();
}

public void connectUser(String status, String roomId, ChatMessage chatMessage) {
    log.info("[ ConnectUser ] roomId : " + roomId);

    if (Objects.equals(status, "Connect")) {
        connectUsers.merge(roomId, 1, Integer::sum);
        saveFile(chatMessage);
    } else if (Objects.equals(status, "Disconnect")) {
        connectUsers.merge(roomId, -1, Integer::sum);
    }

    log.info("현재 인원 : " + connectUsers.get(roomId));
}

Java에서는 ConcurrentHashMap을 사용하여 동시성 문제를 해결할 수 있다.

HashMap은 동기화가 적용되어 있지 않아 멀티 스레드 환경에서 안전하지 않다.

Hashtable은 모든 메소드에 synchronized가 적용되어 있어 Thread-safe 하지만

메소드 단위로 전체를 잠그기 때문에 병목 현상이 발생할 수 있다.

ConcurrentHashMap은 이러한 단점을 보완하기 위해 만들어진 클래스다.


읽기 작업은 여러 스레드가 동시에 수행할 수 있도록 허용하고

쓰기 작업은 일부 버킷에 대해서만 Lock을 적용하여 동시성을 제어한다.

image


image

image

따라서 서로 다른 버킷에 접근하는 경우에는 Lock 경합이 발생하지 않아 성능 저하를 줄일 수 있다.


image

기본적으로 초기 용량과 동시성 수준은 16으로 설정되어 있으며 동시에 처리 가능한 작업 단위를 의미한다.

ConcurrentHashMap은 멀티 스레드 환경에서 안전하면서도 성능을 고려한 자료구조다.




방법 2. synchronized 사용

Java에서는 스레드 동기화를 위해 synchronized 키워드를 제공한다.

스레드는 synchronized 영역에 진입하기 위해 Lock을 획득해야 하며 메소드 또는 블록 실행이 끝나면 Lock을 반환한다.

이미 다른 스레드가 Lock을 가지고 있다면 해당 스레드가 Lock을 반환할 때까지 기다려야 한다.

즉, 한 번에 하나의 스레드만 임계 영역에 접근할 수 있도록 보장하는 방식이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Map<String, Integer> connectUsers;
private final Object lock = new Object();

@PostConstruct
private void setUp() {
    this.connectUsers = new HashMap<>();
}

public void connectUser(String status, String roomId, ChatMessage chatMessage) {
    log.info("[ connectUser ] roomId : " + roomId);

    synchronized (lock) {
        if (Objects.equals(status, "Connect")) {
            connectUsers.putIfAbsent(roomId, 0);
            int num = connectUsers.get(roomId);
            connectUsers.put(roomId, num + 1);
            saveFile(chatMessage);
        } else if (Objects.equals(status, "Disconnect")) {
            int num = connectUsers.get(roomId);
            connectUsers.put(roomId, num - 1);
        }

        log.info("현재 인원 : " + connectUsers.get(roomId));
    }
}

이 방식은 임계 영역 전체를 하나의 Lock으로 보호하기 때문에 동시성 문제는 해결할 수 있었다.

하지만 서로 다른 roomId에 접근하더라도 동일한 Lock을 사용하기 때문에 Blocking이 발생할 수 있다.

사용자 A와 사용자 B가 서로 다른 채팅방에 접속하더라도

한 스레드가 작업을 마칠 때까지 다른 스레드는 대기해야 하므로 성능 저하가 발생할 수 있다.




결론

처음에는 synchronizedConcurrentHashMap을 동시에 적용했다.

하지만 ConcurrentHashMap은 버킷 단위로 Lock을 관리하여 동시성을 최대한 보장하는 구조이기 때문에

여기에 다시 synchronized를 적용하면 전체를 한 번 더 묶는 구조가 되어 장점을 활용하지 못한다고 판단했다.

채팅 서비스 특성상 여러 사용자가 동시에 접속하는 상황이 자주 발생할 것이라고 판단했기 때문에

전체 Blocking이 발생하는 synchronized 방식보다는 부분 Lock을 사용하는 ConcurrentHashMap을 선택했다.






REFERENCE

This post is licensed under CC BY 4.0 by the author.