Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- mco
- 백준
- 현금흐름표
- 주식
- 주린이
- 오버라이딩
- XLF
- 기업분석
- 다형성
- 제태크
- 자바
- javascript
- 무디스
- S&P500
- object
- 접근제어자
- FCF
- 잉여현금흐름
- 금리인하
- 그리디 알고리즘
- StringBuffer
- 객체지향
- Java
- 미국주식
- 인플레이션
- 금리인상
- 프로그래머스
- etf
- 알고리즘
- 배당성장
Archives
- Today
- Total
오늘의하루
[Spring] 부하 테스트에서 발견한 회원가입 로직 이상 동작에 대한 고찰 본문
문제 상황
부하 테스트를 진행하면서 100명의 사용자가 동시에 같은 아이디로 회원가입을 시도했습니다. 이때 회원가입 로직은 아이디의 중복 여부를 확인하고, 중복된 경우에는 회원가입을 거부해야 합니다. 하지만 신기하게도, 동시에 여러 명의 사용자가 같은 아이디로 회원가입에 성공했습니다.
또한, 처음 회원가입에 성공한 사용자는 삽입(insert) 쿼리가 실행되었지만, 그 외의 사용자들은 업데이트(update) 쿼리가 실행되었습니다. 이러한 이상 동작은 예상과는 매우 다르게 나타났습니다.
문제 로직
Controller -> Service -> DB
@Override
public synchronized void joinProcess(JoinDTO joinDTO) {
Assert.hasText(joinDTO.getUsername(), "아이디는 필수 입력 사항입니다.");
Assert.hasText(joinDTO.getPassword(), "비밀번호는 필수 입력 사항입니다.");
Assert.hasText(joinDTO.getAccessUrl(), "접근 URL은 필수 선택 사항입니다.");
boolean check = userEntityRepository.existsById_UsernameAndId_AccessUrl(joinDTO.getUsername(), joinDTO.getAccessUrl());
Assert.isTrue(!check, "중복된 아이디가 존재합니다.");
UserEntity userEntity = UserEntity.builder()
.id(UserEntityId.builder().username(joinDTO.getUsername()).accessUrl(joinDTO.getAccessUrl()).build())
.password(bCryptPasswordEncoder.encode(joinDTO.getPassword())).role("ROLE_AUSER").apprv(false).build();
userEntityRepository.save(userEntity);
}
원인분석
문제의 원인은 동시성 문제로 추정됩니다.
동시에 여러 사용자가 같은 아이디로 회원가입을 시도할 때, 회원가입 로직은 아이디의 중복 여부를 확인하고 중복된 경우에는 회원가입을 거부해야 합니다. 하지만 동시에 여러 사용자가 동시에 같은 아이디로 회원가입을 시도하면, 중복 확인 과정에서 충돌이 발생하여 중복된 아이디로의 회원가입이 허용될 수 있습니다.
또한, 동시성 문제로 인해 처음 회원가입에 성공한 사용자는 삽입(insert) 쿼리가 실행되었지만, 다른 사용자들은 이미 중복 확인이 완료되어 업데이트(update) 쿼리가 실행될 수 있습니다.
해결 방법
이러한 문제를 해결하기 위해서는 다음과 같은 방법을 고려할 수 있습니다.
- Transactional 격리 수준 설정 [REPEATABLE_READ, SERIALIZABLE]
- SERIALIZABLE(직렬화)
- 트랜젝션 격리 수준을 Serializable로 설정하여 동시에 여러 트랜젝션이 실행되는 것을 막고 순차적으로 트랜젝션이 실행되도록 만들어서 문제를 해결할 수 있습니다.
- Serializable은 읽기와 쓰기 모두를 잠금 설정하기 때문에 다른 트랜젝션은 해당 데이터를 동시에 읽거나 수정할 수 없습니다.
- 주의할 점은 Serializable을 설정하면 트랜젝션끼리의 경합이 높아져 Deadlock이 발생할 가능성이 높습니다.
- REPEATABLE_READ
- 트랜젝션이 시작될 때 읽은 데이터에 대한 일관성을 보장할 수 있습니다. 이 말은 한 트랜젝션에서 조회한 데이터는 해당 트랜잭션이 종료될 때까지 변경되지 않습니다.
- 주의할 점은 Phantom Read 문제가 발생할 수 있는데 이는 동일한 쿼리를 두 번 실행했을 때 두 번째 실행에서 새로운 데이터가 나타나는 경우입니다. 이런 문제가 발생하는 이유는 트랜젝션이 시작될 때 읽은 데이터에 대한 일관성은 보장하지만 트랜젝션이 실행되는 동안 새로운 행이 추가, 삭제, 수정되는 것은 허용하기 때문입니다.
- 이 격리 수준은 Mysql의 기본 격리 수준으로 지정되어 있습니다.
- SERIALIZABLE(직렬화)
- Method 동기화 처리
- 동기화(ex : synchronized)를 통해 Method를 동기화 처리하여 스레드가 순차적으로 접근하도록 만들어서 문제를 해결할 수 있습니다.
'Spring' 카테고리의 다른 글
[Spring] Proxy 내부 호출 문제 해결 및 순환 참조 문제 해결 (1) | 2024.08.28 |
---|---|
[Spring] Thread Local 사용 시 주의 사항 (0) | 2024.08.03 |
Spring 서비스 추상화와 단일 책임 원칙 (0) | 2023.10.16 |
Spring boot 오류 페이지 (0) | 2023.08.30 |
Servlet의 예외 처리하는 방법 (0) | 2023.08.30 |
Comments