아래는 멀티스레드 환경에서 문제가 발생할 수 있는 코드와 그에 대한 테스트 예시입니다. 이번에는 스레드 안전하지 않은 ArrayList
를 사용하여 발생할 수 있는 문제를 설명하겠습니다.
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);
}
}
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(),
"메시지 수가 예상과 다릅니다. 스레드 안전하지 않은 코드로 인해 데이터 손실이 발생할 수 있습니다.");
}
}
UnsafeChatService
클래스에서 chatMessages
는 ArrayList
로 구현되어 있습니다. ArrayList
는 스레드 안전하지 않은 자료 구조로, 여러 스레드가 동시에 접근하여 데이터를 추가하거나 수정할 경우, 내부 데이터 구조에 손상이 발생할 수 있습니다.addMessage
메서드를 호출하여 ArrayList
에 메시지를 추가하려고 합니다. 그러나 ArrayList
는 스레드 안전하지 않기 때문에, 일부 메시지가 제대로 추가되지 않거나, 내부 데이터 구조가 손상될 수 있습니다.assertEquals(threadCount, unsafeChatService.getMessages().size())
를 통해 ArrayList
에 추가된 메시지의 개수가 100개인지 확인합니다.이 문제를 해결하기 위해서는 스레드 안전한 자료 구조를 사용해야 합니다. 다음은 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);
}
}
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를 사용하면 데이터 손실이 발생하지 않습니다.");
}
}