2026년 10월 27일 화요일
미단재

AI 12년 된 CI3 코드를 CI4로 옮기며 버린 것과 남긴 것

작성자 웅파 작성일 2026-10-27 15:39:34 조회수 5

2026년 10월 27일 · @martin

들어가며 — 12년 된 코드를 다시 열었다

서랍 깊숙이 CI3로 만든 라이선스 관리자가 있었다. 만든 지 12년, 키 생성은 C 모듈이 맡고 관리 화면은 CI3가 맡는 구조다. 이 코드를 CI4로 옮겨 AIvance의 라이선스 매니저 AILicet으로 다시 내놓는 중이다.

CI Korea에서 가장 자주 받는 질문이 "CI3 코드, 지금 옮겨도 되나요?"다. 그래서 이번에는 내 코드로 답해보려 한다. 기준은 단순했다. 프레임워크에 기대던 것은 버리고, 도메인에 기대던 것은 남긴다.

지금 옮기는 이유는 세 가지다.

  • 라이선스 생성·검증 로직은 프레임워크 의존도가 낮다. 옮기는 비용이 생각보다 작다.
  • CI4 공통 기반(ci4-board: JWT, 소셜로그인, API 버저닝, 통합테스트 149개)이 이미 있다. 빈 땅에 짓는 게 아니다.
  • 해외 LaaS(Licensing-as-a-Service) 서비스가 채우지 못한 국내 수요가 보였다. 이 이야기는 뒤에서 다룬다.

원칙 하나 — 한 번에 하나만 바꾼다

오래된 코드를 옮길 때 가장 큰 유혹은 "이참에 MSA로"다. 나도 MSA 백엔드를 주로 해온 사람이라 그 마음을 안다. 하지만 AILicet은 모놀리스 그대로 포팅하기로 했다.

프레임워크 전환과 아키텍처 전환을 동시에 하면, 문제가 생겼을 때 원인을 가르기 어렵다. CI4의 동작 차이인지, 서비스 간 통신 문제인지, 원래 있던 버그인지 알 수 없게 된다. 변수를 하나씩 바꿔야 디버깅이 가능하다.

그래서 순서를 이렇게 잡았다.

  1. 같은 구조, 같은 동작으로 CI4에 옮긴다.
  2. 테스트로 "예전과 같이 동작한다"를 고정한다.
  3. 그 다음에 키 생성 방식을 바꾼다.
  4. 서비스 분리는 실제로 다른 제품이 호출하기 시작할 때 따진다.

분리를 미루어도 경계는 지금 긋는다. 라이선스 로직을 Service 레이어에 모아두면, 나중에 떼어낼 때 잘라야 할 자리가 이미 보인다. 서비스 분리보다 먼저 같은 프로세스 안의 논리적 분리로 풀 수 있는지 확인하는 것, 이게 내가 제품 여럿을 혼자 운영하며 얻은 기준이다.

버린 것

1. CI3식 절차적 구조

$this->load->library()로 불러오고, get_instance()로 어디서나 슈퍼 객체에 접근하던 방식은 버렸다. CI3에서는 편했지만, 무엇이 무엇에 기대는지 코드에 드러나지 않는다. 테스트를 쓰려고 하면 그제서야 보인다.

CI4에서는 네임스페이스 클래스로 나누고, 의존성은 Config\Services로 주입한다. 컨트롤러는 요청과 응답만 다루고, 라이선스 로직은 Service로 내린다. 파일을 옮기는 일이 아니라 책임을 다시 나누는 일이다.

2. 알고리즘을 숨겨서 지키는 키 생성

C 모듈로 키 생성 구조를 감싼 건 12년 전에는 합리적인 선택이었다. 하지만 알고리즘을 숨기는 방식은 한 번 드러나면 끝이다. 키 생성기가 복제되면 이미 발급한 키를 전부 바꿔야 한다.

그래서 방향을 공개키 서명(RSA/ECDSA) 방식으로 돌렸다. 비밀은 알고리즘이 아니라 서버의 개인키에 둔다. 클라이언트는 공개키만 갖고 서명을 검증하니, 인터넷이 없는 현장에서도 검증할 수 있다. 라이선스 키 하나가 새어 나가도 그 키만 폐기하면 되고, 생성 원리가 드러날 일은 없다.

3. "모든 제품에 라이선스 키를 붙이겠다"는 생각

