부동산 서바이벌 – HUG 임대인 보증보험 전세금 돌려받기

9월 6일: 기간 종료 통보

전세계약을 연장하지 않겠다는 문자 메세지를 보내자 집주인으로 부터 전화가 와서 사정이 좋지 않으니 전세계약 중도해지 합의서를 써주겠으니 그걸로 HUG에 보증금 신청을 하라고 한다.

9월 8일

이리저리 합의서 양식과 필요한 서류를 알아보고 나서 집주인에게 어디로 보내주면 사인해서 보내 줄래 물었더니 자기가 필요한 서류와 양식 정보를 이미 모두 가지고 있으니 내가 받을 곳을 말하라고 한다. 이 사람 프로다.

9월 11일

지난 주 금요일에 임대인이 보냈다던 중도계약해지 합의서가 등기로 도착했다. 미리 도장이 찍힌 서류들과 함께 선처 탄원서가 들어있었다. 그냥 잠수 타버려서 연락이 안된다던 다른 임대인 모씨와는 달리 서류도 잘 준비 해주고 해서 당장이라도 사인해서 보내주고 싶긴했지만 일단 잘처리 된 다음으로 미루고 일단 임차권등기명령 신청부터 해야한다.

9월 12일(D+0): 임차권 등기명령 신청

관할지법인 남부지방법원으로 임차권등기명령을 신청하러 반차를 냈다. 필요한 서류는 다음과 같다.

부동산 등기부등본(당해, 발급용)
임대차 계약서 사본
주민등록초본
임대차 종료 입증서류(내용증명 배달증명, 문자메세지 혹은 임대차 해지 합의서)

들고 간 서류들을 가지고 신청서를 작성하고, 송달료, 수수료, 인지세 등등 으로 대략 4만 3천원 정도가 들었다. 카드를 사용할 수는 있으나 추가 서류를 작성하기가 번거로울 것이라는 안내가 있어서 법원 내에 있는 은행에서 현금으로 지불 하고 영수증을 포함한 서류를 받아들고 다시 담당자를 찾아갔다.

다시 번호표를 뽑고 기다린 끝에 만난 담당자는 이리저리 꼼꼼한 첨삭지도를 한 다음 접수해 주면서 기간은 4주 정도 걸릴 것이라고 한다.

9월 27일(D+15)

아직 우편송달은 오지 않았지만 왠만큼 기다렸다 싶어서 안내 받았던 대로 법원의 홈페이지의 ‘나의 사건 검색’ 항목에서 사건 번호를 넣어 봤더니 엊그제 일자로 인용 결정이 되었고 아직 우편 송달은 완료되지 않았다는 내용을 검색할 수 있었다. 인터넷으로 등기를 확인해 봤더니 “교합대기”로 나오는 것으로 봐서 아직 등기가 모두 완료되지는 않은 모양이다. 신청으로 부터 대략 2주 정도 지났는데 오늘 부터는 추석기간이니 좀 더 시간을 두고 봐야겠다.

10월 5일(D+23): 임차권 등기 완료

우편으로 등기내역이 도착했다. 등기사항을 조회해보니 주택임차권이 등기된 내역을 확인할 수 있었다. 이제 드디어 보증보험을 신청 할 수 있게 되었다.

10월10일(D+28)

전경련에 있는 도시보증공사 서부지사를 찾아갔다. 지사별로 관할하는 임대인이 다른 모양인데, 입구에서 집주인의 주민번호를 이를 확인해 주신다. 주섬주섬 꺼내드는 서류에 적힌 임대인의 이름을 쓱 보더니 “(임대인이) OOO 에요? 그럼 여기 맞아요” 하신다. 이 지사 보증사고 1등 이랜다.

대기시간이 너무 길어서 오픈런하는게 낫다는 이야기를 들었는데, 오후 2시쯤에 방문했더니 앞에 15명 있었다. 처음에는 금방 빠지겠거니 했는데 대기 예상 대기 시간은 무려 4시간 30분 이라고 나온다. 다들 이 긴 예상시간을 보고 다른 일을 하러 갔던 것인지 대기 순번이 없어서 건너 뛰는 경우가 많았다. 그 와중에 자기 번호가 이미 넘어 갔으니 먼저 처리해 달라는 사람들이 몇몇 있었고 결국 2시간 정도가 걸려서 내 순서가 되었다.

