Skip to content

Commit 030a901

Browse files
authored
Merge pull request #6 from chhs2131/SpringMVC
ConcurrenyExample 내용 추가
2 parents 9c31631 + 48458c7 commit 030a901

File tree

18 files changed

+1119
-0
lines changed

18 files changed

+1119
-0
lines changed

concurrency-example/.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/

concurrency-example/README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# Concurrency Example
2+
keyword: 암시적 락, 명시적 락, 낙관적 락, 비관적 락, Synchronized, volatile, ReentrantLock, StampedLock, Atomic, CAS Algorithm, Future, ExecutorService, completableFuture, ForkJoinPool, ConcurrentHashMap, Semaphore
3+
4+
<br/>
5+
6+
java에서 동시성을 제어하기 위한 3개의 예시를 다룹니다.
7+
8+
<br/>
9+
<br/>
10+
11+
## 간단한 Counter 예제
12+
increase 호출시 count 값을 1씩 증가시키는 Counter 클래스를 동시에 접근하는 방법에 대해 다룹니다.
13+
14+
Counter Class
15+
```java
16+
public class Counter {
17+
private static final int DELAY_TIME = 50;
18+
private int count;
19+
20+
public int increase() {
21+
delay(DELAY_TIME);
22+
return count++;
23+
}
24+
}
25+
```
26+
27+
실패하는 경우
28+
```java
29+
@Test
30+
void originalTest_Fail() throws Exception {
31+
int loop = 100;
32+
executorService = Executors.newFixedThreadPool(loop);
33+
34+
for (int i = 0; i < loop; i++) {
35+
executorService.execute(() -> counter.increase());
36+
}
37+
executorService.awaitTermination(TIME_OUT, TimeUnit.SECONDS);
38+
39+
Assertions.assertThat(counter.getCount()).isNotEqualTo(loop);
40+
}
41+
```
42+
43+
성공하는 경우 (Synchronized 사용)
44+
```java
45+
@Test
46+
void synchronizedTest_Success() throws Exception {
47+
int loop = 100;
48+
executorService = Executors.newFixedThreadPool(loop);
49+
50+
for (int i = 0; i < loop; i++) {
51+
executorService.execute(() -> synchronizedIncrease());
52+
}
53+
executorService.awaitTermination(TIME_OUT, TimeUnit.SECONDS);
54+
55+
Assertions.assertThat(counter.getCount()).isEqualTo(loop);
56+
}
57+
58+
// or use this method
59+
synchronized void synchronizedIncrease() {
60+
counter.increase();
61+
}
62+
```
63+
64+
## 선착순 이벤트 예제
65+
선착순 n명에게 엄청난 선물을 주는 예제입니다. 동시성 이슈로 중복 당첨자가 발생하면 손해가 막심하겠네요.
66+
그리고 분명 당첨되었다고 나왔는데, 전산오류여서 취소됬다고 하면 기분이 정말 안좋겠습니다.
67+
68+
Prizes Class - 선착순으로 당첨된 사람들을 기록합니다.
69+
```java
70+
public class Prizes {
71+
private final List<String> winners;
72+
private int maxWinners;
73+
74+
public Prizes(List<String> winners) {
75+
this.winners = winners;
76+
}
77+
78+
public void init(int maxWinners) {
79+
Assert.isTrue(maxWinners >= 0, "최대 당첨 인원은 음수가 될 수 없습니다.");
80+
this.maxWinners = maxWinners;
81+
}
82+
}
83+
```
84+
85+
실패하는 경우 - 반드시 서버(Prizes Class)에 기록된 당첨자와, 당첨됬다고 통지받은 클라이언트 리스트가 둘다 정상인지 확인해봐야합니다. Prizes Class는 동시성이슈로 덮어쓰기 되었을 수 있습니다.
86+
```java
87+
@Test
88+
void 천명이동시에도전_그중10명이당첨_실패() throws Exception {
89+
final int maxWinners = 10;
90+
final int threadCount = 1000;
91+
executorService = Executors.newFixedThreadPool(threadCount);
92+
prizes.init(maxWinners);
93+
final List<String> winners = new ArrayList<>();
94+
95+
for (int i = 0; i < threadCount; i++) {
96+
final int num = i;
97+
executorService.execute(() -> {
98+
if (tryToWin(num)) winners.add(String.valueOf(num));
99+
});
100+
}
101+
executorService.awaitTermination(TIME_OUT, TimeUnit.SECONDS);
102+
103+
Assertions.assertThat(prizes.getCountWinners()).isEqualTo(maxWinners);
104+
Assertions.assertThat(winners.size()).isNotEqualTo(maxWinners);
105+
}
106+
```
107+
108+
성공하는 경우 (ReentrantLock 사용)
109+
```java
110+
@Test
111+
void 천명이동시에도전_그중10명이당첨_reentrantLock_성공() throws Exception {
112+
final Lock lock = new ReentrantLock();
113+
final int maxWinners = 10;
114+
final int threadCount = 1000;
115+
executorService = Executors.newFixedThreadPool(threadCount);
116+
prizes.init(maxWinners);
117+
final List<String> winners = new ArrayList<>();
118+
119+
for (int i = 0; i < threadCount; i++) {
120+
final int num = i;
121+
executorService.execute(() -> {
122+
lock.lock();
123+
try {
124+
if (tryToWin(num))
125+
winners.add(String.valueOf(num));
126+
} finally {
127+
lock.unlock();
128+
}
129+
});
130+
}
131+
executorService.awaitTermination(TIME_OUT, TimeUnit.SECONDS);
132+
133+
Assertions.assertThat(prizes.getCountWinners()).isEqualTo(maxWinners);
134+
Assertions.assertThat(winners).hasSize(maxWinners);
135+
}
136+
```
137+
138+
## 게시글 조회수 (with JPA)
139+
JPA가 관리하는 Entity도 자바 락을 사용할 수 있을까요?
140+
141+
### 테스트 코드
142+
아래 테스트코드가 성공하길 기대합니다.
143+
144+
```java
145+
@SpringBootTest
146+
class BoardServiceTest {
147+
@Autowired
148+
private BoardService boardService;
149+
private static final int TIME_OUT = 5;
150+
151+
@Test
152+
void 백명이동시에조회_예상조회수100_성공() throws Exception {
153+
final Long boardId = 1L;
154+
final int threadCount = 100;
155+
ExecutorService service = Executors.newFixedThreadPool(threadCount);
156+
게시글생성요청_생성();
157+
158+
for (int i = 0; i < threadCount; i++) {
159+
service.execute(() -> boardService.getBoard(boardId));
160+
}
161+
service.awaitTermination(10, TimeUnit.SECONDS);
162+
final Board board = boardService.getBoard(boardId);
163+
164+
Assertions.assertThat(board.getCount()).isEqualTo(101);
165+
}
166+
167+
private void 게시글생성요청_생성() {
168+
final BoardRequest request = new BoardRequest("테스트게시글", "테스트내용123");
169+
boardService.write(request);
170+
}
171+
}
172+
```
173+
174+
### 실패하는 경우
175+
트랜잭션으로 관리되는 객체(Entity)의 상태가 전부(Java변수, Persistence 객체, 외부 DB) 동일하게 관리되야 때문에, JVM LEVEL에서만 부여한 Lock은 효과를 보지 못합니다.
176+
177+
```java
178+
@GetMapping("/{id}")
179+
@Transactional
180+
public Board getBoard(@PathVariable("id") final Long id) {
181+
final Board board = boardRepository.findById(id)
182+
.orElseThrow(() -> new IllegalArgumentException("id에 해당하는 게시글이 없습니다."));
183+
184+
lock.lock();
185+
try {
186+
board.increaseViewCount();
187+
boardRepository.save(board);
188+
} finally {
189+
lock.unlock();
190+
}
191+
192+
return board;
193+
}
194+
```
195+
196+
197+
### 성공하는 경우
198+
select for update
199+
```java
200+
@Repository
201+
public interface BoardRepository extends JpaRepository<Board, Long> {
202+
@Lock(LockModeType.PESSIMISTIC_WRITE)
203+
Optional<Board> findById(Long id);
204+
}
205+
```
206+
207+
### 상태가 변경되는 경우를 예외로 처리하기
208+
Entity에 Version을 부여해서 최신버전이 아닌경우 예외를 발생시키는 구조입니다. version은 JPA에 의해서 관리되며 상태변화에 맞춰 증가하게 됩니다.
209+
210+
```java
211+
212+
@Entity
213+
@Table(name = "boards")
214+
@Getter
215+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
216+
public class Board {
217+
@Id
218+
@GeneratedValue(strategy = GenerationType.IDENTITY)
219+
private Long id;
220+
private String title;
221+
private String content;
222+
private int count;
223+
224+
@Version
225+
private int version;
226+
}
227+
```
228+
229+

concurrency-example/build.gradle

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '3.2.3'
4+
id 'io.spring.dependency-management' version '1.1.4'
5+
}
6+
7+
group = 'org.example'
8+
version = '0.0.1-SNAPSHOT'
9+
10+
java {
11+
sourceCompatibility = '17'
12+
}
13+
14+
configurations {
15+
compileOnly {
16+
extendsFrom annotationProcessor
17+
}
18+
}
19+
20+
repositories {
21+
mavenCentral()
22+
}
23+
24+
dependencies {
25+
implementation 'org.springframework.boot:spring-boot-starter-web'
26+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
27+
runtimeOnly 'com.h2database:h2'
28+
29+
compileOnly 'org.projectlombok:lombok'
30+
annotationProcessor 'org.projectlombok:lombok'
31+
32+
implementation("org.springframework.boot:spring-boot-starter-validation")
33+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
34+
}
35+
36+
tasks.named('test') {
37+
useJUnitPlatform()
38+
}

0 commit comments

Comments
 (0)