-
Couldn't load subscription status.
- Fork 10
[22기_서가영] 동시성 & 결제 연동 미션 제출합니다. #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: caminobelllo
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다!!
결제 부분이랑 로깅 부분이 자세하게 구현되어있어 제 코드와 비교해보며 많이 배웠습니다!
|
|
||
| - 멀티 스레드로 동시성을 만족시킬 수 있는 것이지 동시성과 멀티 스레드는 연관이 없다. 반례로 코틀린은 싱글스레드에서 코루틴을 이용하여 동시성을 만족할 수 있다. | ||
|
|
||
| - 코루틴(Coroutine): 싱글 스레드에서도 루틴(routine) 이라는 단위(맥락상 함수와 동일)로 루틴간 협력이 가능하며, 동시성 프로그래밍을 지원하고 비동기 처리를 쉽게 도와주는 개념을 말한다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 동시성 조사할 때 못봤던 내용인데 신기하네요! 배우고 갑니당
| } catch (DataIntegrityViolationException e) { | ||
| throw new IllegalStateException("좌석을 예매할 수 없습니다.", e); | ||
| } | ||
| Member member = memberRepository.findByEmail(email) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 member를 id가 아닌 email로 찾는 이유가 따로 있을까요??
| .map(ls -> ls.getSeat().getRowNo() + "-" + ls.getSeat().getColumnNo()) | ||
| .toList()) | ||
| .build(); | ||
| List<String> seatLabels = lines.stream() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 코드는 다른 비즈니스 로직에서도 많이 사용할 수 있을 거 같아요! entity단에 구현해서 재사용하는 방식은 어떨까싶은데, 가영님은 어떻게 생각하시는지 궁금합니다!
| public ResponseEntity<String> signup(@RequestBody SignUpRequestDto requestDto) { | ||
| memberService.signup(requestDto); | ||
| return ResponseEntity.status(HttpStatus.CREATED).body("회원가입이 성공적으로 완료되었습니다."); | ||
| public CustomResponse<?> signup(@RequestBody SignUpRequestDto request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string에서 ?로 바뀐 이유가 궁금합니다!
|
|
||
| try { | ||
| bookingSeatRepository.saveAll(lines); | ||
| } catch (DataIntegrityViolationException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
커스텀에러를 많이 활용하신 거 같아요! 배우고 갑니다~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 배우고 갑니다...
| paymentService.approvePayment(bookingNum, paymentRequest); | ||
| log.info("Payment 성공. bookingNum(paymentId): {}", bookingNum); | ||
|
|
||
| // 결제 성공 시 DB 저장 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 결제는 성공했으나 DB 저장이 실패했을 경우의 롤백 로직도 담겨있나요??
제가 알기로는 @transactional 만으로는 외부 api 호출까지 롤백해주지 않는 걸로 알고있어서요!
https://stackoverflow.com/questions/48814651/transaction-management-of-jpa-and-external-api-calls?utm_source=chatgpt.com
해당 스택오버플로우를 한 번 확인하시는 것도 좋을 거 같아요!!
| boolean mismatch = seats.stream().anyMatch(s -> !Objects.equals( | ||
| s.getAuditorium().getId(), screeningAuditoriumId)); | ||
| if (mismatch) throw new CustomException(ErrorCode.SEAT_IN_AUDITORIUM_NOT_FOUND); | ||
| log.info("락 획득 성공. user={}, screeningId={}, seatIds={}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
락을 획득한 후에, db나 캐시단에서 좌석을 잠궈두는 코드가 보이지않는데, 혹시 따로 이유가 있을까요??
제가 알기로는 락을 획득한 직후에 좌석 상태를 예약 중 또는 잠금 상태로 변경해서 다른 요청에서 예약 가능 여부가 바로 반영되도록 하는 걸로 알고있어서요! 가영님은 어떻게 생각하시는지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고 많으셨습니다!!!
분산 락, 로깅, CustomException 어떻게 적용하고 활용하시는지 잘 봤습니다!! 배워갑니당
| 동시성 제어 방법 | ||
| 1. 암시적 lock (synchronized) | ||
| - 문제가 되는 메서드/변수에 각각 synchronized 키워드를 넣는다. | ||
| ``` | ||
| class Count { | ||
| private int count; | ||
| public synchronized int view() {return count++;} | ||
| } | ||
| class Count { | ||
| private Integer count = 0; | ||
| public int view() { | ||
| synchronized (this.count) { | ||
| return count++; | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| - 배타적 실행 | ||
| - 한 스레드가 객체를 변경하는 중이라 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는다. | ||
| - 일관된 상태를 갖는 객체에 접근하는 메서드가 해당 객체에 lock을 걸어, 객체의 상태를 확인하고 수정한다. | ||
| - synchronized로 설정된 메서드 혹은 코드 블럭에 lock이 생긴다. | ||
| - 그 사이에 일관성이 깨지는 순간을 다른 어떤 메서드도 확인할 수 없다. | ||
| - 스레드간 통신 | ||
| - 동기화된 메서드나 블록에 들어간 Thread가 같은 lock의 보호 하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다. | ||
| - 같은 임계 영역을 갖는 스레드에서 일관된 값을 보게 한다. | ||
| 2. 명시적 lock | ||
| - synchronized 키워드 없이 명시적으로 ReentrantLock을 사용하는 방법 | ||
| - synchronized 보다 세밀하게 락을 제어 가능하다. (Lock polling 지원 / 타임아웃 지정 가능 / condition을 적용해 대기 중인 스레드를 선별적으로 깨울 수 있음 / lock 획득을 위해 wating pool에 있는 스레드에게 인터럽트를 걸 수 있음) | ||
| - 가장 중요한 것은 CPU cache와 메인 메모리 간의 동기화를 명시적으로 제어할 수 있게 된다는 것이다. | ||
| - 해당 락의 범위를 메서드 내부에서 한정하기 어렵거나, 동시에 여러 락을 사용해야 할 때 쓴다. | ||
| - 직접적으로 Lock 객체를 생성해 사용한다. | ||
| ``` | ||
| public class CountingTest { | ||
| public static void main(String[] args) { | ||
| Count count = new Count(); | ||
| for (int i = 0; i < 100; i++) { | ||
| new Thread(){ | ||
| public void run(){ | ||
| for (int j = 0; j < 1000; j++) { | ||
| count.getLock().lock(); | ||
| System.out.println(count.view()); | ||
| count.getLock().unlock(); | ||
| } | ||
| } | ||
| }.start(); | ||
| } | ||
| } | ||
| } | ||
| class Count { | ||
| private int count = 0; | ||
| private Lock lock = new ReentrantLock(); | ||
| public int view() { | ||
| return count++; | ||
| } | ||
| public Lock getLock(){ | ||
| return lock; | ||
| }; | ||
| } | ||
| ``` | ||
| 3. 낙관적 lock | ||
| - 앞서 나온 방법들은 동시성 문제를 나름대로 해결하지만, 다중화된 서버 환경에서는 보장할 수 없다는 단점이 있다. 동시성 제어를 위한 Lock 방법은 장치 내의 메모리 혹은 Lock을 통해 동기화 돼있기 때문에 이를 해결하기 위해서는 모든 서버의 동시성을 다룰 외부 시스템(DataBase, Redis 등)이 필요해진다. | ||
| - 그래서 낙관적 락은 자원에 lock을 걸지 않고, 동시성 문제가 발생하면 그때 처리하는 방식이다. | ||
| - 즉, DB의 Lock을 사용하지 않고, 애플리케이션 레벨에서 Entity의 버전을 관리하면서 변경을 감지하는 방법이다. | ||
| - 하지만 경우에 따라 데드락에 걸릴 수 있기에 유의해야 하며, 애플리케이션 레벨에서 처리하는 특징으로 인해 재시도 로직을 별도 생성해야 한다. | ||
| - 낙관적 락은 경합이 많고 충돌이 많을 수록 트랜잭션을 중단할 가능성이 매우 크고, 롤백은 테이블 행과 레코드를 모두 포함할 수 있는 현재의 보류 중인 변경 사항을 모두 되돌려야 하므로 DB 시스템 비용이 많이 들 수 있다. | ||
| 4. 비관적 lock | ||
| - 동시성 문제가 발생하기 전에 자원의 접근을 막아버리는 방식이다. | ||
| - 즉, 동시성 문제가 발생할 것이라 가정하고 자원에 대한 접근을 막고 시작하는 것이다. 따라서 Transaction이 시작할 때 shared lock 또는 exclusive lock을 걸어버린다. | ||
| 1. shared lock (공유 락) : read lock이라고도 부르며 데이터를 읽을 때는 같은 shared lock끼리의 접근을 허용하지만, write 작업은 막는다. | ||
| 2. exclusive lock : write lock이라고도 부르며, 트랜잭션이 완료될 때까지 유지되면서 exclusive lock이 끝나기 전까지 read/write 작업을 모두 막는다. | ||
| - 특정 데이터에 배타적 락(Exclusive Lock)을 걸어, 하나의 처리가 완료되기 전까진 해당 데이터의 읽기, 수정, 삭제를 방지하기 때문에 동시성 문제 측면에서 매우 안전하다. | ||
| - 하지만 모든 작업을 순차적으로 처리하기 때문에 속도가 매우 저하되고, 특정 데이터의 조회까지 막기 때문에 전혀 상관없는 기능에서조차 병목 현상이 발생할 수 있다는 단점이 존재한다. | ||
| - 충돌이 많이 발생하는 환경에서는 낙관적 락보다는 비관적 락이 더 적합할 것이라 생각한다. | ||
| 5. 분산 락 | ||
| <img width="923" height="492" alt="image" src="https://github.com/user-attachments/assets/975a6011-a656-49ec-aa40-b7df24624a45" /> | ||
| - 낙관적 락은 데이터의 쓰기 작업은 별로 없지만, 읽기 작업이 많아 동시 접근 성능이 중요할 때 많이 쓰이고, 비관적 락은 충돌이 많이 발생하더라도 데이터의 무결성을 지킬 수 있다는 장점을 갖는다. | ||
| - 하지만 충돌은 막으면서, DB의 부담도 줄이고 읽기 조회의 병목 현상도 줄일 수 있는 방법을 생각했을 때 락의 위치를 옮기는 것이 가장 쉽게 떠오르는 방법이다. DB보다 훨씬 접근이 빠른 Redis를 사용하면서, 특정 작업에서만 동시성이 관리되도록 처리하는 방법이다. | ||
| 하지만 분산 락 개발 시에는 주의가 필요하다. | ||
| - 만약에 분산 락을 SpinLock 방식으로 구현했다고 하면 (-> busy waiting), 반드시 "락이 존재하는 지 확인한다"와 "존재하지 않으면 락을 획득한다"라는 두 연산이 atomic하게 이루어져야 하고 try 구문 안에서 Lock 획득에 성공할 때까지 무한 루프를 실행해야 한다. | ||
| 이 방식에는 문제점이 있다. | ||
| 1. lock timeout | ||
| - 어떤 애플리케이션에서 tryLock에 성공해서 자원 접근을 막았는데 종료되어버리면, 다른 모든 애플리케이션도 영원히 Lock을 얻지 못하는 Dead Lock 현상이 발생한다. | ||
| 2. tryLock은 try-finally 밖에서 수행해야 한다. | ||
| - try 문에서 시도 횟수 초과에 대한 예외를 발생시키면, Lock을 얻지도 못 한 Thread에서 Lock을 해제시킬 수 있게 된다. 따라서 try-finally 밖에서 Lock 획득 시도 횟수 초과 예외를 처리해주어야 한다. | ||
| 3. Redis 부하 | ||
| - SpinLock 방식은 Redis에 엄청난 부담을 줄 수밖에 없다. Critical Section 내에서 수행할 작업 속도가 느릴 수록 빈번히 수행되는 기능일 수록 문제는 더 심해진다. | ||
| 일반적으로 많이 사용하는 Lettuce는 spin lock 기법으로 락을 획득하는 방식이다. 그럼 위와 같은 문제가 발생할 수 있기에 Redisson에 대해 찾아보았다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
동시성 처리 방법을 상세하게 적어주셔서 이해가 잘 됩니다!! 배워갑니당
|
|
||
| try { | ||
| bookingSeatRepository.saveAll(lines); | ||
| } catch (DataIntegrityViolationException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 배우고 갑니다...
| email, order.getId(), cinema.getId()); | ||
|
|
||
| // 응답 DTO | ||
| return ProductOrderResponseDto.builder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로깅 꼼꼼하게 하셨네욤 배워갑니다!!!
좌석 예매 테스트입니다.