아래는 멀티스레드 환경에서 문제가 발생할 수 있는 코드와 그에 대한 테스트 예시입니다. 이번에는 스레드 안전하지 않은 ArrayList를 사용하여 발생할 수 있는 문제를 설명하겠습니다.

1. 문제가 발생할 수 있는 코드

package com.example.chat;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UnsafeChatService {

    // ArrayList는 스레드 안전하지 않습니다.
    private final List<String> chatMessages = new ArrayList<>();

    // 메시지를 추가하는 메서드
    public void addMessage(String message) {
        chatMessages.add(message);
    }

    // 모든 메시지를 반환하는 메서드
    public List<String> getMessages() {
        return new ArrayList<>(chatMessages);
    }
}

2. 테스트 코드 - 스레드 안전하지 않은 코드의 문제 시연

package com.example.chat;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.*;

class UnsafeChatServiceTest {

    private UnsafeChatService unsafeChatService;

    @BeforeEach
    void setUp() {
        unsafeChatService = new UnsafeChatService();
    }

    @Test
    void testConcurrentAddMessage() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        // 100개의 스레드가 동시에 메시지를 추가하는 테스트
        for (int i = 0; i < threadCount; i++) {
            int threadNumber = i;
            executorService.submit(() -> {
                unsafeChatService.addMessage("Message " + threadNumber);
                latch.countDown();
            });
        }

        latch.await();

        // 예상되는 메시지 수는 100개여야 합니다.
        assertEquals(threadCount, unsafeChatService.getMessages().size(),
            "메시지 수가 예상과 다릅니다. 스레드 안전하지 않은 코드로 인해 데이터 손실이 발생할 수 있습니다.");
    }
}

3. 문제 설명

3.1. ArrayList의 스레드 안전성 문제

3.2. 테스트 결과

4. 코드 개선

이 문제를 해결하기 위해서는 스레드 안전한 자료 구조를 사용해야 합니다. 다음은 CopyOnWriteArrayList를 사용한 안전한 코드입니다.

package com.example.chat;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@Service
@RequiredArgsConstructor
public class SafeChatService {

    // CopyOnWriteArrayList는 스레드 안전합니다.
    private final List<String> chatMessages = new CopyOnWriteArrayList<>();

    // 메시지를 추가하는 메서드
    public void addMessage(String message) {
        chatMessages.add(message);
    }

    // 모든 메시지를 반환하는 메서드
    public List<String> getMessages() {
        return new CopyOnWriteArrayList<>(chatMessages);
    }
}

5. 테스트 코드 - 스레드 안전한 코드의 테스트

package com.example.chat;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SafeChatServiceTest {

    private SafeChatService safeChatService;

    @BeforeEach
    void setUp() {
        safeChatService = new SafeChatService();
    }

    @Test
    void testConcurrentAddMessage() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        // 100개의 스레드가 동시에 메시지를 추가하는 테스트
        for (int i = 0; i < threadCount; i++) {
            int threadNumber = i;
            executorService.submit(() -> {
                safeChatService.addMessage("Message " + threadNumber);
                latch.countDown();
            });
        }

        latch.await();

        // 예상되는 메시지 수는 100개여야 합니다.
        assertEquals(threadCount, safeChatService.getMessages().size(),
            "CopyOnWriteArrayList를 사용하면 데이터 손실이 발생하지 않습니다.");
    }
}

6. 결론