SVN 저장소를 GitLab으로 이관하며 만난 네 가지 함정
중첩 경로, 작성자 매핑, Git GC, 그리고 HTTP 413 해결 기록
오래된 SVN 저장소를 GitLab으로 옮기는 작업은 얼핏 단순해 보입니다.
SVN 저장소 가져오기
→ Git 저장소로 변환
→ GitLab에 push
하지만 실제로 진행해보면 저장소 구조, 작성자 정보, 프록시 구성, 인증서까지 여러 계층이 한꺼번에 얽힙니다.
이번 글은 실제 이관 과정에서 발생했던 문제와 해결 방법을 일반화해 정리한 기록입니다. 회사명, 실제 도메인, 프로젝트명, 계정정보 등은 모두 제거했으며 예제에는 예약 도메인인 example.org와 임의의 프로젝트명 legacy-app을 사용했습니다.
1. 이관 대상과 기본 구조
예제 환경은 다음과 같습니다.
SVN
└─ https://svn.example.org/svn/ARCHIVE/legacy-app
GitLab
└─ https://git.example.org/platform/legacy-app.git
이관 서버에는 다음과 같이 작업 디렉터리를 구성했습니다.
/opt/svn2git/
├─ bin/
│ ├─ svn2git.sh
│ └─ authors.txt
├─ projects/
│ └─ legacy-app.sh
├─ sources/
└─ logs/
프로젝트별로 달라지는 값은 별도 설정 파일에 둡니다.
# /opt/svn2git/projects/legacy-app.sh
PROJECT="legacy-app"
SVN_URL="https://svn.example.org/svn/ARCHIVE/legacy-app"
GITLAB_URL="https://git.example.org/platform/legacy-app.git"
SVN_LAYOUT="sub"
WORK_BASE="/opt/svn2git/sources"
AUTHORS_FILE="/opt/svn2git/bin/authors.txt"
TARGET_BRANCH="main"
GIT_SVN_NO_METADATA="true"
CHECK_AUTHORS="true"
# 이관 서버와 Nginx Proxy Manager가 같은 서버에 있을 때 사용
GIT_PUSH_DIRECT_IP="127.0.0.1"
계정과 비밀번호는 설정 파일에 저장하지 않고 실행 중 직접 입력하도록 유지했습니다.
자동화 관점에서는 인증정보를 파일에 넣는 것이 편하지만, 다음 문제가 있습니다.
- 서버에 평문 비밀번호 또는 토큰이 남는다.
- 다른 관리자가 스크립트를 실행할 때 내 계정으로 push될 수 있다.
- 이관된 저장소의 소유 및 작업 주체를 혼동할 수 있다.
- 백업이나 형상관리 과정에서 인증정보가 노출될 수 있다.
반복 실행의 편의성보다 계정 경계가 더 중요하다고 판단했습니다.
2. 첫 번째 문제: SVN 경로가 상위 저장소까지 확장된다
증상
대상 SVN URL은 아래와 같이 하위 경로였습니다.
https://svn.example.org/svn/ARCHIVE/legacy-app
처음에는 다음 방식으로 실행했습니다.
git svn clone \
--trunk=/ \
https://svn.example.org/svn/ARCHIVE/legacy-app
그런데 git svn이 대상 경로에만 머물지 않고 상위 저장소까지 탐색하기 시작했습니다.
/svn/ARCHIVE/legacy-app
→ /svn/ARCHIVE
→ /svn
그 결과 대상 프로젝트와 관계없는 SVN 작성자가 발견되거나, 필요 이상의 경로가 조회되는 문제가 생겼습니다.
원인
git svn은 저장소 루트를 자동 탐색하면서 URL을 가능한 상위 경로로 축소하려는 특성이 있습니다.
대상 URL 자체를 하나의 독립된 프로젝트 루트처럼 가져오려면 URL 최소화를 막아야 합니다.
해결
하위 경로형 저장소에는 --no-minimize-url을 적용했습니다.
git -c gc.auto=0 svn clone \
"$SVN_URL" \
"$WORK_DIR" \
--no-minimize-url \
--authors-file="$AUTHORS_FILE" \
--no-metadata
저장소 유형별로 옵션을 분리했습니다.
case "$SVN_LAYOUT" in
root)
git -c gc.auto=0 svn clone \
"$SVN_URL" \
"$WORK_DIR" \
--trunk=/ \
--authors-file="$AUTHORS_FILE"
;;
sub)
git -c gc.auto=0 svn clone \
"$SVN_URL" \
"$WORK_DIR" \
--no-minimize-url \
--authors-file="$AUTHORS_FILE"
;;
std)
git -c gc.auto=0 svn clone \
"$SVN_URL" \
"$WORK_DIR" \
--stdlayout \
--authors-file="$AUTHORS_FILE"
;;
*)
echo "[ERROR] 지원하지 않는 SVN_LAYOUT: $SVN_LAYOUT"
exit 1
;;
esac
정리하면 다음과 같습니다.
유형사용 조건주요 옵션
| root | 지정 URL 전체가 저장소 루트 | --trunk=/ |
| sub | 큰 SVN 저장소 안의 특정 하위 프로젝트 | --no-minimize-url |
| std | trunk, branches, tags 표준 구조 | --stdlayout |
이번 사례에서 핵심은 sub 유형을 별도로 둔 것이었습니다.
3. 두 번째 문제: authors.txt에 없는 SVN 작성자
증상
이관이 꽤 진행된 뒤 다음과 같은 오류가 발생했습니다.
Author: VisualSVN Server not defined in authors file
SVN 커밋 작성자를 Git 작성자 형식으로 변환하려면 매핑 파일이 필요합니다.
SVN계정 = 표시이름 <이메일>
예를 들어 다음과 같습니다.
developer01 = Developer One <developer01@example.org>
developer02 = Developer Two <developer02@example.org>
VisualSVN Server = VisualSVN Server <visualsvn-server@example.org>
문제는 사람이 직접 작성한 계정뿐 아니라 SVN 관리 도구나 시스템 계정이 커밋 작성자로 남을 수 있다는 점입니다.
개선 방법
긴 변환 작업을 시작하기 전에 작성자 목록을 미리 검사하도록 했습니다.
SVN 작성자 추출 예시는 다음과 같습니다.
svn log --quiet "$SVN_URL" |
awk -F'|' '
/^r[0-9]+/ {
author=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", author)
if (author != "") {
print author
}
}
' |
sort -u
매핑 파일에 등록된 작성자도 추출합니다.
awk -F'=' '
/^[[:space:]]*#/ { next }
NF >= 2 {
author=$1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", author)
if (author != "") {
print author
}
}
' "$AUTHORS_FILE" |
sort -u
두 목록을 비교합니다.
comm -23 \
<(svn log --quiet "$SVN_URL" |
awk -F'|' '/^r[0-9]+/ {
author=$2
gsub(/^[[:space:]]+|[[:space:]]+$/, "", author)
if (author != "") print author
}' |
sort -u) \
<(awk -F'=' '
/^[[:space:]]*#/ { next }
NF >= 2 {
author=$1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", author)
if (author != "") print author
}' "$AUTHORS_FILE" |
sort -u)
결과가 존재하면 변환을 시작하지 않고 중단합니다.
[ERROR] authors.txt에 없는 SVN 작성자가 있습니다.
VisualSVN Server
legacy-admin
이 사전검사 하나로 몇 시간 진행된 변환이 작성자 한 명 때문에 실패하는 상황을 줄일 수 있습니다.
4. 세 번째 문제: git gc is already running
증상
SVN 변환이 끝난 뒤 저장소 최적화를 수행하는 과정에서 다음 오류가 발생했습니다.
fatal: gc is already running
처음 보면 전체 변환이 실패한 것처럼 보이지만, 이 단계에서는 이미 SVN 커밋 대부분이 Git 객체로 만들어진 상태일 수 있습니다.
즉, git gc 실패와 이관 실패를 동일하게 처리해서는 안 됩니다.
원인 후보
- 이전 git gc 프로세스가 아직 실행 중
- 비정상 종료 후 .git/gc.pid가 남음
- git svn clone 도중 자동 GC와 후처리 GC가 겹침
개선
먼저 git svn clone 중 자동 GC를 억제했습니다.
git -c gc.auto=0 svn clone ...
후처리에서는 PID가 실제로 존재하는지 확인합니다.
cleanup_stale_gc_pid() {
local gc_pid_file=".git/gc.pid"
local gc_pid=""
if [ ! -f "$gc_pid_file" ]; then
return 0
fi
gc_pid="$(head -n 1 "$gc_pid_file" 2>/dev/null || true)"
if [ -n "$gc_pid" ] && kill -0 "$gc_pid" 2>/dev/null; then
echo "[INFO] git gc 프로세스가 실행 중입니다. PID=$gc_pid"
return 1
fi
echo "[WARN] 오래된 gc.pid 파일을 제거합니다."
rm -f "$gc_pid_file"
}
그리고 GC 실패는 경고로 처리했습니다.
if cleanup_stale_gc_pid; then
if ! git gc --prune=now; then
echo "[WARN] git gc에 실패했습니다."
echo "[WARN] 변환 결과에는 영향이 없으므로 다음 단계로 진행합니다."
fi
fi
저장소 최적화는 다시 수행할 수 있지만, 이미 끝난 SVN 변환은 되돌릴 필요가 없습니다.
이런 후처리 단계는 다음처럼 구분하는 편이 안전합니다.
필수 단계
- SVN 커밋 변환
- 브랜치 생성
- 원격 저장소 push
보조 단계
- git gc
- 불필요한 git-svn 메타데이터 정리
- 로그 압축
보조 단계 실패 때문에 전체 변환 결과를 삭제하면 아까운 작업 시간을 또 태우게 됩니다.
5. 네 번째 문제: GitLab push에서 HTTP 413
증상
로컬 변환은 정상적으로 완료됐지만 GitLab으로 push하는 단계에서 실패했습니다.
Writing objects: 100%
error: RPC failed; HTTP 413
send-pack: unexpected disconnect
fatal: remote end hung up unexpectedly
HTTP 413은 서버가 요청 본문 크기를 허용하지 않았다는 뜻입니다.
문제는 GitLab 앞단에 여러 계층이 있을 수 있다는 점입니다.
Git Client
→ CDN 또는 외부 프록시
→ Nginx Proxy Manager
→ GitLab 내부 Nginx
→ GitLab Workhorse
이 중 어느 한 곳이라도 업로드 크기를 제한하면 push가 실패합니다.
Nginx Proxy Manager 설정
Nginx Proxy Manager의 GitLab Proxy Host에는 다음과 같은 설정이 적용되어 있었습니다.
client_max_body_size 1g;
proxy_request_buffering off;
proxy_buffering off;
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
send_timeout 3600;
GitLab 내부 Nginx도 충분한 값으로 설정했습니다.
nginx['client_max_body_size'] = '1g'
설정 반영:
docker exec gitlab gitlab-ctl reconfigure
docker exec gitlab gitlab-ctl hup nginx
그럼에도 413이 계속 발생했습니다.
실제 원인
GitLab 도메인이 외부 CDN 프록시를 경유하고 있었고, 최초 이관 push의 요청 크기가 그 앞단의 허용 범위를 초과했습니다.
NPM과 GitLab을 아무리 크게 설정해도 요청이 그 앞단에서 차단되면 내부 서버까지 도달하지 못합니다.
문제는 CDN 설정을 직접 변경할 권한이 없었다는 점입니다.
6. CDN 설정을 바꾸지 않고 로컬 NPM으로 직접 push하기
이관 서버와 Nginx Proxy Manager가 같은 서버에 있었기 때문에, push할 때만 외부 DNS를 거치지 않고 로컬 NPM으로 직접 연결했습니다.
원격 URL은 그대로 유지합니다.
https://git.example.org/platform/legacy-app.git
연결 대상 IP만 127.0.0.1로 강제합니다.
git \
-c "http.curloptResolve=git.example.org:443:127.0.0.1" \
push -u origin main
이 명령의 통신 경로는 다음과 같습니다.
일반 push
Git Client
→ 외부 DNS
→ CDN 프록시
→ Nginx Proxy Manager
→ GitLab
이관용 직접 push
Git Client
→ 127.0.0.1:443
→ Nginx Proxy Manager
→ GitLab
도메인은 그대로 사용하기 때문에 NPM의 Virtual Host 라우팅도 유지됩니다.
http.curloptResolve는 단순히 URL을 IP 주소로 바꾸는 방식과 다릅니다.
접속 IP : 127.0.0.1
HTTP Host : git.example.org
TLS 서버 이름 : git.example.org
따라서 NPM은 어떤 Proxy Host로 전달해야 하는지 알 수 있습니다.
7. 직접 연결했더니 인증서 오류가 발생한 이유
처음 직접 연결을 시도했을 때 다음 오류가 발생했습니다.
SSL: no alternative certificate subject name matches target host name
확인 결과 Git remote에 등록된 도메인과 NPM 인증서의 도메인이 달랐습니다.
Git remote
git-old.example.org
NPM 인증서
git.example.org
브라우저에서는 CDN이 별도의 인증서를 제공하므로 접속됐지만, CDN을 우회하자 NPM 원본 인증서가 직접 노출되면서 불일치가 드러난 것입니다.
인증서를 확인하는 명령은 다음과 같습니다.
echo |
openssl s_client \
-connect 127.0.0.1:443 \
-servername git.example.org \
2>/dev/null |
openssl x509 \
-noout \
-subject \
-issuer \
-ext subjectAltName
예상 결과:
subject=CN = git.example.org
issuer=C = US, O = Let's Encrypt
X509v3 Subject Alternative Name:
DNS:git.example.org
Git remote도 인증서와 일치하는 주소로 수정했습니다.
git remote set-url origin \
https://git.example.org/platform/legacy-app.git
그다음 다시 push했습니다.
git \
-c "http.curloptResolve=git.example.org:443:127.0.0.1" \
push -u origin main
이 방식으로 정상 이관됐습니다.
인증서 검증을 끄는 다음 방법도 기술적으로는 가능하지만 사용하지 않았습니다.
git -c http.sslVerify=false push
인증서와 도메인을 올바르게 맞출 수 있다면 검증을 비활성화할 이유가 없습니다.
8. push 출력만 믿지 않고 HEAD를 비교한다
한 번 실패한 push 뒤에 다음 문구가 출력된 적이 있었습니다.
Everything up-to-date
하지만 앞에서는 HTTP 413으로 전송이 실패한 상태였습니다.
따라서 최종 성공 여부는 push 메시지가 아니라 로컬과 원격 커밋 해시를 비교해 판단했습니다.
LOCAL_HEAD="$(git rev-parse HEAD)"
REMOTE_HEAD="$(
git \
-c "http.curloptResolve=git.example.org:443:127.0.0.1" \
ls-remote origin refs/heads/main |
awk '{print $1}'
)"
echo "LOCAL : $LOCAL_HEAD"
echo "REMOTE: ${REMOTE_HEAD:-<main 없음>}"
검증:
if [ -n "$REMOTE_HEAD" ] && [ "$LOCAL_HEAD" = "$REMOTE_HEAD" ]; then
echo "[OK] GitLab 이관 완료"
else
echo "[FAIL] 원격 main과 로컬 HEAD가 다릅니다."
exit 1
fi
최종적으로 두 값이 같아야 이관 완료로 판단합니다.
9. git-svn 메타데이터 정리
변환 후 로그에 다음과 같이 git-svn 참조가 남을 수 있습니다.
HEAD -> main, git-svn
단순히 .git/svn 폴더만 지워서는 packed-refs에 참조가 남을 수 있습니다.
다음과 같이 Git 명령으로 참조를 제거합니다.
git update-ref -d refs/remotes/git-svn
git config --remove-section svn-remote.svn \
2>/dev/null || true
rm -rf .git/svn
확인:
git show-ref | grep git-svn || true
아무 결과가 나오지 않으면 정리된 상태입니다.
10. 인증정보를 스크립트에 넣지 않은 이유
작업을 자동화하다 보면 다음과 같이 만들고 싶은 유혹이 있습니다.
SVN_USERNAME="..."
SVN_PASSWORD="..."
GITLAB_USERNAME="..."
GITLAB_PASSWORD="..."
별도 권한 파일에 넣고 chmod 600으로 제한하는 방법도 있지만, 이번에는 적용하지 않았습니다.
이관 작업은 일반적인 자동 배포와 성격이 조금 다릅니다.
- 프로젝트마다 작업 계정이 달라질 수 있다.
- 다른 관리자가 동일 스크립트를 실행할 수 있다.
- 잘못된 계정으로 저장소가 생성되거나 push될 수 있다.
- 개인 토큰이 공용 서버에 장기간 남을 수 있다.
- 스크립트 복사와 백업 과정에서 유출될 수 있다.
따라서 다음 원칙으로 정리했습니다.
프로젝트 설정
- SVN URL
- GitLab URL
- 저장소 구조
- 브랜치명
- 작업 경로
실행 시 입력
- SVN 계정
- SVN 비밀번호
- GitLab 계정
- GitLab 토큰 또는 비밀번호
조금 불편하더라도 작업 주체를 명확하게 유지하는 쪽이 낫다고 판단했습니다.
또한 인증정보를 다루는 스크립트는 다음 방식으로 실행하지 않는 것이 좋습니다.
bash -x svn2git.sh
쉘 디버깅 출력에 변수나 명령 인자가 노출될 수 있기 때문입니다.
11. 최종 이관 절차
전체 작업은 다음 흐름으로 정리할 수 있습니다.
1. 프로젝트 설정 검증
2. SVN 인증 확인
3. SVN 작성자 목록 사전검사
4. git svn clone 수행
5. main 브랜치 생성
6. git-svn 메타데이터 제거
7. git gc 수행
8. GitLab 원격 저장소 연결
9. GitLab 인증정보 입력
10. 필요 시 CDN을 우회해 로컬 NPM으로 push
11. 로컬 HEAD와 원격 main HEAD 비교
실행 예시:
cd /opt/svn2git
./bin/svn2git.sh \
-p ./projects/legacy-app.sh
SVN 변환은 끝났고 push만 다시 수행해야 한다면 전체 작업을 재실행하지 않도록 push-only 모드를 두는 것도 유용합니다.
./bin/svn2git.sh \
-p ./projects/legacy-app.sh \
--push-only
대용량 저장소에서는 “실패하면 처음부터”가 가장 비싼 해결책입니다.
단계를 잘게 나누고 재개 지점을 만드는 것이 중요합니다.
마무리
이번 이관에서 가장 중요한 교훈은 git svn clone 명령 하나보다 전체 통신 구조와 실패 지점을 구분하는 것이었습니다.
문제를 요약하면 다음과 같습니다.
문제원인해결
| 상위 SVN 경로까지 탐색 | URL 자동 최소화 | --no-minimize-url |
| 변환 중 작성자 오류 | authors 매핑 누락 | 변환 전 작성자 사전검사 |
| git gc is already running | 자동 GC 또는 오래된 PID | 자동 GC 억제, 보조 단계로 처리 |
| GitLab push HTTP 413 | 앞단 프록시의 업로드 제한 | 로컬 NPM으로 직접 push |
| 직접 연결 SSL 오류 | remote 도메인과 인증서 불일치 | 인증서에 맞는 도메인 사용 |
| 성공 여부 불명확 | push 출력만 확인 | 로컬·원격 HEAD 비교 |
SVN에서 Git으로 옮기는 작업은 단순한 소스 복사가 아닙니다.
커밋 이력, 작성자, 브랜치, 네트워크, 인증서, 프록시를 모두 통과해야 비로소 “이관 완료”라고 할 수 있습니다. 특히 변환과 push를 별도 단계로 분리해두면, 마지막 5분의 네트워크 오류 때문에 몇 시간짜리 변환을 처음부터 다시 하는 일을 피할 수 있습니다.
'study > TIP' 카테고리의 다른 글
| 당근마켓 반품 후기 (0) | 2026.07.21 |
|---|---|
| GitHub Sponsors 가입 과정 정리 (0) | 2026.07.09 |
| 커서 학생인증은 안되는구나... edu 메일만 되는군요. (0) | 2026.07.01 |
| Git 이슈 2. VSCode에서 Pull을 다 받았는지 확인하는 방법 (0) | 2026.06.10 |
| Git 이슈 1. Eclipse / 디자이너 PC에서 Pull 시 .gitignore 충돌 해결 (0) | 2026.06.10 |