적은 돈이 아니기에 예상은 했지만, 나름대로 꼼꼼히 준비해 간 서류였는데도 여러건의 빨간펜을 받았다.

  1. 보증 사고 접수는 계약 해지로 부터 2달 이후부터 가능하다. 즉, 나는 9월 11일자로 해지 계약을 했으니 11월 12월 부터 접수가 가능하다.
  2. 임대인의 인감 증명서는 관계 없으나 임차인의 인감 증명서(내꺼)는 등기사항이 포함된 접수일 1개월 이내의 것이 필요하다. 4부를 제출 해야 한다.
  3. 주민등록 초본은 임차권 등기 이후의 것을 제출해야 하며 이 것 역시 접수일 1개월 이내의 것이어야 한다.
  4. 부동산 등기부 등본은 법원의 등기사항이 기재되어 있는 것이어야 한다.

심사일정은 빠르면 한 달 정도 소요되고 새로운 주택을 구할 일정이 충분히 주어지니 계약서 상의 날짜에는 구애받을 필요가 없다는 설명을 들었다.

11월 13일(D+62): 전세 보증보험 신청

오늘은 오전 반차를 내고 오픈런 할 생각이었는데 생각해 보니 지난 번에 필요하다던 서류를 준비 못했다. 서둘러서 동사무소에 들러 인감증명, 주민등록 초본, 등기부 등본을 챙기고 HUG로 달려서 9시 40분 무렵에 도착했는데 대기순번을 등록하고 보니 앞에 10여명이 이미 있었고 대기 예상시간은 1시간 30분 이라고 했다. 시간이 꽤 지나고 앞에 5명이 남았는데 여전히 대기 예상시간은 1시간 30분이라고 한다. 인원 수 당 시간으로 대기 예상시간을 계산하는 것이 아닌가 보다. 옆에서 들리기로는 한 사람당 대략 30분 걸린다고 하니, 1시간 30분은 표시되는 가장 큰 값인지도 모르겠다.

실제로는 대략 30분 정도가 걸려서 내 차례가 되었다. 자신 만만하게 들이민 서류를 유심히 보던 다른 접수원은 일단은 접수하고 미비한 서류를 추가로 구비해서 보내주면 심사 절차를 시작하겠다고 한다.

보완 해야 할 서류 목록을 받았다.

  • 주택 임차권등기명령 법원 결정문의 원본: 사본은 안되고 원본이 있어야 함.
  • 신분증 사본: 누락되었으므로 추가해야 함. 양면 복사.
  • 전세금 입금 했던 통장 내역: 받는 사람의 이름이 나와 있어야 함.
  • 주민등록 등본: 원래는 등본/초본 중 하나로도 되지만 이왕 미비 서류 보완하는 김에 추가하라고 함.

다른 건 다 마련하면 되겠지만 법원 결정문 원본 서류를 찾을 래야 찾을 수가 없었다. 분명 소중히 모셔 둔답시고 어딘가에 고이 보관한 기억이 있는데, 다른 서류들과 함께 둔 뭉치에서는 도무지 찾을 수가 없었다. 어떡하냐 법원으로 달려야지 뭐…

법원에서 발급해 준 결정문 정본은 접수하고 인지세 1000원을 납부하자 곧바로 발급되었다. 보완 서류들을 우편으로 보낼 수도 있지만 4시 이전까지는 직접 방문해서 제출할 수도 있다. 다행히 보완 서류는 번호표를 뽑지 않고 빠르게 접수 할 수 있었다.

11월 14일 (D+63)

HUG로 부터 담당자가 배정되었다는 문자 메세지를 받았다.

12월 11일 (D+90): 심사승인!

5주 정도를 기다린 끝에 드디어 심사 승인되었다는 문자 메세지가 왔다.

