- 글의 순서
- 동시성 처리 이슈 상황
- 자바 동시성 처리 기초 정보 및 동시성 처리 테스트 코드 작성 방안
- java.lang.Thread & java.lang.Runnable
- ExecutorFramework
- Thread VS ExecutorFramework
- CountDownLatch
- 처리방안
- 트랜잭션 격리수준
- Uncommitted Read
- Committed Read
- Repeatable Read
- Serializable
- 데이터베이스 수준에서의 처리 방안
- 락
- 비관적 락
- 락관적 락
- 어플리케이션 수준에서의 처리 방안
- 왜 어플리케이션에서 처리할 수 있어야 하는가?
- syncronized
- ConcurrentHashMap
- 레디스 - 분산락
- 스핀락
- 메시지 브로커
안녕하세요! 린내입니다.
이번 시간에는 동시성 요청 이슈처리에 대해 알아보겠습니다.
먼저 문제가 되는 상황부터 가정해볼까요?
아래와 같은 포인트 엔티티가 있습니다.
기본적인 id 필드가 있고, 잔여 포인트를 Long으로 나타내는 points 필드가 있습니다.
@Entity
@Getter
@ToString
@NoArgsConstructor
public class Point {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
private Long points;
public Point(Long points) {
this.points = points;
}
public void substractPoint(Long point){
points -= point;
}
}
substractPoint() 메소드로 잔여 포인트에 특정 포인트를 차감할 수 있습니다.
- 간단한 컨트롤러와 서비스를 구현해봅니다.
@RestController
@RequiredArgsConstructor
public class PointController {
private final PointService pointService;
@GetMapping("/points/{id}")
public Point getPoints(@PathVariable Long id){
return pointService.findPost(id);
}
@PostMapping("/points/{id}/{point}")
public Point subtractPoints(@PathVariable Long id, @PathVariable Long point){
return pointService.subtractPoint(id, point);
}
}
@Service
@RequiredArgsConstructor
public class PointService {
private final PointRepository pointRepository;
public Point findPost(Long id) {
return pointRepository.findById(id).orElseThrow();
}
@Transactional
public Point subtractPoint(Long id, Long point){
Point post = findPost(id);
if(post.getPoints() - point >= 0){ //차감결과가 0 이상인 경우
post.substractPoint(point);
}
return post;
}
}
- 빠르게 테스트 코드를 작성해봅니다.
- 먼저 단건 차감 요청의 경우를 볼까요?
@SpringBootTest
@AutoConfigureMockMvc
class PointControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
PointRepository pointRepository;
@Test
public void 포인트_차감_단건_요청() throws Exception {
Point point = pointRepository.save(new Point(10000L));
mockMvc.perform(MockMvcRequestBuilders.post("/points/{id}/{point}", point.id, 1000L)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)) //성공
.andExpect(MockMvcResultMatchers.jsonPath("$.points").value(9000L)); //성공
}
}
- 10,000포인트를 입력하고 1,000 포인트 차감을 요청한 결과가 9,000 포인트인 것을 확인하였습니다.
그럼 이제 본격적으로 동시성 요청 시 이슈 상황에 대해 알아보겠습니다.
우리가 입출금 모임통장을 사용하듯이
위의 포인트 엔티티가 공유될 수 포인트라서
여러 사람이 포인트를 꺼내서 쓸 수 있으면 어떻게 될까요?
그것도 동시에 꺼내서 쓰게 되면 일어날 수 있는 이슈는 무엇이 있을까요?
또한, 이러한 상황을 테스트하기 위해서는 일반적인 요청으로 테스트는 불가능합니다.
동시 요청을 테스트하기 위해 병렬 스레드로 요청을 생성한 뒤 동시에 요청을 보내야하는데요
이를 위해 병렬 스레드로 요청을 보내기 위해 필요한 기초 지식들과 그 예시코드를 먼저 알아보겠습니다.
Thread VS ExecutorFramework
- 자바에서 병렬 스레드로 작업을 요청하기 위한 방법에는 두가지가 있습니다.
- 스레드 객체(java.lang.Thread)를 이용하는 방식과 스레드풀 (Executor Framework)을 이용하는 방식이 있습니다.
Thread
java.lang.Thread 객체는 JVM 내부에서 스레드를 생성하고 관리하는 기능을 제공합니다.
java.lang.Runnable 인터페이스를 구현하여 run()을 실행하는 주체이며, 이 객체를 이용하여 병렬로 작업을 처리할 수도 있습니다.
Thread 객체의 사용 방식은 두가지 방법이 있는데,
첫 번째 방법은 Thead 객체를 상속받는 방법이고, 두 번째 방법은 Runnable 인터페이스를 직접 구현하는 방법입니다.
public class SubstractRequestThread extends Thread{ // 첫 번째 -> Thread 상속
//..
@Override //Runnable의 run() 구현 메소드를 재정의
public void run() {
super.run();
}
}
public class SubstractRequest implements Runnable{ // 두 번째 -> Runnble 구현
@Override
public void run() { // 직접 구현
//..
}
}
@Test
public void 스레드_객체_테스트(){
SubstractRequestThread thread1 = new SubstractRequestThread();
thread1.start(); // 스레드1 병렬 실행
SubstractRequest substractRequest = new SubstractRequest();
Thread thread2 = new Thread(substractRequest);
thread2.start();; // 스레드2 병렬 실행
}
Executor Framework
jdk 1.5에 추가된 Executor Framework는 자바 어플리케이션에 스레드풀 기능을 이용할 수 있게 해줍니다.
위에서 알아보았던 Thread 객체의 직접 생성 및 관리는 비교적 성능을 요하는 작업이었습니다.
따라서, Executor Framework 가 제공해주는 스레드풀을 이용하는 것이 스레드 생성과 관리를 추상화하고, 오버헤드를 최소화할 수 있는 좋은 방법입니다.
cf) 스레드 생성과 관리를 추상화한다??
Executor Framework를 사용하여 스레드를 스레드풀로 관리하게 됨
→ 개발자가 스레드의 생성과 관리에 대한 코드 직접 작성하지 않음
또한 이미 생성된 객체를 작업에 재사용하여 더 효율적인 처리가 가능해짐
java.util.concurrent.Excutor는 Executor Framework 에서 핵심이 되는 인터페이스입니다.
기존 Thread객체에선 작업(task)과 실행 (execution)이 분리되어 있지 않았지만
Executor를 사용하여 작업과 실행을 분리시킬 수 있게 되었습니다.
또한 Executor를 확장한 ExecutorService는 구현된 스레드풀까지 제공하여 개발자가 더 많은 기능을 사용할 수 있게 도와줍니다.
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 작업을 즉시 시작 (실행 == 작업)
}
static class MyRunnable implements Runnable {
@Override
public void run() { // 실행 시 즉시 해당 작업을 호출
System.out.println("This is executed in a separate thread.");
}
}
}
public class ExecutorExample {
public static void main(String[] args) {
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(new MyRunnable()); // 작업을 실행과 분리하기 위한 execute()
// 스레드풀과 적용되면 실행 != 작업 가능해짐
// 스레드풀 생성
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new MyRunnable()); // 작업을 스레드풀에 제출하여 실행
executorService.shutdown(); // 작업이 완료되면 ExecutorService 종료 (확장기능제공)
}
static class MyRunnable implements Runnable {
@Override
public void run() { // 특정 시점에 작업을 호출
System.out.println("This is executed via an Executor.");
}
}
}
자, 이번 시간에는 이렇게 동시성 이슈가 되는 상황을 가정하고
테스트하기 위한 준비 작업으로써 자바 동시성 처리에 대한 지식을 알아보았습니다!
다음 시간엔 동시성 처리 테스트 코드를 작성하고 실제 처리 방안에 대해서까지 알아보겠습니다!!
출처:
https://tecoble.techcourse.co.kr/post/2023-08-16-concurrency-managing/
https://www.baeldung.com/java-single-thread-executor-service
https://www.linkedin.com/pulse/difference-between-executor-executorservice-executors-omar-ismail/
'백엔드 > Java' 카테고리의 다른 글
동시성 이슈 처리 및 테스트 방안 3 - 트랜잭션 격리수준 (2) | 2024.04.11 |
---|---|
동시성 이슈 처리 및 테스트 방안 - 2(CountDownLatch 활용) (6) | 2024.03.25 |
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 2 (5) | 2024.02.13 |
천사와 악마 사이 추상클래스... - 인터페이스 및 정적 유틸리티 클래스 비교 - 1 (0) | 2024.02.01 |
이넘아 놀자 4 - 인터페이스 사용하여 Enum 공통화하기 (0) | 2023.09.03 |