작업/온라인 아카데미

중복 문제 해결을 위한, Synchronized 써보기 !!

qpmi1zm29 2023. 12. 26. 23:08
프로젝트 환경 
Tomcat 8.5
JDK 1.8
SpringFramework 4.3.25  
Mybatis 3.5
Oracle 11g

 

Synchronized ?
- JAVA 단에서 지원하는 프로세스 동기화 키워드 
  참고 : https://qpmi1zm29.tistory.com/43
📝문제 상황 
해당 앱을 유지보수 할 당시, 반복적으로 접수되는 버그성 문의가 있었습니다.
사용자들의 수강 이력을 히스토리 성으로 관리하기 위해, 강의 최초 수강 시점에 "( 사용자 아이디 + 강의 고유 값 )" 을 키로 하여 테이블에 데이터를 남겼는데요.

당연히 삽입 전에 데이터 중복 확인을 하였으나, 같은 삽입 요청이 굉장히 빠르게 발생하는 경우
테이블에 insert 문이 반영되기 전, select ( 중복 조회 )가 이루어지게 되어 의도하지 않게 같은 데이터가 2건 씩 들어가는 케이스가 종종 발생하였습니다.

앱 운영에 치명적인 버그가 아니었고, 기존 사용자 분들이 해당 버그에 무딘 상태라, 문의가 접수되면 테이블에 중복된 데이터를 지우는 방식으로 앱을 계속 운영하였습니다.

 

 

 

앱을 새로 단장 하면서, 앱에 대한 애정도가 많이 커진 상태라 사용자 분의 편의성 또한 개선되길 바라며..

해당 버그를 픽스해보자!! 결심하게 되었습니다.

 

 

😂 첫번째 시도!
매니저 분들께 이런저런 방법을 여쭤보았을 때, 
MERGE 문을 →  SELECT + INSERT 문으로 나누어서 로직을 구성하면 시점 문제를 해결하는데 도움이 된다고 조언해주셨습니다.
MERGE INTO 강의히스토리테이블
USING DUAL
ON ( 과정ID = #{}
     AND 강의ID = #{}
     AND 사용자id = #{} )
WHEN MATCHED THEN 
	UPDATE SET 강의시간 = #{}
WHEN NOT MATCHED THEN
    최초 히스토리 INSERT문​

 

 

관련 문의 접수 빈도가 낮아진 것은 맞으나, 완전히 해결되지는 못했습니다.그러나 싱글톤 아키텍쳐 구현 방법을 공부하다가,  "Synchronized" 키워드를 알게 되었고, 팀 선배와 고민 끝에 코드에 적용해보기로 하였습니다!

 

😂 두번째 시도!
해당 로직의 메소드 레벨에 Synchronized 를 걸게 되었고, 테스트 시 클라이언트 단에서 더블 클릭으로 인한 중복 데이터 발생이 요청되었을 시에도, 로직이 정상적으로 동작하였습니다.

해당 앱의 평균 사용자 수는 1만 명 / 월  미만이었고, 교육의 성격 상, 한 달사용자 당 10 - 13개의 영상만을 수강하기 때문에, 처리 속도와 관련해 앱 사용에 문제가 없을 것이라고 생각하였습니다.

( 아래 코드는 임의의 코드 입니다. )
public synchronized void logicFunction( Object o ) {

        try{
            //중복 조회 select 문
            User userHistory = dbAdapter.selectOne( DataSource, User.history, new Map<String, Object> //input data
                    , User.class ); // return data type

            //히스토리 데이터 삽입
            if ( userHistory == null ) {
                int SuccessRow = dbAdapter.insert( Datasource, User.insertHistory, new Map<String, Object> ); //input data
                //SuccessRow 처리...
            }

            // 이후 로직...

        }catch( Exception e ) {

        }
    }​

 

그러나... 실제 운영 환경에 배포 후, 서비스를 오픈하고 얼마 안되어 병목 현상으로 인해 Timeout 응답이 계속 발생하였고,

intermax로 트랜잭션 현황을 확인 한 결과, 저 메소드 구간에서 응답 지연이 발생하고 있었습니다.. 😥..

급하게 이 전 버전으로 앱을 재배포 한 뒤, 해당 코드의 문제점을 확인하고 처리 속도를 높이기 위한 방법을 고민하게 되었습니다.

 

 

😃 세번째 시도!

1. 메소드 레벨이 아닌, 블록 레벨로 임계 영역을 제한하기 
    public void logicFunction( Object o ) {

        try{

            synchronized ( this ) {
                //중복 조회 select 문
                User userHistory = dbAdapter.selectOne( DataSource, User.history, new Map<String, Object> //input data
                        , User.class ); // return data type

                //히스토리 데이터 삽입
                if ( userHistory == null ) {
                    int SuccessRow = dbAdapter.insert( Datasource, User.insertHistory, new Map<String, Object> ); //input data
                    //SuccessRow 처리...
                }
            }

            // 이후 로직...

        }catch( Exception e ) {

        }
    }​


 메서드 레벨로 synchronized 키워드를 거는 것이 아닌,  특정 조건에 따라 메서드 내부 코드 블럭에 접근할 때에만 lock이 적용될 수 있도록 수정하였습니다. → 모든 요청에 대한 lock 할당/해지 및 동기화 처리에 낭비되던 부하 감소.



2. FBI ( Function Based Index ) 사용

FBI?
- 함수를 적용시킨 결과 컬럼에 인덱스를 생성하는 방법 

보통 로 사용하는 사용자 아이디 컬럼에 대하여, DB 테이블 마다 대문자와 소문자 데이터가 혼합되어 있었습니다.
그래서 Mybatis XML 파일에 UPPER나 LOWER 같은 function이 WHERE 절에 사용되는 부분이 있었는데,
해당 함수 사용으로 인해, 질의 시 인덱스를 타지 않아 속도가 오래 걸리게 되었고, DBA 분의 권고로 코드 블럭 내부에서 사용되는 쿼리에 FBI를 적용하게 되었습니다.


3. 히스토리성 테이블 데이터 정리
처음 앱을 런칭한 이후로 사용자 수강에 관한 이력 테이블 데이터를 따로 정리하거나 백업한 이력이 없었습니다.
더 이상 참조하지 않는 2010년도 데이터부터 존재하였기 때문에 테이블 row의 수가 매우 많았고, 이 부분이 질의 속도를 저해한다고 생각하였습니다.
현업 담당자님과 소통 후 2019년도 이전 데이터는 별도의 테이블로 백업하고, 이후 데이터만 남겨두게 되어, 테이블의 크기를  1/3 정도로 축소하였습니다.

저는 수기로 위 작업을 수행했으나, DB 스케쥴러를 통해, 주기적으로 backup용 테이블을 생성하여 데이터를 정리했으면 좋았을 껄,, 하는 아쉬움이 남습니다 😭



3가지 방법을 적용한 후, 인프라 담당자님을 통해 k6를 사용한 부하 테스트 결과 10,000 건의 사용자 요청이 들어왔을 때, 한 건당 요청 처리 속도0.26초로 떨어진 것과 병목 현상이 해결된 것을 확인할 수 있었습니다. 🎊