이제 본격적으로 이사 할 집을 알아 봐야겠다.

12월 19일

지난주에 부동산에서 추천해 주었던 다섯채 중에 네채를 둘러 보고 이사갈 집을 결정했다.

1월 6일: 이사갈 집 계약

이사 갈 집에 대한 전세 계약서를 작성하고 HUG에서 받은 문자에 적혀 있는 담당자의 이메일로 “2월 29일에 이사예정이니 명도확인 절차에 대한 안내를 부탁드립니다.”라고 메일을 보냈다.

안심전세 앱에 있는 악성임대인 조회에서 예전 집주인을 검색해봤더니 반환 채무가 65억이 넘는다. 크게 땡기셨고만…

2월 27일

관리회사에 연락 해서 관리비 완납 증명서를 받고 가스앱에서 이사예약을 신청했다.

2월 28일

이사예정일 하루 전이 되자 HUG에서 보증금이 입금될 예정이라는 카톡이 왔다. 저녁 6시 이후에 안심전세 앱에서 “명도증빙자료제출”을 누르니 자료를 올리는 화면이 나왔다.

2월 29일: 이사날

전기와 수도 요금은 모두 당일에 정산하고 영수증을 받는 것이 가능했는데 디지털 전기계량기의 전력사용량을 보는 방법이 복잡해서 조금 헤맸다.

인터넷도 끊겨있어서 4G잡고 천신만고 끝에 관련서류들을 모두 올렸는데, 담당자에게서 전화가 와서 “전산이 고장나서” 서류를 확인 할 수 없다고 한다. 뿐만 아니라 보증금 반환도 지연되고 있다고. 아뉘! 부동산, 집주인, 이전 살던 사람, 나까지 기다리고 있는 사람들이 줄줄이인데 지금…

일단 이메일로 보완 할 서류가 있는지 확인해 주겠다고 해서 앱에 올렸던 내용을 담당자에게 모두 보내고 기다리자 확약서가 빠져있었다면서 양식을 보내 줄테니 자필로 써서 보내라고 했다. 이건 제출 서류에서 못봐서 생각지도 못했는데 “내가 모든 요금을 정산했고 이후에 문제가 있으면 책임지겠다.” 이런 내용을 쓰고 자필 서명을 했다.

그리고 얼마간 기다리고 있으니 시스템이 복구 되었다며 보증금이 들어 왔다. 최악의 경우에는 모두가 오후까지 기다리거나 이사가 나가리 될 수도 있었는데 어찌저찌 잔금을 치르고 이사를 마칠 수 있게 되었다.

참조

보증이행 안내 https://www.khug.or.kr/hug/web/ge/er/geer001100.jsp

GitHub release에 바이너리 첨부 자동화

GitHub에서 release를 생성하면 source code의 snapshot이 zip과 tar.gz로 저장된다. 여기에 추가해서 컴파일된 결과가 자동으로 추가하도록 한다면, 간단히 source code와 연계된 바이너리도 함께 배포할 수 있을 것이다.

이 글에서는 안드로이드 프로젝트를 가정해서 release를 생성할 때 안드로이드 APK를 빌드하고 source code와 함께 배포하는 간단한 workflow를 설명한다.

전체 코드

Event trigger

on:
  release:
    types: [published]

Release에서만 동작하므로 event trigger는 release – published이다. 이 event는 web상에서 새로운 release package를 publish 할때 trigger 된다.

환경변수

env:
  TAG: ${{ github.ref_name }}
  ASSET_FILE_PATH: "./prebuilt-${{ github.ref_name }}.zip"

Release package를 생성할 때 넣는 version의 이름은 github.ref_name으로 참조 된다. 첨부되는 파일의 이름은 prebuilt-<version_tag>.zip으로 설정한다. 참고로 GitHub에서는 release에 추가되는 소스코드외의 파일들을 “Asset”이라 부른다.

