이슈 해결

[SCM / 개선] SCM Rest API를 사용한 다중 커밋 성능 개선

cob 2025. 6. 30. 17:31

 

SCM Manager란?
SCM Manager는 Git, Subversion(SVN), Mercurial 등 다양한 버전 관리 시스템을 통합적으로 관리할 수 있는 오픈소스 형상관리 플랫폼이다. 일반적으로 Git이나 SVN을 단독으로 사용할 경우, 별도의 서버 설정이나 사용자 관리, 권한 관리가 번거롭다. SCM Manager는 이런 불편을 해소하기 위해 웹 기반 UI와 Rest API를 제공하며, 다음과 같은 기능을 지원한다.

 

SCM Manager 주요 특징

  • Git, SVN, Mercurial 등 멀티 저장소 통합 관리
  • 직관적인 웹 UI 제공
  • 사용자 및 권한 관리 기능
  • Rest API 기반의 자동화 및 연동 지원
  • 플러그인 확장을 통한 기능 추가 가능
  • 소스 코드 브라우징 및 변경 이력 조회
  • 사내 구축형으로 운영 가능

 

 

Issue - 대량 코드 등록 시 성능 지연

개발 / 운영 과정에서 소스 코드를 한두 개씩 등록하는 것은 문제가 없지만, 특정 상황에서는 수십 건의 소스 파일을 한 번에 등록해야 하는 경우 문제가 발생했다. 예를 들어, 엑셀 파일로 정리해 한 번에 등록하는 방식이 있다.

 

 

 

Problem - SCM Rest API

SCM 시스템에서는 소스 등록을 위해 Rest API를 제공하고,  Rest API를 문서 정리되어 있었지만, 안내하는 방식은 다음과 같은 한계가 있었다.

단일 파일 커밋만 지원
✅ 대량 등록 시 파일 건수만큼 반복 요청 필요
✅ 매 요청마다 네트워크 통신, 인증, 서버 처리 로직이 반복되어 누적 지연 발생

 

실제 테스트에서 엑셀로 준비한 41건의 소스 파일을 순차적으로 등록하자, 약 2분 24초(2.4분)가 소요되었다.
단순히 소스 등록 속도의 문제를 넘어, 운영 효율성과 자동화 측면에서 상당한 제약이 있어 개선이 필요!

 

 

Solution - 다중 커밋 & 메모리 기반 처리로 성능 개선

1) 다중 파일 커밋을 지원하는 API 활용

SCM Rest API 문서에는 없었지만, SVN 플러그인을 뜯어보니 내부적으로 다중 파일을 한 번에 커밋할 수 있는 API가 존재해 이를 활용하면 별도의 반복 요청 없이, 여러 파일을 한 번에 묶어서 처리할 수 있었다.

플러그인 API

 

 

MultipartFormDataInput는 다중 파일 처리가 가능하기 때문에 해당 소스를 분석하여 양식에 맞게 값을 설정

 

protected void commitFiles(String ment) throws Throwable {
        try {
            String url;
            int i = 0;

            HttpHeaders headers = createHttpHeaders();

            headers.setContentType(MediaType.MULTIPART_FORM_DATA);

            JSONObject files = JSONObject.createNewInstance();
            JSONObject commitBody = new JSONObjectImpl();
            commitBody.put("commitMessage", StringUtil.isBlank(ment)? " ": ment);

            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            for (CommitData commitData: getFileCommitList()) {
                if (commitData!= null)
                {
                    //생성된 ByteArrayResource를 멀티파트 요청에 추가
                    body.add("file" + i, commitData);  // 멀티파트로 파일 추가
                    files.put(commitData.getFilename(), toPath(commitData.getCommitPath() +"/"+ commitData.getFilename(), true) );
                    i++;
                }
            }

            commitBody.put("names", files);
            body.add("commit", commitBody.toString());
            /**
             * 파일 생성/수정 커밋
             */
            url = getSCMProperties().getApiUrl() + "/v2/edit/"
                    + getSCMProperties().getAdminId()
                    + "/" + getProjectId()
                    + "/create";

            post(url, new HttpEntity<>(body, headers), String.class);
        }
        catch (Throwable t) {
            if (t instanceof HttpClientErrorException)
            {
                String detailMessage = ((HttpClientErrorException)t).getResponseBodyAsString();
                if (!StringUtil.isBlank(detailMessage))
                {
                    JSONObject errorJson = JSONObject.fromObject(detailMessage);
                    String errorCode = errorJson.getString("errorCode");
                    // 40RaYIeeR1 : 기존 파일과 변경사항 없을 시 오류 코드
                    if (VALUE_SCM_NOT_MODIFY.equals(errorCode)) return;
                }
            }

            LOG.debug(t);
            if (t instanceof RuntimeException) {
                throw (RuntimeException)t;
            }
            throw new JexRuntimeException(t);
        }
        finally {
            closeSource();
        }
    }

 

 

2) 메모리 기반 처리

기존 방식은 엑셀 데이터를 기반으로, 매번 SCM에 템플릿을 요청해 파일을 생성한 뒤, 해당 파일을 서버로 전송하는 구조였다.
즉, SCM 템플릿 요청 → 파일 생성 → SCM Commit 순서로 동작했기 때문에, 중복된 템플릿 요청이 반복되고 불필요한 파일 생성으로 인해 디스크 I/O 비용이 발생한다.

이를 개선하기 위해, 템플릿 매니저를 싱글톤 패턴으로 설계하고, 요청된 템플릿은 메모리에 캐싱하여 재사용하는 구조로 변경하였다.
이로 인해 중복 요청을 제거하고 디스크 I/O를 줄여, 전체 성능을 크게 향상시킬 수 있었다.

@Component
public class StudioPRJSettingManager {
    private Map<Class<?>, Map<String, AbstractTemplet>> templetPool = new Hashtable<>();


	 ...
     <T extends AbstractTemplet> T getTempletManager(String projectId, Class<T> targetClass, Supplier<T> supplier)
     {
         Map<String, AbstractTemplet> pool = templetPool.computeIfAbsent(targetClass, key-> new Hashtable<String, AbstractTemplet>());
         
         return targetClass.cast(pool.computeIfAbsent(projectId, key -> supplier.get()));
     }
}

 

 

Test 결과 - 극적인 성능 개선 확인

구분 요청-응답 시간
AS-IS 기존 구조 2.4분
TO-BE 개선 구조 30.77ms

 

 

반응형