문제 상황
재고가 감소되는 비즈니스 로직에 대해서 트랜잭션 처리를 해주고 비관적 락을 사용해 동시성 처리를 해주도록 해서 동시성을 제어하려고 로직을 짰다.
그리고 테스트를 통해 스레드풀을 생성해서 여러 스레드가 한번에 해당 트랜잭션에 접근하는 환경을 만들어 테스트를 하였다.
분명 Pessimistic Lock을 걸어서 해당 트랜잭션에 순차적으로 접근하도록 했다. 100개의 요청이 순차적으로 처리된다면 값은 0이 되어야 하는데 아래와 같이 순차적으로 처리가 되지 않았다.
어디선가 DB에 락이 제대로 걸리지 않았던가 트랜잭션 처리가 되지 않았거나 둘 중 하나의 문제인 것 같아서 어디가 문제인지 찾아보기로 했다.
Pessimistic Lock 설정
위와 같이 쿼리와 함께 락 설정을 해주었다. 아래는 비즈니스 로직이다.
파라미터로 들어온 id로 해당 상품을 가져오고 재고를 한개 감소시키는 로직이다. 트랜잭션을 걸어주었기 때문에 비관적 락이 작동할 것으로 예상된다.
락은 걸렸으나 실패한 테스트
해당 재고 감소에 대한 동시성 테스트 코드이다.
적절하게 락이 걸렸다면 select ~~ for update 라는 쿼리가 로그에 뜰 것 이다.
보니까 락도 잘 걸려서 가는 모양이다. 근데 어째 여전히 트랜잭션이 보장되지 않는다.
mySQL에서 락이 걸렸다면 트랜잭션 작업동안 다른 트랜잭션이 접근할 수 없어야 하는데 이상하게 트랜잭션이 보장되지 않는다. 왜 이럴까 답답했다...
혹시나 바꿔본 설정 Dialect, 그리고 성공 !
코드에 문제가 없다면 설정에 문제가 있을거라 생각하고 application.yml 파일을 열었다.
근데 뭔가 저기 저 dialect 부분이 거슬렸다. 내가 아는 mySQL 최신 버전은 8.0 버전인데 5라고 쓰여져 있는게 뭔가 수생했다.
혹시나 하는 마음에 해당 부분을 주석처리하고 다시 테스트를 돌렸다. 설마 되겠어...?
순차적으로 아주 잘 update 되는 것을 볼 수 있다.
아니 대체 dialect 가 뭐길래 제대로 동작하지 않았던걸까?
그래서 구글링을 하고 어떤 녀석인지 알아봤다.
Dialect
dialect의 사전적 정의는 '사투리' 또는 '방언' 이다.
SQL은 표준으로 통용되는 ANSI SQL이 존재한다. ANSI SQL 이외에도 DBMS Vendor(벤더, 공급업체)인 MS-SQL, Oracle, MySQL, PostgreSQL 에서 자신만의 기능을 추가한 SQL이 있다.
위의 그림처럼 MS-SQL의 T-SQL 또는 Oracle의 PL/SQL이 대표적이다.
또한 기본 키를 할당하는 방법에도 차이가 있다. MySQL의 경우 AUTO_INCREMENT 라는 기능과 Oracle의 경우 SEQUENCE라는 기능이 있다.
마치 수도권에서 사용하는 표준어가 있고 각 지방마다 사투리(방언)을 사용하는 것과 같다.
ANSI 란?
DBMS들이 (MS-SQL, Oracle, MySQL 등) 각기 다른 SQL을 사용하므로 미국 표준 협회에서 이를 표준화화여 표준 SQL문을 정립시켜 놓은 것이다.
아니 그러면서 미터법이랑 섭씨는 왜...
JPA는 어플리케이션이 직접 JDBC 레벨에서 SQL을 작성하는 것이 아니라 JPA가 직접 SQL을 작성하고 실행하는 형태이다.
그렇다면 DBMS 종류별로 SQL 문법이 다르니 이에 맞게 사용할 필요가 있다.
예를 들어 JPA를 사용해 게시판을 개발할 때 DBMS마다 다른 페이징 방법을 처리할 필요가 생기게 된다. 또한 각각 DBMS 벤더별로 다른 모듈을 개발 해주어야 한다.
만약 클라이언트의 요구에 따라 MS-SQL을 기준으로 작성했던 게시판 프로그램을 Oracle에 맞게 추가적으로 개발하려면 그만큼 비용이 더 많이 들게된다.
하지만 JPA를 사용하면 걱정 할 필요가 없게된다. 애초에 JPA, 아니 ORM Framework를 우리가 왜 사용하는지를 생각해보자.
객체와 관계형 데이터베이스를 자동으로 매핑해주고 추상화시켜 개발자가 코드를 좀 더 객체지향적으로 짤 수 있게끔 도와주는 녀석이다. 외부 시스템인 DBMS에 의존하던 개발 방식에서 벗어나 개발할 수 있게끔 해주는 고마운 녀석이다. (잘 모르고 쓰면 성능만 나빠진다..)
JPA로 개발하면서 복잡한 쿼리를 제외하고 단순 CRUD 선에서 쿼리를 짤 필요가 없다.
그렇다는건 우리의 관심사는 어떤 DBMS를 사용하는지가 아니다. 이 관심사는 JPA가 가지고 있으니 우리는 어떤 Dialect를 사용하게 되던 신경 쓸 필요가 없다. 왜냐하면 JPA가 Dialect라는 추상화된 방언 클래스를 제공하고 각 벤더에 맞는 구현체를 제공한다.
MySQL 버전의 차이
그렇다면 Dialect가 뭔지 알았고 원인을 알아냈다.
그런데 나는 Oracle도 아니고 MS-SQL도 아닌 MySQL로 잘 설정을 해놨는데 왜 안됐을까? 위의 application.yml을 다시보면 알 수 있다.
MySQL5Dialect 라고 설정을 해주었다. 뭔가 현재 버전이 아닌 구버전을 설정해놓은 것 같았다.
이래서 뭘 가져다써도 알고 가져다 써야 한다는게 뼈저리게 느낄 수 있었다. (이걸로 몇시간 날림)
MySQL5Dialect의 경우 5.5버전 이전의 버전을 사용한다고 나와있다. 해당 버전은 스토리지 엔진이 현재 MySQL 8 버전에서 사용하는 InnoDB 가 아닌 MyISAM로 채택되어 사용되고 있었다.
아래 캡처 사진을 보면 테이블이 만들어질 때 InnoDB가 아닌 MyISAM으로 설정되는 것을 볼 수 있다.
결정적으로 MyISAM 스토리지 엔진을 사용하게 되면 트랜잭션 기능을 지원을 안한다.
그러니 아무리 락을 먹여봤자 소용이 없었지... 애초에 트랜잭션이 작동을 안했으니...
그래도 이번 기회에 설정이나 코드를 가져다 쓸 때는 뭔지 알고서 써야 한다는 교훈을 또 얻을 수 있었다.
결론
그렇다면 Dialect를 알맞게 다시 따로 설정해주어야 하느냐? 하면 그렇지 않다.
Springboot 에서는 따로 설정을 해주지 않아도 연결되어 있는 DB에 맞게 설정을 해준다. 특별한 이유가 있지 않는 이상 따로 설정해주지 않아도 된다는 소리이다.
생각없이 MySQL5Dialect 설정을 가져다 썼다. => 스토리지 엔진이 MyISAM으로 설정됨. => 트랜잭션이 작동 안함. => 그러니 동시적 요청에 대한 격리가 안됨. => 베타 락도 소용 없음. => 테스트 실패.
라고 이번 문제 상황을 정의할 수 있겠다. 그리고 해당 설정을 없앰으로써 문제 상황을 해결 !
'Dev > Test' 카테고리의 다른 글
@EnableJpaAuditing을 분리하자 ! (0) | 2023.10.30 |
---|