빌드 수행 및 Asset 생성

    # Checkout source code and build Android APKs.
    - name: Checkout
      uses: actions/checkout@v3
    - name: Setup JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: gradle
    - name: Build Android APK and zip
      run: |
        sh ./gradlew assemble
        zip ${{ env.ASSET_FILE_PATH }} \
          ./app/build/outputs/apk/debug/app-debug.apk \
          ./app/build/outputs/apk/release/app-release*.apk

Android build를 위한 JAVA를 설정하고 Gradle의 assemble target을 설정하여 APK file에 대한 빌드를 수행한다. Assemble은 debug와 release용 두개의 apk를 성성하므로 이들을 하나의 zip파일로 만들어서 prebuilt-<version_tag>.zip에 추가한다.

Release에 Asset 추가

Release에 asset을 추가해주는 GitHub action은 보이질 않는다. 그래서 GitHub script를 이용해서 release에 asset을 추가하는 동작을 다음과 같이 정의했다. 이는 두 개의 동작으로 이루어지는데 하나는 주어진 tag로 release 정보를 찾아오는 것이고, 다른 하나는 가져온 release 정보에 파일을 업로드하는 것이다.

동작 1. Tag로 release 가져오기

GitHub script project의 README.md에 따르면 GitHub script상에서 github객체는 사전인증된(pre-authenticated) Octokit client라고 한다. 따라서 새롭게 instance를 만들지 않고 바로 Octokit의 API를 사용할 수 있다. getReleaseByTag()에 tag를 넘겨주면 통해 release에 대한 객체가 반환된다. 그 중에 release를 구분하기 위한 ID만 사용한다.

    // Get a release for given tag.
    const release = await github.rest.repos.getReleaseByTag({
       owner: context.repo.owner,
       repo: context.repo.repo,
       tag: process.env.TAG
    });
    const release_id = release.data.id;
    console.log("Release id for the tag " + process.env.TAG + ": " + release_id);

동작 2. Release에 파일 업로드 하기

앞의 과정에서 만든 업로드할 zip파일과 release를 구분할 ID 정보를 uploadReleaseAsset()에 넘겨 주면 해당 파일이 asset으로 등록된다.

    // Upload release assets.
    const fs = require("fs");
    const filename = process.env.ASSET_FILE_PATH.replace(/^.*[\\/]/, "");
    var uploaded = await github.rest.repos.uploadReleaseAsset({
       owner: context.repo.owner,
       repo: context.repo.repo,
       release_id: release_id,
       name: filename,
       data: await fs.readFileSync(process.env.ASSET_FILE_PATH),
    });
    console.log(
       process.env.ASSET_FILE_PATH
       + " has been uploaded as " + filename
       + " to the release " + process.env.TAG);

주의: upload 동작은 release를 수정하는 것이므로 workflow가 repository에 대한 write permission이 있어야 하므로 settings 항목에서 에서 write permission이 허가되어 있는지 확인한다. 이 부분이 안되어 있다면 403 error가 날 것이다.

Release workflow 실행

GitHub repo의 Code 탭에서 Tags -> Releases -> Tag -> Draft a new release를 선택한 후 Choose a tag를 눌러서 나오는 입력창에 새로 생성할 version tag를 입력해 Create new tag를 선택한 다음 Publish release 버튼을 누른다.

이 때 만들어지는 release에는 소스코드만 들어 있지만, 정상적으로 동작했다면 Actions tab에 release workflow가 등록되어 수행되는 것이 보일 것이다. Workflow가 정상 종료된 후 해당 release로 다시 가보면 asset이 등록되어 있는 것을 볼 수 있다.

Update failed: upgrade-temp-backup 디렉토리를 생성할 수 없습니다.

WordPress에서 몇 번 플러그인 업데이트 실패에 대한 알람이 왔었는데, 오늘에야 살펴보니 자동은 물론 수동 업데이트도 계속해서 실패하고 있었다.

새로 릴리즈된 WordPress 6.3에서 플러그인과 테마를 롤백할 수 있는 기능이 추가되었는데, 이 기능을 위해 wp-content/upgrade-temp-backup/plugins와 wp-content/upgrade-temp-backup/themes 디렉토리를 사용하는 모양이다. (관련 기사 링크)

