19,800원
다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
중간테이블에 대한 낙관적 락 적용법
현재 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이 나오는걸까요.. 궁금합니다!(컴파일 에러는 없습니다)
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
PessimisticLock 관련
안녕하세요. 강의 잘 듣고 있습니다. StockRepository 인터페이스에 PESSIMISTIC_WRITE 락을 걸고 테스트를 돌리면 잘 통과합니다. 다만, 궁금한 것은 쿼리를 보고 나서인데요,예를 들어, thread-5가 SELECT 쿼리를 젤 먼저 날립니다. 그러면 thread-5가 락을 가지고 있는거겠죠? 이후 다른 스레드들도 모두 SELECT 쿼리만 날리게 됩니다. 제가 예상한 것은 thread-5가 UPDATE 쿼리를 날리고 메서드가 종료되면 COMMIT 되면서 락을 놓게 되고, 다른 스레드가 락을 가져가면서 또 UPDATE 쿼리를 날릴 수 있게 되는 것이였습니다. 하지만 제일 먼서 UPDATE 쿼리를 날린 것은 thread-5가 아니라 다른 스레드였는데요, 혹시 왜 그런 것인지 설명 가능하실까요?
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
docker restart시에 java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
2024-04-22 21:51:48.515 ERROR 1928 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YESTerminal상에서 아래와 같이 Docker를 재시동해서Docker restart 8550a9a141fdDocker ps로 아래와 같이 제대로 동작하는 것도 확인했고igwangmin@igwangmin-ui-MacBookPro ~ % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8550a9a141fd mysql "docker-entrypoint.s…" 2 days ago Up 17 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysql컨테이너가 잘 작동하는것도 아래처럼 보았습니다.다른 게시물에서 알려주신 방법으로 Terminal을 확인했을때 아래와 같이 로그인을 하고 databases를 정상적으로 사용했지만 서버를 가동시켰을 때 하루 전에는 잘 작동했지만 다시 시작을 해보니 아래와 같이 오류가 발생합니다. 아래의 오류 구문이 발생합니다. 혹시 다른 방법이 있을까요?java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
- 해결됨재고시스템으로 알아보는 동시성이슈 해결방법
네임드락 테스트 오류
저는 @Query 어노테이션의 값을 아래와 같이 강의 내용대로 따라했을 때 오류가 발생해서 public interface LockRepository extends JpaRepository<Stock, Long> { @Query(value = "select get_lock(:key, 3000)", nativeQuery = true) void getLock(String key); @Query(value = "select release_lock(:key)", nativeQuery = true) void releaseLock(String key); }아래와 같이 정합성이 맞지 않아서 테스트를 통과하지 못했습니다.그래서 구문을 아래와 같이 바꿔서 통과했는데 참고 하시길 바랍니다. ?1의 값은 key로의 값으로 고정되어 있는데 이럴 경우 문제가 없을지 궁금합니다.@Query(value = "select get_lock(?1, 3000)", nativeQuery = true) void getLock(String key); @Query(value = "select release_lock(?1)", nativeQuery = true) void releaseLock(String key);
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
Docker restart시 spring boot 연동이 안됩니다.
igwangmin@igwangmin-ui-MacBookPro ~ % docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8550a9a141fd mysql "docker-entrypoint.s…" 15 hours ago Exited (0) 11 hours ago mysql igwangmin@igwangmin-ui-MacBookPro ~ % docker container restart 8550a9a141fdaee67b6f7c586a24085c98a9b99512cb7a8901f0c23cf9082536 8550a9a141fdaee67b6f7c586a24085c98a9b99512cb7a8901f0c23cf9082536 igwangmin@igwangmin-ui-MacBookPro ~ % docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8550a9a141fd mysql "docker-entrypoint.s…" 15 hours ago Up 4 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysqlDocker restart를 한 후 Spring을 가동했을 때 아래와 같은 오류가 발생합니다. 2024-04-21 09:25:02.485 INFO 1378 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-04-21 09:25:03.641 ERROR 1378 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.29.jar:8.0.29] at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.29.jar:8.0.29] at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828) ~[mysql-connector-java-8.0.29.jar:8.0.29] at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:448) ~[mysql-connector-java-8.0.29.jar:8.0.29] at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241) ~[mysql-connector-java-8.0.29.jar:8.0.29] 기존에 알려주신 방법인 아래와 같은 터미널 창에서 입력후 재실행하였으나 똑같은 오류가 발생해 혹시 다른 방법이 존재할까요igwangmin@igwangmin-ui-MacBookPro Mikor % docker exec -it mysql bash bash-4.4# mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.3.0 MySQL Community Server - GPL Copyright (c) 2000, 2024, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> use stock_example; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | stock_example | | sys | +--------------------+ 5 rows in set (0.00 sec) mysql> use stock_example; Database changed mysql> Spring 버전은 아래와 같고 plugins { id 'org.springframework.boot' version '2.7.1' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.shop' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } 도커 버전은 아래와 같습니다. igwangmin@igwangmin-ui-MacBookPro ~ % docker version Client: Docker Engine - Community Version: 26.0.0 API version: 1.44 (downgraded from 1.45) Go version: go1.22.1 Git commit: 2ae903e86c Built: Wed Mar 20 15:10:03 2024 OS/Arch: darwin/arm64 Context: desktop-linux Server: Docker Desktop 4.28.0 (139021) Engine: Version: 25.0.3 API version: 1.44 (minimum version 1.24) Go version: go1.21.6 Git commit: f417435 Built: Tue Feb 6 21:14:22 2024 OS/Arch: linux/arm64 Experimental: false containerd: Version: 1.6.28 GitCommit: ae07eda36dd25f8a1b98dfbf587313b99c0190bb runc: Version: 1.1.12 GitCommit: v1.1.12-0-g51d5e94 docker-init: Version: 0.19.0 GitCommit: de40ad0
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
Docker의 mysql과 스프링 연동이 안됩니다 (윈도우)
docker pull mysql:8.3.0 $ docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=1234 -d -p 3305:3305 mysql:8.3.0 ... use stock_example로컬에 mysql이 이미 깔려있어서 포트번호를 3305로 바꿔서 만들었어요. 위와같이 도커 초기설정 후 만든 yml 파일입니다spring: jpa: hibernate: ddl-auto: create show-sql: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3305/stock_example username: root password: 1234 logging: level: org: hibernate: SQL: DEBUG type: descriptor: sql: BasicBinder: TRACE 이렇게하고 Application을 실행했는데 com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failureThe last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:438) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:241) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:189) ~[mysql-connector-j-8.3.0.jar:8.3.0] at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:359) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:201) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:470) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:100) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-5.0.1.jar:na] at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:428) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:61) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:276) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:107) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:68) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.boot.model.relational.Database.<init>(Database.java:45) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:223) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:191) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:170) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1432) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1503) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75) ~[spring-orm-6.1.5.jar:6.1.5] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390) ~[spring-orm-6.1.5.jar:6.1.5] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-6.1.5.jar:6.1.5] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-6.1.5.jar:6.1.5] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366) ~[spring-orm-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1833) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.5.jar:6.1.5] at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1234) ~[spring-context-6.1.5.jar:6.1.5] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952) ~[spring-context-6.1.5.jar:6.1.5] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.5.jar:6.1.5] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.4.jar:3.2.4] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.4.jar:3.2.4] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.4.jar:3.2.4] at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.4.jar:3.2.4] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.4.jar:3.2.4] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.4.jar:3.2.4] at com.example.stock.StockApplication.main(StockApplication.java:10) ~[main/:na]Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failureThe last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. 이런 오류가 나네요 ㅠㅠ 보면 연결이 안되는거 같은데 어디서 문제일까요..
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
Lock 해제 시점이 궁금합니다.
안녕하세요!Lock을 해제하는 시점이 궁금합니다.아래 단계에서 어느 시점 이전 혹은 이후 인지 알려주시면 감사드리겠습니다. 1) 트랜잭션 시작2) Lock 획득 후 비즈니스 로직 실행3) 트랜잭션 커밋 (혹시, 3번 이전에 Lock을 해제할 경우 이전과 동일한 동시성 문제가 발생할 것 같은데.. 우선 답변부탁드립니다!)
- 해결됨재고시스템으로 알아보는 동시성이슈 해결방법
프로젝트를 처음 시작시에 java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES) 오류가 발생합니다.
spring: jpa: hibernate: ddl-auto: create show-sql: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/stock_example username: root password: 1234 # JPA 쿼리가 어떻게 나가는지 logging: level: org: hibernate: SQL: DEBUG type: descriptor: sql: BasicBinder: TRACEyml 설정은 위와 같이 하고 docker 이미지 확인시에 아래와 같이 작동하고 있습니다. igwangmin@igwangmin-ui-MacBookPro ~ % docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 03d3fae9019b mysql "docker-entrypoint.s…" 4 days ago Up 16 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysql DB 조회 시에도 아래와 같이 나오는 데 혹시 제가 놓친 부분이 있을까요? mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | stock_example | | sys | +--------------------+ 5 rows in set (0.01 sec)
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
Redisson을 Ebedded로 실행하는 방법이 있을까요?
안녕하세요.Redisson을 이용해서 분산락을 적용하고 있습니다.로컬과 테스트 코드를 실행할 때, Redis를 로컬에서 띄우지 않으면 사용할 수가 없습니다. 그래서 implementation 'com.github.codemonstur:embedded-redis:1.4.2' 의존성을 추가해서 Redis를 embedded로 실행할 수 있도록 했습니다. 하지만 Redisson은 연결이 되지 않는것처럼 보여서요. 혹시 방법이 있을까요? 만약, embedded로 실행할 수 없다면 로컬, 테스트에서는 어떤 방식으로 진행하시는지 경험적 의견을 여쭤보고 싶습니다. 감사합니다. Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisson' defined in class path resource [org/redisson/spring/starter/RedissonAutoConfigurationV2.class]: Failed to instantiate [org.redisson.api.RedissonClient]: Factory method 'redisson' threw exception with message: java.util.concurrent.ExecutionException: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: localhost/127.0.0.1:6379
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
테스트가 실패해요
하다가 안돼서 깃허브 소스 복붙하고 돌려도 테스트가 실패해요.
- 해결됨재고시스템으로 알아보는 동시성이슈 해결방법
docker ps시에 실행되는 이미지가 없다고 나옵니다.
docker ps시에 실행되는 이미지가 아래와 같이 존재하지 않습니다. 혹시 docker를 homebrew를 이용해 설치 한 후 desktop버전도 사이트에서 직접 깔아서 문제가 되는걸까여?
- 해결됨재고시스템으로 알아보는 동시성이슈 해결방법
docker version 오류
homebrew로 docker 설치하는 과정에서 docker version 확인 시에 아래와 같이 오류가 발생합니다. igwangmin@igwangmin-ui-MacBookPro ~ % brew list ==> Formulaeca-certificates libevent protobuf@21docker libfido2 xzdocker-completion lz4 zlibicu4c mysql zstdlibcbor openssl@3 ==> Casksdbeaver-community postmanigwangmin@igwangmin-ui-MacBookPro ~ % brew link docker Warning: Already linked: /opt/homebrew/Cellar/docker/26.0.0To relink, run: brew unlink docker && brew link dockerigwangmin@igwangmin-ui-MacBookPro ~ % docker versionClient: Docker Engine - CommunityVersion: 26.0.0API version: 1.45Go version: go1.22.1Git commit: 2ae903e86cBuilt: Wed Mar 20 15:10:03 2024OS/Arch: darwin/arm64Context: defaultCannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 그래서 docker 자체를 아래와 같이 삭제하고 새로 시작했는데도 계속 같은 오류가 발생합니다.
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
테스트 수행 결과가 항상 에러가 발생합니다.
안녕하세요. 해당 강의를 듣고 있는 수강생입니다. 제가 작성한 코드에서는 계속 테스트가 실패하여 강사님이 올려주신 코드를 기반으로 테스트를 재수행하였습니다. 그런데 여전히 해당 화면처럼 테스트가 실패하여서 어떤 문제일까하여 질문드립니다.코드 : https://github.com/sangyongchoi/stock-example 이슈 . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.7.0) 2024-03-24 19:26:47.245 INFO 5004 --- [ main] c.e.s.facade.NamedLockStockFacadeTest : Starting NamedLockStockFacadeTest using Java 17.0.10 on windows with PID 5004 (started by ac2di in C:\Users\ac2di\Desktop\vscode\stock-example-main) 2024-03-24 19:26:47.247 INFO 5004 --- [ main] c.e.s.facade.NamedLockStockFacadeTest : No active profile set, falling back to 1 default profile: "default" 2024-03-24 19:26:48.250 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode! 2024-03-24 19:26:48.252 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2024-03-24 19:26:48.519 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 257 ms. Found 2 JPA repository interfaces. 2024-03-24 19:26:48.532 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode! 2024-03-24 19:26:48.535 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode. 2024-03-24 19:26:48.553 INFO 5004 --- [ main] .RepositoryConfigurationExtensionSupport : Spring Data Redis - Could not safely identify store assignment for repository candidate interface com.example.stock.repository.LockRepository. If you want this repository to be a Redis repository, consider annotating your entities with one of these annotations: org.springframework.data.redis.core.RedisHash (preferred), or consider extending one of the following types with your repository: org.springframework.data.keyvalue.repository.KeyValueRepository. 2024-03-24 19:26:48.554 INFO 5004 --- [ main] .RepositoryConfigurationExtensionSupport : Spring Data Redis - Could not safely identify store assignment for repository candidate interface com.example.stock.repository.StockRepository. If you want this repository to be a Redis repository, consider annotating your entities with one of these annotations: org.springframework.data.redis.core.RedisHash (preferred), or consider extending one of the following types with your repository: org.springframework.data.keyvalue.repository.KeyValueRepository. 2024-03-24 19:26:48.554 INFO 5004 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 8 ms. Found 0 Redis repository interfaces. 2024-03-24 19:26:49.201 INFO 5004 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2024-03-24 19:26:49.275 INFO 5004 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.9.Final 2024-03-24 19:26:49.465 INFO 5004 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final} 2024-03-24 19:26:49.622 INFO 5004 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-03-24 19:26:49.962 INFO 5004 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2024-03-24 19:26:49.986 INFO 5004 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect 2024-03-24 19:26:50.663 DEBUG 5004 --- [ main] org.hibernate.SQL : drop table if exists stock Hibernate: drop table if exists stock 2024-03-24 19:26:50.708 DEBUG 5004 --- [ main] org.hibernate.SQL : create table stock (id bigint not null auto_increment, product_id bigint, quantity bigint, version bigint, primary key (id)) engine=InnoDB Hibernate: create table stock (id bigint not null auto_increment, product_id bigint, quantity bigint, version bigint, primary key (id)) engine=InnoDB 2024-03-24 19:26:50.762 INFO 5004 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 2024-03-24 19:26:50.775 INFO 5004 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2024-03-24 19:26:51.083 INFO 5004 --- [ main] org.redisson.Version : Redisson 3.17.4 2024-03-24 19:26:51.313 INFO 5004 --- [isson-netty-2-6] o.r.c.pool.MasterPubSubConnectionPool : 1 connections initialized for localhost/127.0.0.1:6379 2024-03-24 19:26:51.468 INFO 5004 --- [sson-netty-2-20] o.r.c.pool.MasterConnectionPool : 24 connections initialized for localhost/127.0.0.1:6379 2024-03-24 19:26:52.949 WARN 5004 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2024-03-24 19:26:54.634 INFO 5004 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator' 2024-03-24 19:26:54.714 INFO 5004 --- [ main] c.e.s.facade.NamedLockStockFacadeTest : Started NamedLockStockFacadeTest in 7.788 seconds (JVM running for 9.06) 2024-03-24 19:26:55.040 DEBUG 5004 --- [ main] org.hibernate.SQL : insert into stock (product_id, quantity, version) values (?, ?, ?) Hibernate: insert into stock (product_id, quantity, version) values (?, ?, ?) 2024-03-24 19:26:55.051 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1] 2024-03-24 19:26:55.052 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [100] 2024-03-24 19:26:55.052 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [0] 2024-03-24 19:26:55.251 DEBUG 5004 --- [ main] org.hibernate.SQL : select stock0_.id as id1_0_0_, stock0_.product_id as product_2_0_0_, stock0_.quantity as quantity3_0_0_, stock0_.version as version4_0_0_ from stock stock0_ where stock0_.id=? Hibernate: select stock0_.id as id1_0_0_, stock0_.product_id as product_2_0_0_, stock0_.quantity as quantity3_0_0_, stock0_.version as version4_0_0_ from stock stock0_ where stock0_.id=? 2024-03-24 19:26:55.253 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1] 2024-03-24 19:26:55.316 DEBUG 5004 --- [ main] org.hibernate.SQL : select stock0_.id as id1_0_, stock0_.product_id as product_2_0_, stock0_.quantity as quantity3_0_, stock0_.version as version4_0_ from stock stock0_ Hibernate: select stock0_.id as id1_0_, stock0_.product_id as product_2_0_, stock0_.quantity as quantity3_0_, stock0_.version as version4_0_ from stock stock0_ 2024-03-24 19:26:55.334 DEBUG 5004 --- [ main] org.hibernate.SQL : delete from stock where id=? and version=? Hibernate: delete from stock where id=? and version=? 2024-03-24 19:26:55.335 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1] 2024-03-24 19:26:55.335 TRACE 5004 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [0] 2024-03-24 19:26:55.437 INFO 5004 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2024-03-24 19:26:55.441 INFO 5004 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2024-03-24 19:26:55.514 INFO 5004 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.테스트 수행 시 콘솔 출력 창도 같이 첨부드립니다. 감사합니다.
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
yml 설정 내용 공유
혹시 귀찮으신분들 있으실까봐 공유드립니다.2칸(뎁스) 는 주의부탁드려요 spring: jpa: hibernate: ddl-auto: create show-sql: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/stock_example username: root password: 1234 # JPA 쿼리가 어떻게 나가는지 logging: level: org: hibernate: SQL: DEBUG type: descriptor: sql: BasicBinder: TRACE
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
Pessmistic Lock 획득 순서가 보장되는지 궁금합니다
안녕하세요 강사님.다름이 아니라 Pessmistic Lock 획득을 요청한 쓰레드 순서가 쓰레드1, 쓰레드2 쓰레드3이고 이때 쓰레드1이 먼저 락을 획득한 후 락을 해제하면,먼저 요청한 쓰레드2가 락을 반환한받는지 궁금합니다.제가 gpt 및 postgresql, spring data jpa 공식문서를 검색했을 땐, 락 획득 요청대로 락 획득 순서가 보장된다는 내용은 없었어서,혹시 이부분에 대해 알고계신지 궁금하여 질문 올립니다.
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
네임드락 선점 시간 설정
안녕하세요! 네임드락 사용 시 락 선점 시간 설정에 있어 궁금한 부분이 생겨 질문 드립니다. Named Lock 활용해보기 강의 25초 부분에 "선점 시간이 끝나야 락이 해제된다" 고 언급해주셨는데, 이 선점 시간을 어떻게 설정하는지 궁금합니다. "get_lock" 명령어에 주는 파라미터는 해당 lock 을 획득하기 위해 대기하는 시간의 최대값으로 알고 있습니다. 그래서 hikariCp 에서 얻어온 커넥션을 점유할 수 있는 최대 시간 설정 값도 찾아봤는데 찾지 못했습니다.별도의 타이머를 구현해서 타이머가 끝나면 해제로직을 실행시켜야 되는걸까요??혹은, @Transactional 의 timeout 설정 값을 통해 선점 시간을 설정하는 방법도 가능할 것 같습니다.
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
분산 DB 에서 비관적 락을 통한 동시성 제어
안녕하세요. 분산락을 언제 쓰는게 좋을지 고민하다가 몇 가지 궁금증이 생겨 질문드립니다.분산 DB 환경이 무엇을 의미하는지?흔히 분산 DB 환경에서 낙관적, 비관적 락으로 동시성 문제를 해결하기 힘들다고 얘기하더라고요. 여기서 말하는 분산 DB 라는 것이 샤딩에 의해 여러 DB 서버가 있는 것인지, 동일한 데이터를 저장하는 DB 서버가 여러 대 있는 환경을 의미하는 것인지 모르겠습니다.예를 들어, Ticket 이란 데이터를 저장하는데 동일한 ticket 데이터가 DB server 1, DB server 2 에 저장되어 있는 환경일까요??만약 분산 DB 가 샤딩인 경우 비관적 락으로도 동시성 이슈를 해결할 수 있을 것 같은데 맞을까요?아래 그림처럼 ticketId = 1 인 티켓을 예매하기 위한 요청이 동시에 올 경우 입니다. 처음 x-lock 을 잡은 요청이 끝나야 뒤늦게 온 요청이 해당 티켓의 잔여 수량을 확인하고 예매 하기 때문에 샤딩으로 인한 분산 DB 에서는 비관적 락으로 동시성 이슈를 해결할 수 있을 것 같습니다.동일한 Ticket 데이터가 여러 DB 서버에 중복되어 저장된 분산 DB 환경에서는 분산락을 사용해야 될 것 같습니다.그러나, 샤딩은 동일한 Ticket 데이터에 접근하기 위해서는 동일한 서버로 접근하기 때문에 비관적 락으로도 충분히 해결 가능할 것 같아서 질문 드립니다!
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
PessimisticLock을 분산락으로 활용하는 질문에 대한 답변 남기겠습니다.
github repository url: https://github.com/developer-yoni/ecommerce/tree/study/concurrency/redis여기서 study/concurrency/redis 브랜치를 확인해주시면 됩니다.StockServiceTest의 400번째 라인부터 503번째 라인까지테스트 코드가 작성되어 있습니다.큰 흐름은 동시성 이슈가 발생가능한 Stock에는 Lock을 걸지 않고,다른 Entity인 Market Entity에 PessimisticLock을 걸어,PessimisticLock을 분산락으로 활용하려는 시도 입니다.여기서 4_1 테스트는 Market에 PessimisticLock을 거는 트랜잭션과 Stock의 재고를 감소시키는 트랜잭션을 하나의 트랜잭션으로 묶었고,4_2 테스트는 별개의 트랜잭션으로 분리했습니다.이때 질문은 다음과 같습니다Q1. 동시성 이슈가 일어나지 않는 다른 Entity에 PessimisticLock을걸어 분산락처럼 활용하는게 문제가 되지 않을지 궁금합니다.혹시 문제가 된다면 , 어떤 측면에서 문제가 될지 궁금합니다.왜냐하면 어차피 동시성 이슈가 일어날 수 있는 측면의 값을 커밋하여 update함과 동시에PessmisticLock을 반환하는 것이니 문제가 되지 않을것이라고 생각했기 때문입니다.Q2. PessimisticLock을 건 트랜잭션이 커밋되거나 롤백될 때 비로소 PessmisticLock이 반환된다는 점을 근거로,4_2 테스트에서는 일부로 재고감소를 먼저 커밋한 후,Market의 PessimisticLock을 커밋하여,반드시 업데이트가 이뤄난 후 락을 반환하는것을 의도하였습니다.그러나 제 의도와 다르게 4_2 테스트는 계속 lock이 걸려있는?흐름을 보이면서 테스트에 실패합니다.그원인을 잘 모르겠습니다.바쁘신 와중에 답변 달아주셔서 감사합니다 강사님.
- 미해결재고시스템으로 알아보는 동시성이슈 해결방법
혹시 PessimisticLock을 NameLock처럼 분산락으로 활용할 수는 없을까요?
질문의 의도처럼 변경하고자 하는 Entity는 A인데,사실 Entity B에 PessimisticLock을 획득하고, 획득한 쓰레드들에 한해서만 A를 변경하게 하면 PessimisticLock을 분산락으로 활용할 수 있을것 같았습니다.그러나 사실, 제가 테스트해본 결과 동시성 이슈가 발생해서, 그 원인을 모르겠습니다.제 생각은 PessimisticLock을 얻지 못한 쓰레드는 계속 대기하면서 변경하고자 하는 Entity B를 변경할 수 없을것이라고 생각했습니다.@Transactionalpublic void decrease2(Long id, Long quantity) {//0. 여기서 동일한 UserEntity에 대해 PesimisticLock을 건다 userRepository.findByIdWithPessimisticLock(userId); //1. stock 조회 Stock stock = stockRepository.findByIdAndEntityStatus(id, EntityStatus.ACTIVE).orElseThrow(() -> new ApiException(ApiCode.CODE_000_0011, "재고 감소시, 요청값으로 들어온 stockId로 Stock 조회 실패")); //2. 재고 감소 // 여기서 stock.decreaseInventoryQuantity(quantity); //3. 갱신된 값을 저장 stockRepository.saveAndFlush(stock); // 마지막에 트랜잭션 커밋 되야 -> Pessimistic Lock이 반환된다}