02. 100명의 사용자가 동시에 요청을 하면 서버는 올바르게 처리하는가?
OS | Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836) |
Edit Tool | IntelliJ IDEA 2019.1 |
FrameWork | Spring Boot |
DB | H2 |
ORM | JPA |
Test Tool | JMeter |
시나리오
1. 한 사이트에 동시 접속자가 100명이라 가정한다.
2. 영화 좌석을 예매하기 위해 100명의 사용자가 한 곳의 자리를 동시에 예매한다고 가정한다.
(밀리초 까지 동일시간으로 맞추려면 어렵기 때문에 JMeter 를 이용하여 비슷하게 맞춰 테스트를 진행)
코드
SeatRestController 클래스
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import com.example.demo3.seat.application.SeatService;
import com.example.demo3.seat.domain.Seat;
import com.example.demo3.seat.domain.SeatRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
@RestController
@RequestMapping("/seats")
public class SeatRestController {
@Autowired
private SeatRepository seatRepository;
@Autowired
private SeatService seatService;
@PostMapping()
public ResponseEntity<?> create(@RequestBody Seat resources) throws URISyntaxException {
System.out.println(resources.toString());
Seat seat = seatRepository.save(seatService.save(Seat.builder()
.xPos(resources.getXpos())
.yPos(resources.getYpos())
.build()
));
URI location = new URI("/seats/" + seat.getId());
return ResponseEntity.created(location).body(seat);
}
@GetMapping()
public List<Seat> list() {
return seatRepository.findAll();
}
}
|
cs |
Seat 클래스
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
26
27
28
29
|
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"xpos", "ypos"})
})
public class Seat {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long xpos;
private Long ypos;
@Builder
public Seat(Long xPos, Long yPos) {
this.xpos = xPos;
this.ypos = yPos;
}
}
|
cs |
실험 1
[첫 번째 실험 결과]
ID 가 1이여야 하는데 3으로 비정상적으로 들어간 모습을 볼 수 있다.
실험 2
[두 번째 실험 결과]
정상적으로 1이 들어간 모습
실험 3
[세 번째 실험 결과]
ID가 2로 비정상적으로 들어간 모습
실험 4
위 실험 1,2,3 은 서버를 재시작하지 않고 값만 바꿔서
JMeter 로 다시 서버에 Post 로 보낼경우
id 값은 100이 증가된 101 부터 시작이 된다.
똑같이 또 보내면 다음 생성되는 ID 값은 201 이상이 된다.
이유는 DB에 값이 있는지 체크를 하지않고 seatRepository.save() 메소드를 실행했기 때문이다
Seat 클래스 private Long id 부분에
@GeneratedValue(strategy = GenerationType.IDENTITY) 로 주었기 때문에
DB 의 Inert 오류가 발생하기도 전에 같은시간에서의 AUTO_INCREMENT 처리가 빨라
id 값이 증가 된 후에 오류가 발생한다
이를 막기위해서는 값 중복체크를 한 후 save() 메소드를 사용하여야 한다.
Contoller 에서 하는 일이 아니고 코드가 길어지기때문에
Service 를 추가하여 코드를 분리시킨다.
SeatRestController 클래스 (수정)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import com.example.demo3.seat.application.SeatService;
import com.example.demo3.seat.domain.Seat;
import com.example.demo3.seat.domain.SeatRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
@RestController
@RequestMapping("/seats")
public class SeatRestController {
@Autowired
private SeatRepository seatRepository;
@Autowired
private SeatService seatService;
@PostMapping()
public ResponseEntity<?> create(@RequestBody Seat resources) throws URISyntaxException {
System.out.println(resources.toString());
Seat seat = seatService.save(resources);
URI location = new URI("/seats/" + seat.getId());
return ResponseEntity.created(location).body(seat);
}
@GetMapping()
public List<Seat> list() {
return seatRepository.findAll();
}
}
|
cs |
SeatService 클래스
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
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import com.example.demo3.seat.domain.Seat;
import com.example.demo3.seat.domain.SeatRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SeatService {
private SeatRepository seatRepository;
@Autowired
public SeatService(SeatRepository seatRepository) {
this.seatRepository = seatRepository;
}
public Seat save(Seat resources) {
Seat tempSeat = seatRepository.findByXposAndYpos(resources.getXpos(), resources.getYpos()).orElse(null);
if (tempSeat != null) {
System.out.println(tempSeat.toString());
if (tempSeat.getXpos().equals(resources.getXpos()) && tempSeat.getYpos().equals(resources.getYpos())) {
throw new IllegalArgumentException("값 중복");
}
}
Seat seat = Seat.builder()
.xPos(resources.getXpos())
.yPos(resources.getYpos())
.build();
return seatRepository.save(seat);
}
}
|
cs |
실험 5
중복체크를 했음에도 불구하고, 실험4 에서는 ID값이 증가되어있다.
이번 이유는 DB 에 Insert 문이 들어가기도 전에
사용자들이 DB를 조회하여
tempSeat 객체안에는 null (DB에 값이 존재하지않음)이 들어가게 된다.
예를들어 사용자 1 ~ 사용자 50 까지 null 로 들어간 경우
seatRepository.save() 메소드가 실행 할 수 있게되어
아까와 같은 문제가 발생되는 것이다.
이렇게 증가되는 원인을 본다면 위 그림처럼 생각된다 (필자생각)
멀티 쓰레드 환경을 위해 SeatService 클래스 코드를 수정한다
SeatService 클래스 (수정)
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
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import com.example.demo3.seat.domain.Seat;
import com.example.demo3.seat.domain.SeatRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SeatService {
private SeatRepository seatRepository;
@Autowired
public SeatService(SeatRepository seatRepository) {
this.seatRepository = seatRepository;
}
synchronized public Seat save(Seat resources) {
Seat tempSeat = seatRepository.findByXposAndYpos(resources.getXpos(), resources.getYpos()).orElse(null);
if (tempSeat != null) {
System.out.println(tempSeat.toString());
if (tempSeat.getXpos().equals(resources.getXpos()) && tempSeat.getYpos().equals(resources.getYpos())) {
throw new IllegalArgumentException("값 중복");
}
}
Seat seat = Seat.builder()
.xPos(resources.getXpos())
.yPos(resources.getYpos())
.build();
return seatRepository.save(seat);
}
}
|
cs |
10명의 사용자로 요청했더니 DB 조회 결과 모두 null 로 나왔다
적용 후에는 1명의 사용자만이 null 로 나왔으며
나머지 9명의 사용자들에게는 DB 에서 조회 된 값을 반환했다.
결론
실제로 사용자들이 밀리초 까지 똑같게 클릭 확률은 적지만,
가끔 11시 특가 할인 상품 100개 판매 같은 이벤트 할 때
지금 시나리오에서는 100명으로 설정 했지만 1000명 ~ 2000명 이상이 클릭 한다면,
이런 문제가 야기될 수 있다고 본다.
가장 최악에 상황을 가정한다면, DB에서 unique 설정을 하지 않았고
조회를 통해 중복검사를 하였다면,
insert 로 들어갈 때 10명의 사용자가 전부 같은 좌석을 예매하게 된다고 볼 수 있다.
참고
면접에 갔을 때 6년차 된 개발자도 synchronized 의 동기 비동기의 이론만 알고있고
실제로는 어떻게 사용하는지 모르는 사람을 본 적 있었다
'10. Spring > 실험' 카테고리의 다른 글
??. Spring 5 (스프링 5) - Dto 클래스는 Service 계층에 넘기는 것이 맞는가? (0) | 2021.08.20 |
---|---|
03. 스프링 부트 (Spring Boot 2) - JPA Test 시 유의사항 @Where (0) | 2021.05.30 |
01. 크롬 개발자 도구를 이용하여 Ajax 회원가입이 가능한가? (0) | 2020.07.07 |
댓글
이 글 공유하기
다른 글
-
??. Spring 5 (스프링 5) - Dto 클래스는 Service 계층에 넘기는 것이 맞는가?
??. Spring 5 (스프링 5) - Dto 클래스는 Service 계층에 넘기는 것이 맞는가?
2021.08.20 -
03. 스프링 부트 (Spring Boot 2) - JPA Test 시 유의사항 @Where
03. 스프링 부트 (Spring Boot 2) - JPA Test 시 유의사항 @Where
2021.05.30 -
01. 크롬 개발자 도구를 이용하여 Ajax 회원가입이 가능한가?
01. 크롬 개발자 도구를 이용하여 Ajax 회원가입이 가능한가?
2020.07.07