문제의 원인은 간단했는데, wp-content 디렉토리의 쓰기 권한이 daemon에게 없기 때문에 디렉토리를 만들지 못하고 실패하는 것이었다. 따라서 간단히 디렉토리를 수동으로 만들고 권한을 할당해 주는 것으로 해결되었다.

콘솔에 접근해서 다음과 같이 명령어들을 입력한다.

# WordPress경로로 이동해서
#e.g) cd ~/apps/wordpress/htdocs

# 디렉토리 경로를 만들어주고
mkdir -p ./wp-content/upgrade-temp-backup/plugins
mkdir -p ./wp-content/upgrade-temp-backup/themes

# 소유권 그룹을 할당한 다음
sudo chown bitnami:daemon -R ./wp-content/upgrade-temp-backup

# daemon group에 쓰기 권한을 준다.
chmod 775 -R ./wp-content/upgrade-temp-backup

관리자 화면으로 돌아와서 설치 재시도 하니 잘 동작되었다.

Multi-container app의 Bad Gateway(502) 문제 디버깅

Intel Training and Learning Suite(TLS)는 처음으로 경험해보는 multi-container application이다. 중간에 native개발을 위한 환경 설정을 끼워 넣어 보려고 이것 저것 시도하는 중에 web interface를 담당하는 tls_proxy라는 docker가 Bad Gateway를 띄우면서 문제가 생겼다. 디버깅 하는 과정에서 사용해 본 docker-compose명령어 들의 쓰임새가 유용할 것 같아서 기록으로 남겨 둔다.

Port number와 docker찾기: docker-compose ps

