묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI, async, await)
jinja2templates
수업대로 127.0.0.1:8000/items/{id} 를 넣었는데starlette.routing.NoMatchFound: No route exists for name "static" and params "path".internal Server Error 가 나옵니다 ㅠBASE_DIR, directory 모두 수업대로 다 입력했습니다. 구글링을 해도 방법을 찾질 못해서요.
-
해결됨재고시스템으로 알아보는 동시성이슈 해결방법
Optimistic Lock 버전 관리 질문
버전 관리를 통하여 동시성을 피할수 있다고 하셨는데, 서로 다른 서버에서 동시에 version = 1 인 데이터룰 얻고, 동시에 stock = stock + 1, version = 2 로 업데이트 치면 optimistic lock 또한 동시성이 발생할 수 있는 것 아닌가요? 강의에서는 version = 1 인 데이터를 동시에 얻지만 업데이트는 순차적으로 하는 표만 보여주셔서 헷갈려요. 순차적으로 할 수 밖에 없는 메커니즘이라면 그 부분을 설명해주셨으면 좋겠습니다.
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
동기화와 CPU 관계 불일치 흐름도
무릎을 탁치는 명강의였습니다. 감사합니다.
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
예제 transfer 메서드
안녕하세요, 강사님. 강의 40분에 나오는 예제의 transfer메서드에서 호출하는 withdraw, deposit 메소드는 이미 synchronized 블록 동기화가 되어있는데 왜 또 호출전에 synchronized 블록 동기화 구문을 작성하는지 궁금합니다. 감사합니다.
-
미해결재고시스템으로 알아보는 동시성이슈 해결방법
낙관적 락 무한루프 질문
강사님의 강의를 보며 현재 제 프로젝트에 낙관적 락을 적용시켜 보았습니다.구현하고자 하는 서비스는 쿠폰 발급 서비스이며, DB 구조는user 1 : N user_coupon N : 1 coupon 입니다. @Transactional public void issueCoupon(CouponIssueParam param, User user) { // 쿠폰 조회 Coupon coupon = getCoupon(param.getCouponId()); // 쿠폰 발급 UserCoupon userCoupon = UserCoupon.CreateUserCoupon(coupon, user); userCouponQueryService.saveUserCoupon(userCoupon); }public static UserCoupon CreateUserCoupon(Coupon coupon, User user) { // 쿠폰 검증 coupon.validateCoupon(); // 재고 감소 coupon.decreaseQuantity(); return UserCoupon.builder() .coupon(coupon) .user(user) .build(); }위 코드와 같이 유저가 특정 쿠폰 Id 를 통해 쿠폰 발급 요청을 하고, 중간 테이블에 관계가 매핑됨으로써 쿠폰 발급이 이루어집니다. public void validateCoupon() { if(this.stockStatus.equals(StockStatus.OUT_OF_STOCK)) throw new IllegalArgumentException("쿠폰이 매진되었습니다."); if(this.expiredAt.isBefore(LocalDateTime.now())) throw new IllegalArgumentException("쿠폰이 만료되었습니다."); }이때 user_coupon 생성 전 쿠폰의 매진 및 만료 상태에 따라 예외를 던지는 검증 메서드가 존재합니다. public void decreaseQuantity() { this.remainQuantity = this.remainQuantity - 1; if(this.remainQuantity <= 0){ this.stockStatus = StockStatus.OUT_OF_STOCK; } }추가로 쿠폰 내부에 재고가 감소하는 메서드가 존재하며 0에 다다를 경우 상태값을 변경해줍니다. 이때 동시성 이슈가 발생하는 이유는 user_coupon 을 insert 하면서, 부모 테이블인 coupon 의 재고를 update 하는 과정에서 발생하는 것으로 파악했습니다. 이에 쿠폰 발급 초기에 coupon 을 조회할 때 @Lock(LockModeType.OPTIMISTIC) @Query("select c from Coupon c where c.id = :couponId and c.isDeleted = false") Optional<Coupon> findOneCouponByCouponId(@Param("couponId") Long couponId);이처럼 낙관적 락 어노테이션을 달아주었으며 @Component @RequiredArgsConstructor public class OptimisticLockFacade { private final CouponService couponService; public void issueCoupon(CouponIssueParam param, User user) throws InterruptedException { while (true) { try { couponService.issueCoupon(param, user); break; } catch (Exception e) { Thread.sleep(50); // 재시도 전 잠시 대기 } } } }위와 같이 퍼사드 클래스를 생성하였습니다. @Test @DisplayName("쿠폰 여러 명 발급") void 쿠폰_여러_명_발급() throws InterruptedException { int threadCount = 1000; ExecutorService executorService = Executors.newFixedThreadPool(32); CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadNumber = i + 1; int key = i; executorService.submit(() -> { try { optimisticLockFacade.issueCoupon(param, users.get(key)); System.out.println("Thread " + threadNumber + " - 성공"); } catch (PessimisticLockingFailureException e) { System.out.println("Thread " + threadNumber + " - 락 충돌 감지"); } catch (Exception e) { System.out.println("Thread " + threadNumber + " - " + e.getMessage()); } finally { latch.countDown(); } }); } latch.await(); executorService.shutdown(); Long count = userCouponRepository.countByCouponId(param.getCouponId()); assertThat(count).isEqualTo(100); } 쿠폰의 재고가 100개며, 1,000 명의 유저가 32개의 스레드 환경에서 쿠폰 발급을 요청할 때 예상되는 발급 쿠폰 수는 100개가 되어야 합니다. 이렇게 낙관적 락을 적용하여 테스트를 수행하니 무한 루프에 빠지게 되었습니다.무한루프로 인해 테스트가 종료되지 않자, 강제적으로 정지 시킨 후 DB 를 확인했는데 쿠폰의 수는 예상한 대로 100개가 생성된 것을 확인할 수 있었습니다. 그런데 왜 메서드가 종료되지 않고 무한루프가 돌아간 것인지 이유를 모르겠습니다. public void issueCoupon(CouponIssueParam param, User user) throws InterruptedException { while (true) { try { couponService.issueCoupon(param, user); break; } catch (IllegalArgumentException e) { System.out.println("쿠폰 발급 실패: " + e.getMessage()); break; } catch (Exception e) { Thread.sleep(50); // 재시도 전 잠시 대기 } } } 혹시나 하여, 쿠폰 발급 로직에 쿠폰이 매진이 될 경우 예외를 던지는 검증 메서드가 존재했고 이에 퍼사드 클래스에서 해당 예외를 잡아내면 루프를 빠져나오게 설정했습니다. 이렇게 구현하니 테스트는 성공적으로 통과하였습니다. 현재 제가 구현한 서비스 구성에서는 이런식으로 접근하는 게 맞는 걸까요 ?
-
미해결운영체제 공룡책 강의
강의 ppt 제공
강의 ppt는 어디서 다운 받을 수 있나요?
-
미해결파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI, async, await)
몽고DB 설정 관련 질문
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. FastAPI + MongoDB : MongoDB ODM 셋업 강의를 듣고 있는데 궁금한 점이 생겨서요!섹션4에서 몽고DB 데이터베이스 설정할 때 "nest"로 프로젝트명을 설정하여 해당 url도 받고 했었는데 섹션5에서는 프로젝트명도 바뀌고 url도 바껴서요. 제가 "nest" 설정했던 것처럼 새로 "fastapi-pj" 프로젝트 만들어서 url 받고 secret.json에 넣으면 될까요?
-
미해결재고시스템으로 알아보는 동시성이슈 해결방법
중간테이블에 대한 낙관적 락 적용법
현재 Member 테이블과 Appointment 테이블이 존재하는데, N:N 관계이기 때문에 아래와 같이 AppointmentUser라는 중간 테이블이 존재합니다.@Entity @Table(name = "appointment_and_user") public class AppointmentUser extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "apppointment_id", nullable = false) private Appointment appointment; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; @Enumerated(value = EnumType.STRING) @Column(name = "member_authority", nullable = false) private MemberAuthority memberAuthority; @Version private Long version;하나의 AppointmentUser 테이블은 약속과 멤버의 id를 하나씩 가집니다. 여기서 레포지토리의 코드는 이러합니다.public interface AppointmentUserRepository extends JpaRepository<AppointmentUser, Long> { ... @Lock(LockModeType.OPTIMISTIC) @Query("select au from AppointmentUser au where au.id = :id") AppointmentUser findByIdWithOptimisticLock(Long id);findByIdWithOptimistic 메서드를 통하여 특정 유저-약속 테이블의 데이터를 통해 AppointmentUser 객체를 반환합니다. 서비스 계층의 코드는 아래와 같습니다.@Transactional public void updateAuthority(Long appointmentId, Long loginMemberId, Long targetMemberId) { Member loginMember = memberRepository.getById(loginMemberId); Member targetMember = memberRepository.getById(targetMemberId); Appointment appointment = appointmentRepository.getById(appointmentId); AppointmentUser loginAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(loginMember, appointment); AppointmentUser targetAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(targetMember, appointment); appointment.changeTitle("asd"); validateIsAdminMember(loginAppointmentUser.getId()); MemberAuthority targetAuthority = targetAppointmentUser.getMemberAuthority(); targetAppointmentUser.updateAuthority(MemberAuthority.getAnotherAuthority(targetAuthority)); appointmentRepository.save(appointment); appointmentUserRepository.save(targetAppointmentUser); } private void validateIsAdminMember(Long loginAppointmentUserId) { if (appointmentRepository.findByIdWithOptimisticLock(loginAppointmentUserId).getMemberAuthority() != MemberAuthority.ADMIN) { throw new NotAdminMemberException(); } } <로직 설명>하나의 약속 내에 멤버 두명이 존재각 멤버들은 ADMIN or NORMAL 권한을 갖고 있음.두명 다 약속 내 에서 ADMIN 권한을 갖고 있다는 상황을 가정. (ADMIN은 AppointmentUser 엔티티의 MemberAuthority 라는 필드의 Enum 값입니다.)두명이 서로를 동시에 ADMIN에서 NORMAL로 권한을 박탈하는 경우, 하나의 약속 안에 ADMIN인 사람이 없어지는 예외적인 문제 상황이 발생함.그래서 @Version을 AppointmentUser 엔티티의 필드로 등록하여 해결하려고 했으나..사용자 A와 B가 있다고 할 때 service 코드 내의 validateIsAdminMember 메서드를 통해 상대방의 권한을 박탈하려는 유저(본인)가 ADMIN인지 검증하여 ADMIN이 맞다면 박탈하고, ADMIN이 아니라면, 예외를 던지게 끔 하는 로직에서,레포지토리 내의 findByIdWithOptimisticLock 메서드를 동시에 접근했을때 Version 필드를 통해 동시성 문제를 제어할 수 있다고 생각했으나..validateIsAdminMember로 검증하는 A와 B의 AppointmentUser 엔티티는 서로 다른 객체(데이터)이기 때문에 서로 다른 테이블의 Version 값을 변경하기 때문에 동시성 보장이 안됨..그래서 Appointment에 Version필드를 넣어주려 했지만, Version값은 해당 테이블에 변화가 생겨야 변한다.하지만 로직상, AppointmentUser(중간테이블)에 변화가 생기는게 맞다...위와 같은 중간 테이블 사용으로 인한 문제가 발생하였을 때 어떻게 강의자님이시라면 어떻게 해결하실지 궁금합니다!
-
미해결재고시스템으로 알아보는 동시성이슈 해결방법
version 컬럼이 증가하지 않는 이유
현재 강의 내용을 바탕으로 OptimisticLock 을 구현중인데,version이 증가하지 않는 이유를 알고 싶습니다.도메인은 이렇습니다.@Entity @Table(name = "appointment") public class Appointment extends BaseEntity { ... @OneToMany(mappedBy = "appointment") private List<AppointmentUser> appointmentUsers = new ArrayList<AppointmentUser>(); @Version private Long version; 그리고, 레포지토리는 이렇습니다.public interface AppointmentRepository extends JpaRepository<Appointment, Long> { ... @Lock(LockModeType.OPTIMISTIC) @Query("SELECT au FROM Appointment a " + "JOIN a.appointmentUsers au " + "WHERE au.id = :appointmentUserId ") AppointmentUser findByIdWithOptimisticLock(Long appointmentUserId); } 그리고, 서비스 계층 메서드는 이렇습니다.@Transactional(readOnly = true) @Service public class AppointmentUserService { ... @Transactional public void updateAuthority(Long appointmentId, Long loginMemberId, Long targetMemberId) { Member loginMember = memberRepository.getById(loginMemberId); Member targetMember = memberRepository.getById(targetMemberId); Appointment appointment = appointmentRepository.getById(appointmentId); AppointmentUser loginAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(loginMember, appointment); AppointmentUser targetAppointmentUser = appointmentUserRepository.getByMemberAndAppointment(targetMember, appointment); validateIsAdminMember(loginAppointmentUser.getId()); MemberAuthority targetAuthority = targetAppointmentUser.getMemberAuthority(); targetAppointmentUser.updateAuthority(MemberAuthority.getAnotherAuthority(targetAuthority)); appointmentUserRepository.save(targetAppointmentUser); } private void validateIsAdminMember(Long loginAppointmentUserId) { if (appointmentRepository.findByIdWithOptimisticLock(loginAppointmentUserId).getMemberAuthority() != MemberAuthority.ADMIN) { throw new NotAdminMemberException(); } } }위 updateAuthority 메서드 내에서 validateIsAdminMember를 호출하여, validateIsAdminMember 안에 있는 findByIdWithOptimisticLock을 통하여 Version을 올려주어서 낙관적 락을 성공적으로 구현할 수 있을 줄 알았는데 테스트를 해보니 아래와 같이 실패합니다.. @Test void 동시성_테스트() throws InterruptedException { Logger logger = Logger.getLogger(MultiThreadTest.class.getName()); ExecutorService executorService = Executors.newFixedThreadPool(2); CountDownLatch latch = new CountDownLatch(2); AtomicReference<Boolean> flag = new AtomicReference<>(false); executorService.execute(() -> { try { logger.log(Level.INFO, "첫 번째 요청 시작: member1 -> member2"); logger.log(Level.INFO, "버전1전" + appointmentRepository.getById(1L).getVersion()); appointmentUserService.updateAuthority(1L, 1L, 2L); logger.log(Level.INFO, "버전1완" + appointmentRepository.getById(1L).getVersion()); logger.log(Level.INFO, "첫 번째 요청 완료: member1 -> member2"); } catch (Exception e) { logger.log(Level.SEVERE, "첫 번째 요청 중 예외 발생", e); flag.set(true); } finally { latch.countDown(); } }); executorService.execute(() -> { try { logger.log(Level.INFO, "두 번째 요청 시작: member2 -> member1"); logger.log(Level.INFO, "버전2전" + appointmentRepository.getById(1L).getVersion()); appointmentUserService.updateAuthority(1L, 2L, 1L); logger.log(Level.INFO, "버전2완" + appointmentRepository.getById(1L).getVersion()); logger.log(Level.INFO, "두 번째 요청 완료: member2 -> member1"); } catch (Exception e) { logger.log(Level.SEVERE, "두 번째 요청 중 예외 발생", e); flag.set(true); } finally { latch.countDown(); } }); latch.await(); };위와 같이, 두 개의 스레드에서 진행을 하였고, 각 스레드 내에서 updateAuthority()를 실행 전후에 version을 찍어보았으나, 찍힌 4개의 version 모두 0이 나왔고 또한, 동시성 제어도 안되는 상황입니다.왜 0이 나오는걸까요.. 궁금합니다!(컴파일 에러는 없습니다)
-
미해결
동시성 처리 관련 스레드 풀 설정 질문
@Test @DisplayName("쿠폰 여러 명 발급") void 쿠폰_여러_명_발급() throws InterruptedException { int threadCount = 1000; ExecutorService executorService = Executors.newFixedThreadPool(32); CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadNumber = i + 1; Integer key = i; executorService.submit(() -> { try { couponService.issueCoupon(param, usersMap.get(key)); } catch (PessimisticLockingFailureException e) { .... }쿠폰 발급 동시성 처리 관련해서 테스트 코드 작성 간 궁금한 점이 있어서 질문을 올립니다. 구글링해서 작성해봤는데, 제가 이해하는 게 맞나 싶어서요... Executors.newFixedThreadPool(32) 이렇게 설정해주면, 32 개의 고정된 스레드 풀을 생성한다는 것이고 1,000 명의 유저가 해당 스레드 풀이 나눠서 작업이 수행된다는 것인가요 ? 그러니까 하나의 스레드에서 약 31명의 유저를 담당한다는 뜻일까요 ? 아니면 순차적으로 1,000 명의 유저를 하나의 스레드에 한 명씩 배치하여 작업하는 것이고, 실질적으로 한 순간에 32명의 유저만 작업한다는 뜻일까요 ? ㅠㅠㅠ
-
미해결1시간만에 끝내는 virtual thread in spring boot
pinned 확인법
https://github.com/openjdk/jdk/pull/17221이런 내용이 있네요. -Djdk.tracePinnedThreads 보다 JFR 을 권장하는 것 같습니다.
-
미해결1시간만에 끝내는 virtual thread in spring boot
1강 성능비교 질문
tomcat thread 를 500개로 늘리면 되지 않나요?
-
미해결Practical Testing: 실용적인 테스트 가이드
동시성 이슈 - 3회 이상 재시도를 자동으로 하게 하는 방법
안녕하세요,동시성 이슈에 대한 이야기를 해주시면서 "등록 시도를 했는데 유니크에서 튕겼다면 누군가 먼저 신규 번호를 선점했다는 뜻이니 3회 이상 재시도를 자동으로 하게 하는 방법으로 풀 수 있다"란 말씀을 해주셨습니다.그래서 아래 예시 코드처럼 DataIntegrityViolationException 을 try-catch로 잡아서 재시도를 하라는 뜻으로 이해했습니다. (하지만 로컬에서 동시성 문제를 발생시키는 방법을 찾지 못해 맞는 코드인지 모르겠습니다.. @Transactional이 있다면 DataIntegrityViolationException 를 잡기 위해서 try-catch 블럭 내부에서 flush()를 호출하기 위해 saveAndFlush() 를 사용한다는 말도 있더라구요?)public ExampleEntity save(ExampleEntity entity) { for (int i = 0; i < 3; i++) { try { // 저장 시도 return repository.save(entity); } catch (DataIntegrityViolationException e) { // 유니크 제약 조건 위배 예외 처리 log.info("저장 실패"); } } throw new RuntimeException("저장에 실패했습니다."); }이러한 방법이 맞다면 더 좋은 방법이 있는지, 혹시 만약 이 방법이 아니라면 어떤 방법으로 해결할 수 있는지 알려주시면 감사하겠습니다. 좋은 강의 항상 감사드립니다.
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
corePoolSize & maximumPoolSize 수강 중 질문입니다.
corePoolSize & maximumPoolSize - 기본 스레드 & 최대 스레드 22분 35초 정도 듣고 있는데 궁금한게 있습니다.ArrayBlockingQueue에 4개의 태스크가 모두 차면큐에 적재되지 못한 나머지 태스크는 어디서 대기 하나요?(핸들러가 없다는 가정하에) 그리고 max thread 까지 생성되면 큐에 적재되지 못한 나머지 태스크들을 max thread개수 만큼 처리하고 이후 찐으로 초과된 태스크는 처리하지 못해 Exception이 발생하나요? 뭔가 제가 이해를 잘 못하고 있는 것 같은데corePoolSize와 Queue 의 사이즈를 왜 더해서 태스크 개수와 비교하는지 이해를 못하겠어요. 태스크가 큐에 모두 채워지면 나머지 태스크 중 전체 쓰레드 개수 까지는 큐에 적재되지 못해도 처리가 된다로 이해 하면 될까요?아니면 원랜 큐가 다 차면 Exception이 발생하는데 해당 시간대의 예제에선 defaultHandler 가 뭔가 태스크가 모두 차도 쓰레드 개수만큼은 추가로 처리 할 수 있도록 뭔가 처리를 한 걸까요?--> 뒤에 더 보니 worker에서 바로 처리 하는 군요;; 추가로큐에서 태스크가 처리되면 다시 공간이 생길 것 같은데 왜 모두 차는지 이해가 잘 안되요. (제가 뭔가 잘못 알고 있는 걸까요) 강의 너무 좋습니다. 이후 강의도 빨리 내주세요 ㅠ_ㅜ
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
interrupt 스레드 시작안했는데도 걸리나요
sleep 1초 동안 thread1 시작도 안했었는데,어떻게 thread2에서 thread1을 interrupt 해서 결과가 true인지 궁금합니다.다시 말해서 아직 시작도 안한 스레드를 어떻게 interrupt 한걸까요?
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
synchronized 블록 동기화 마지막 예제
데드락 걸릴 수 있을거 같은데 맞나요?thread a: accountA.lockthread b: accountB.lockthread a : accountB.lock (대기)thread b: accountA.lock (대기)
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
SpinLock & Busy Waiting 강의 질문입니다.
안녕하세요! 강의 너무 잘 듣고 있는 수강생입니다.이전 동기화와 CPU의 관계 강의에서는 CPU가 두 개 이상의 명령어를 처리하면 원자성이 보장이 안된다고 설명을 하셨는데, SpinLock & Busy Waiting 강의에서 test_and_set(int *lock) 메서드는 CPU 하드웨어 계층에서 원자성이 보장된다는 게 이해가 잘 가지를 않습니다. test_and_set(int *lock) 이 메서드는 하나의 명령어만 처리하는 건가요??
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
사용자모드와 커널모드 피드백
유저 모드에서 I/O 장치들과 같은 특정 리소스에 접근이 불가한 것을 배웠습니다.스레드가 커널 모드가 필요하지 않는 작업을 수행할때도 어쨌든 CPU를 할당받아서 연산을 할 수 있다는 점도 직관적으로 알 수 있도록 그림이 아래와 같이 바뀌면 어떨까요?
-
미해결자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1]
cpu 개수만큼 실제로 할당되는지.. 궁금합니다.
강의에서 cpu 개수만큼 데이터 생성하고 병렬처리 하셨는데요.실제로 cpu 개수만큼 java 런타임에 모두 할당이 되나요? https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html여기 문서보니까, 자바에 할당된 cpu 개수를 의미하는거 같기도하고요.. chatgpt 는 운영체제에 할당된걸 인식하는거라고 하는데 뭔지 잘모르겠네용
-
미해결고수가 되는 파이썬 : 동시성과 병렬성 문법 배우기 Feat. 멀티스레딩 vs 멀티프로세싱 (Inflearn Original)
4분:59초 질문 그룹쓰레드 (5). 대기중인 작업 -> Queue -> 완료 상태조사 -> 결과 또는 예외 -> 단일화(캡슐화) 에 관해서
그룹쓰레드 (5). 대기중인 작업 -> Queue -> 완료 상태조사 -> 결과 또는 예외 -> 단일화(캡슐화) 에 관해서 궁금한것인데, 쓰레드를 사용할 때 시나리오 - 쓰레드가 여러개 생성되면 GIL로 인해 대기중인 작업은 내부적으로 Queue에 담긴다. - 10개를 실행했을 때 누군가는 완료했고, 누군가는 진행중인지 완료 상태조사를 진행한다. - 플래그 값을 조사해서 결과 또는 예외 등의 결과값을 받아오고 이것을 단일화한다단일화한다는 것을 예시를 들어서 설명해주실 수 있나요?그리고 왜 단일화를 해야하나요??