이건 코드가 아니라 생각을 버린 사례다. CNC·목공 공방용 견적 플랫폼 AILign을 설계할 때, 처음에는 라이선스 키 발급과 도메인 바인딩, 주기 검증까지 스펙에 넣었다.

그런데 AILign은 내가 서버 하나로 운영하는 호스팅형이다. 코드가 고객 서버로 나가지 않으니 키도, 바인딩도, 원격 검증도 필요 없었다. 결제가 끝나면 사용자별 권한 레코드를 만들고, 필터 하나가 접근을 막는 entitlement 방식으로 줄였다.

여기서 AILicet의 자리가 분명해졌다. 라이선스 서버는 코드가 고객 손에 넘어가는 소프트웨어를 위한 것이다. 설치형 솔루션, 온프레미스 납품물, 데스크톱 앱이 그렇다.

남긴 것

1. CodeIgniter 그 자체

프레임워크를 아예 바꿀 수도 있었다. 그래도 CI4에 남았다. AIvance 제품은 전부 CI4와 PHP 8 위에 있고, 인증·결제·에러 규약을 같은 뼈대에서 재사용한다. 혼자 제품 여럿을 운영하는 사람에게는 프레임워크 하나를 깊게 아는 게 어떤 신규 기능보다 크다.

CI3를 써본 사람에게 CI4는 낯선 프레임워크가 아니다. 라우팅, 모델, 뷰의 감각은 그대로고, 네임스페이스와 서비스 로케이터가 더해졌을 뿐이다.

2. 라이선스 도메인 로직

발급, 활성화, 검증, 만료라는 흐름과 그 안의 규칙은 남긴다. 이건 CI3에 맞춘 코드가 아니라 12년 동안 실제 고객을 상대하며 다듬은 업무 규칙이다. 프레임워크를 바꾼다고 버릴 이유가 없다.

로직 자체는 프레임워크 의존도가 낮아서 대부분 그대로 옮겨진다. 바뀌는 건 자리다. Library나 Helper에 흩어져 있던 코드를 Service 클래스 하나로 모은다.

3. 이미 나간 키에 대한 책임

키 생성 방식을 바꾼다고 예전 키를 하루아침에 무효로 만들 수는 없다. 라이선스는 고객과의 약속이다. 구 방식 검증기는 레거시 어댑터로 남겨 새 키와 병행 검증하고, 갱신 시점에 서명 방식 키로 교체하는 게 맞다고 본다.

4. 모든 제품이 공유하는 규약

AILicet도 다른 제품과 같은 규칙을 따른다. Controller–Service–Repository 레이어, RFC 9457 Problem Details 기반 에러 응답, PHPStan과 PHPUnit을 돌리는 GitHub Actions까지 그대로다. 새로 옮긴 코드도 나머지와 같은 기준으로 검증받는다.

한눈에 보는 마이그레이션 지도

CI3를 쓰던 분들이 실제로 손댈 자리를 표로 정리하면 이렇다.

CI3에서

CI4에서

비고

$this->load->library('license')

service('license') 또는 Config\Services 등록

의존성이 한곳에 모인다

get_instance()로 슈퍼 객체 접근

생성자·서비스 로케이터로 주입

테스트에서 가짜 객체로 바꾸기 쉽다

application/libraries, helpers

app/Services, app/Libraries 네임스페이스 클래스

라이선스 로직은 Service 하나로

모델에 섞인 비즈니스 규칙

Service(규칙) + Model/Repository(조회·저장)

컨트롤러는 얇게

C 모듈 키 생성

서버 개인키 서명 + 클라이언트 공개키 검증

오프라인 검증 가능

제각각인 에러 응답

RFC 9457 Problem Details

다른 제품과 같은 규약

왜 Keygen을 쓰지 않고 국내형을 만드나

라이선스 서버는 이미 잘 만든 해외 서비스가 있다. Keygen은 라이선스 키, 기기 활성화, 사용자 관리를 개발자용 API로 제공한다. Cryptolens, LicenseSpring도 같은 LaaS 시장에 있다. 기능만 보면 굳이 새로 만들 이유가 없다.

그런데 국내 중소 솔루션사 입장에서 보면 걸리는 곳이 기능이 아니다.

기준

해외 LaaS (Keygen 등)

AILicet이 노리는 자리

가격·결제