Web browser로 서비스에 접근하면(https://localhost) Bad Gateway(502) error가 발생하는데 처음으로 web-browser의 접속 요청을 받아서 처리하는 docker container는 docker-compose ps 명령어로 docker image별로 listening하는 port number들을 찾을 수 있다.

TLS의 경우는 tls_proxy가 http요청 처리를 위한 80번과 https를 위한 443번 port를 listening하고 있다.

$ docker-compose ps
    Name                  Command               State     Ports                     
------------------------------------------------------------------
tls_apiui      ./webservices/start.sh           Exit 0                                                                                                                                                     
tls_core       ./start.sh                       Up                                                                                                                                                         
tls_mongo      docker-entrypoint.sh --tls ...   Up       127.0.0.1:27017->27017/tcp                                                                                                                        
tls_openvino   /bin/bash                        Exit 0                                                                                                                                                     
tls_proxy      /docker-entrypoint.sh /bin ...   Up       0.0.0.0:443->443/tcp,:::443->443/tcp, 80/tcp                                                                                                      
tls_rabbitmq   docker-entrypoint.sh /init ...   Up       0.0.0.0:15672->15672/tcp,:::15672->15672/tcp, 15691/tcp, 15692/tcp, 0.0.0.0:1883->1883/tcp,:::1883->1883/tcp, 25672/tcp, 4369/tcp, 5671/tcp, 5672/tcp, 0.0.0.0:8883->8883/tcp,:::8883->8883/tcp
tls_redis      docker-entrypoint.sh sh st ...   Up       127.0.0.1:6379->6379/tcp

Docker의 log 보기: docker logs tls_proxy

tls_proxy가 요청을 처리하지 못한 이유는 docker logs <서버 이름>으로 확인할 수 있다.

$ docker logs tls_proxy
2023/05/31 01:31:06 [error] 12#12: *5 connect() failed (113: Host is unreachable) while connecting to upstream, client: 172.31.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://172.31.0.8:3000/", host: "localhost"
...

사설 네트워크인 172.31.0.x로 docker service들을 구성했는데, 그중 인터페이스를 담당하는 172.31.0.1 docker가 172.31.0.8에 요청을 연결해 주어야 하는데 이 부분에서 오류가 나고 있는 모양이다.

Docker network 확인: docker network ls

현재 docker용으로 구성되어 있는 network을 확인해 보면 다음과 같다.

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
d15b28e7a1d5   bridge              bridge    local
acc99b5947d1   cvat_cvat           bridge    local
4485972f8274   host                host      local
6f21c18ba66f   none                null      local
20d132ccfc2a   tls_default         bridge    local

각 네트워크의 구성을 검사(inspect)해 볼 수 있는데 docker network inspect cvat_cvat명령어로 해당 네트워크를 검사해 보면 다음과 같이 위에서 오류가 발생했던 ip인 172.21.0.8번(tls_openvino)이 등록되지 않은 것을 볼 수 있다.

$ docker network inspect cvat_cvat|grep "Name\|IPv4Address"
        "Name": "cvat_cvat",
                "Name": "cvat_redis",
                "IPv4Address": "172.31.0.4/16",
                "Name": "cvat_opa",
                "IPv4Address": "172.31.0.3/16",
                "Name": "cvat_db",
                "IPv4Address": "172.31.0.5/16",
                "Name": "traefik",
                "IPv4Address": "172.31.0.2/16",
                "Name": "cvat",
                "IPv4Address": "172.31.0.6/16",
                "Name": "cvat_ui",
                "IPv4Address": "172.31.0.7/16",
                "Name": "tls_proxy",
                "IPv4Address": "172.31.0.9/16",

Docker의 log 보기: docker logs tls_openvino

문제가 되고 있는 docker container를 특정했으니 docker logs tls_openvino명령어로 다시 한번 해당 container의 로그를 확인해 본다.

error: A hook (`userconfig`) failed to load!
error: Failed to lift app: Error: Attempted to `require('/home/tls/webservices/apiserver/config/env/production.js')`, but an error occurred:
--
Error: ENOENT: no such file or directory, open '../../thirdparty/security/ca/ca_certificate.pem'
    at Object.openSync (fs.js:498:3)
    at Object.readFileSync (fs.js:394:35)
    at Object.<anonymous> (/home/tls/webservices/apiserver/config/env/production.js:70:15)

../../third party/security/ca/ca_certificate.pem파일을 읽지 못해서 오류가 발생했다고 나오는데, 실제로 container에서는 해당 파일의 위치를 상대 경로로 접근하지 않는다. Call stack에 나와 있는 production.js line 70근처의 내용을 보면 먼저 절대경로인 /run/secrets/*에 접근을 시도할 때 예외가 발생해서 상대 경로로 접근을 시도하고(개발용 코드로 추정) 이 마저도 실패한 것이 로그에 나온 것이다.

apiserver/config/env/production.js

try {
  tls_ca = fs.readFileSync("/run/secrets/ca_tls")
  tls_server_cert = fs.readFileSync("/run/secrets/tlsserver_cert")
  tls_server_key = fs.readFileSync("/run/secrets/tlsserver_key")
} catch (err) {
  tls_ca = fs.readFileSync("../../thirdparty/security/ca/ca_certificate.pem");
  tls_server_cert = fs.readFileSync("../../thirdparty/security/TLS_server_cert.crt");
  tls_server_key = fs.readFileSync("../../thirdparty/security/TLS_server_key.pem");
}

정리하자면, 문제는 server secret을 생성하는 과정에서 발생한 것이 docker image상의 key file path read에서 오류를 발생시키고 tls_openvino container가 제대로 뜨지 못한 문제이다.

해결(?)

문제의 원인은 생각보다 싱거웠는데, docker-compose 명령어를 실행시키는 native용 script에서 다음 줄을 연결하기 위한 \를 빼먹는 사소한(!) Bash 문법 오류가 발생 했었고, 이 오류가 무시된 채로 docker-compose build 명령어가 계속 수행되어 service가 up되는 상황까지 된 것이었다.

-       && pip install -U pip wheel setuptools
+       && pip install -U pip wheel setuptools \
        && pip install \

위의 수정과 함께 오류가 생기면 build를 멈추도록 set -e를 bash script에 추가 하는 것으로 이 문제는 일단락 되었다.

C++ template 선언과 구현을 서로 다른 파일에 나눌 수 없다

커스텀 Queue 클래스를 작성하는데 queue의 item은 추후에 변경 될 수 있으므로 템플릿으로 작성하고자 한다. Queue class를 header에 정의하고 구현을 cpp 파일에 작성해 준다음 main 함수에서 다음과 같이 queue를 생성하고 컴파일을 시도한다.

다음은 해당 template을 사용하는 caller(main 함수)이다.

//
// C++ template instantiation test.
//
//                                             - litcoder

#include <iostream>
#include "queue.h"

using namespace std;
int main() {
    // Create an integer type queue.
    queue<int> q;

    // Push elements from the queue.
    q.push(0);
    q.push(1);
    q.push(2);

    // Pop and print all elements.
    while (! q.empty()) {
        cout << "Popped: " << q.pop() << endl;
    }

    return 0;
}

하지만 별 문제 없을것이라는 생각과 달리 안타깝게도 빌드는 실패한다. 공들여서 만든 method가 링커에 의해 하나도 빠짐없이 “undefined reference” 오류를 뱉으면서 실패한다.

$ make
g++  -g -Wall  -c -o queue.o queue.cpp
g++  -g -Wall  -c -o main.o main.cpp
g++ -o qtest queue.o main.o
/usr/bin/ld: main.o: in function `main':
/home/litcoder/Downloads/cpptemplate/main.cpp:12: undefined reference to `queue<int>::queue()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:15: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:16: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:17: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:21: undefined reference to `queue<int>::pop()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:20: undefined reference to `queue<int>::empty()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()'
collect2: error: ld returned 1 exit status
make: *** [Makefile:7: qtest] Error 1

이 문제에 대한 해결책은 생각보다 간단한데, cpp파일에 따로 분리했던 구현 부분을 선언들과 함께 header file에 두는 것이다. (해치웠나?)

$ make
g++  -g -Wall  -c -o queue.o queue.cpp
g++  -g -Wall  -c -o main.o main.cpp
g++ -o qtest queue.o main.o
$ ./qtest
Popped: 0
Popped: 1
Popped: 2

왜 undefined reference 오류가 났을까?

Queue.o가 빌드됐으니 거기 보면 분명히 저 method들이 선언되어 있을텐데 말이다. 빌드된 symbol들을 살펴보자,

$ objdump -t ./queue.o

./queue.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 queue.cpp

O.O 없다. 아무것도. queue.o는 아무 symbol이 없는 빈 파일이었다.

앞서 해결됐다고 했던 header에서 template 선언과 구현을 모두 수행하는 경우에도 queue.o의 symbol table이 비어 있는것은 마찬가지 이지만, 이 경우, caller인 main.o에는 template이 실제 사용하는 type에 binding되어 embed되어 있다. main.o의 symbol들을 살펴보면 다음과 같이 mangle된 symbol들을 볼 수 있다.

$ objdump -t ./main.o

./main.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 main.cpp
...
0000000000000000  w    F .text._ZN5queueIiEC2Ev	0000000000000048 _ZN5queueIiEC1Ev
0000000000000000  w    F .text._ZN5queueIiE4pushEi	000000000000003f _ZN5queueIiE4pushEi
...

참고로, C++ symbol의 type은 demangler를 이용하면 볼 수 있는데, demangle한 결과는 다음과 같다.

_ZN5queueIiEC2Ev -> queue<int>::queue()
_ZN5queueIiE4pushEi -> queue<int>::push(int)

결론

C++ template은 빌드하는 시점에서 어떤 data type으로 binding될 지 알 수 없기 때문에 구현 부분을 별도의 파일로 만들어 봤자 아무 것도 빌드 되지 않는다. Header file에 선언과 구현을 함께 두고 이 template을 사용하는 code가 이를 include하여 어떤 data type으로 binding될 지 결정해 주면 그때서야 실제 symbol이 caller module에 포함된다.

즉, template을 선언과 구현으로 나누어서 따로 빌드하는 것은 불가능할 뿐더러 의미도 없으니 include될 하나의 파일(주로 header)에 넣어서 선언과 구현을 합쳐두는게 가장 좋다.