달러 월정액, API 요청량 기준 티어, 해외 카드 결제

원화 가격, 토스페이먼츠 등 국내 결제

증빙

해외 인보이스

국내 거래 증빙 처리

언어·지원

영문 대시보드·문서

한국어 대시보드, 한국어 지원

주 고객

글로벌 판매 데스크톱·SaaS 벤더

온프레미스 납품이 많은 국내 SI·솔루션사

기술 스택

언어 무관 REST API

같은 API + PHP/CI4 연동 예제

특히 마지막 줄이 내가 잘 아는 시장이다. 국내에는 PHP와 CodeIgniter로 만든 솔루션을 고객사 서버에 설치해 납품하는 회사가 많다. 이들에게 필요한 건 거창한 플랫폼이 아니라, 납품한 코드가 계약 범위 밖에서 돌지 않게 하는 장치다.

시장이 크지 않다는 것도 안다. 그래서 순서를 정했다.

  1. AIvance 제품군 중 설치형으로 나가는 것에 먼저 붙여 내가 첫 고객이 된다.
  2. AI바우처·혁신바우처 같은 정부 IT 바우처 공급기업 활동에서 만나는 SI사에 부가 상품으로 제안한다.
  3. 수요가 확인되면 독립 SaaS로 판다.

반대로 처음부터 해외에 파는 제품이라면 Keygen을 쓰는 게 맞다. AILicet은 Keygen을 이기려는 제품이 아니라, Keygen이 닿지 않는 자리를 채우려는 제품이다.

라이선스 서버를 만들며 정한 원칙

라이선스는 고객을 막는 장치이기 전에 고객과의 약속이다. 그래서 기술보다 정책을 먼저 정했다.

  • 응답은 서명해서 보낸다. 서버는 개인키로 검증 결과에 서명하고, 클라이언트는 공개키로 확인한다. 응답을 가로채 "유효함"으로 바꿔치는 공격을 막는다.
  • 네트워크가 끊겨도 바로 잠그지 않는다. 검증에 실패하면 유예기간(초안 기준 14일)을 두고, 그 뒤에 기능을 잠근다. 망분리 환경이나 일시 장애로 고객 업무가 멈추면 안 된다.
  • 데이터를 인질로 잡지 않는다. 구독이 끝나면 읽기 전용으로 바꾼다. 신규 작성은 막아도 기존 데이터 조회는 남긴다.
  • 차감·발급은 멱등하게. 결제 웹훅이 두 번 와도 키가 두 번 발급되면 안 된다. 멱등키와 트랜잭션으로 묶는다.

API는 인증 API인 AITessera의 패턴을 그대로 가져와 이렇게 잡았다.

POST /v1/licenses/activate   # 키 + 설치 대상 → 활성화
POST /v1/licenses/verify     # 주기 검증 (서명된 응답)
POST /v1/webhooks/payment    # PG 웹훅 → 키 발급·구독 갱신

제품 하나를 만들 때마다 인증, 결제, 에러 규약을 새로 만들지 않는다. 혼자 여러 제품을 운영하려면 이게 유일한 방법이다.

CI3 코드를 옮기려는 분들께

CI3 프로젝트를 앞에 둔 분이라면 이 순서로 점검해보길 권한다.

  • 이번에 바꿀 것을 하나로 정했는가 (프레임워크 or 아키텍처)
  • 프레임워크에 기대던 코드와 업무 규칙을 구분해 목록으로 만들었는가
  • 기존 동작을 고정하는 테스트를 먼저 썼는가
  • get_instance()와 $this->load 사용처를 전부 찾았는가
  • 비즈니스 규칙이 들어갈 Service의 경계를 정했는가
  • 이미 고객에게 나간 데이터·키·URL의 호환 계획이 있는가
  • 옮긴 코드도 PHPStan·PHPUnit CI를 통과하는가

맺으며

12년 된 코드를 열어보면 부끄러운 곳이 많다. 하지만 그 코드가 12년 동안 지켜온 규칙은 부끄러운 게 아니다. 마이그레이션은 그 둘을 가려내는 작업이었다.

버린 건 대부분 그 시절 프레임워크와 보안 관행에 맞춘 방식이었고, 남긴 건 고객과 맺은 약속이었다. CI3 코드 앞에서 망설이는 분들께 이 기준이 도움이 되면 좋겠다. 

댓글 0개