레거시 .NET을 Spring Boot로 전환을 위한 AI Rules & Skills 적용기

2026-06-16

사내 레거시 시스템(ASP.NET WebForms)을 Spring Boot로 전환하는 프로젝트를 진행하면서, AI 코딩 에이전트(Claude Code)를 그냥 쓰지 않고 “규칙(Rules)과 스킬(Skills)” 을 사용한 경험을 정리한다.

배경

전환 대상은 십수 년 된 ASP.NET 3.5 WebForms(.aspx + .aspx.cs, code-behind) 기반 관리자 시스템이었다. 핵심 제약은 다음과 같았다.

  • DB의 Stored Procedure(SP)는 그대로 유지한다. 비즈니스 로직 상당수가 SP에 들어 있고, 운영 중인 다른 시스템도 같은 SP를 공유하므로 함부로 못 바꾼다.
  • 따라서 JPA로 도메인을 재설계하는 게 아니라, SP 호출 구조로 Java 레이어만 새로 올린다.
  • DB가 여러 개(공통/로그/결제 등)로 분리된 멀티 데이터소스 환경이다.

즉 “새로 잘 만들기”가 아니라 “레거시 동작을 그대로 보존하면서 언어/프레임워크만 바꾸기” 가 목표였다.

그런데 진짜 어려운 건 기술이 아니라 “아무도 정답을 모른다”는 점이었다. 이 시스템은 오래전 외주로 제작됐는데,

  • 초기에는 형상관리(버전 관리)가 되지 않아 변경 이력이 남아 있지 않았고,
  • 기획서·설계 문서가 분실되어 “원래 어떻게 동작해야 하는지”를 적어둔 출처가 없었으며,
  • 심지어 기획자·운영자조차 각 기능이 어떤 의도로 만들어졌는지 모른 채 관성적으로 쓰고 있었다.

물어볼 사람도, 참고할 문서도 없었다. 유일하게 신뢰할 수 있는 명세는 “지금 돌아가는 레거시 소스와 SP” 그 자체였고, 이 사실이 뒤에 나오는 모든 원칙 — “추측하지 말고 원본을 확인하라”, “기능을 문서로 남겨라” — 의 출발점이 됐다.

이렇게 양이 많고 반복적인 작업은 AI 에이전트와 궁합이 좋아 보였지만, 막상 그냥 시키면 문제가 많았다.

AI를 “그냥” 쓰면 생기는 문제

처음엔 화면 하나를 통째로 던지고 “이거 자바로 전환해줘” 했다. 결과는 그럴듯했지만 실무에 쓰기엔 위험했다.

  1. 추측(환각) — 존재하지 않는 SP명을 그럴듯하게 지어내거나, 파라미터 순서를 임의로 채워 넣는다.
  2. 과설계 — 시키지도 않은 추상화 계층, 범용 유틸, 옵션 플래그를 만들어낸다.
  3. 일관성 붕괴 — 같은 프로젝트인데 컨트롤러마다 응답 포맷·예외 처리·페이징 방식이 제각각이 된다.
  4. 주석 처리된 죽은 기능까지 전환 — 레거시 .aspx<!-- ... -->로 막아둔 버튼을 멀쩡한 기능으로 되살린다.

결국 “AI가 마음대로 판단하는 영역”을 좁히는 게 핵심이라는 결론에 도달했다. 그래서 Rules(항상 지켜야 하는 제약)Skills(정해진 절차를 밟는 작업 단위) 두 축으로 에이전트를 구성했다.

1. Rules — 판단 영역을 좁히는 제약

.claude/rules/ 아래에 주제별로 규칙 파일을 나눠 두고, 각 파일 상단의 프론트매터로 적용 범위(paths) 를 제한했다. 모든 규칙을 항상 들이미는 게 아니라, 작업 대상 파일에 맞는 규칙만 적용되게 한 것이 포인트다.

---
paths:
  - "src/**"
---
# 추측 금지 규칙
모호하면 추측하지 말고 질문한다. 코딩 시작 전에 가정을 먼저 명시한다.
...

paths로 적용 범위를 나누면 좋은 점

처음엔 “규칙은 많을수록 좋다”고 생각해 한 파일에 다 몰아넣었는데, 오히려 역효과였다. 규칙을 파일별로 쪼개고 paths로 적용 대상을 한정하니 다음 효과가 있었다.

  1. 컨텍스트 절약 — 규칙도 결국 토큰이다. 테스트 코드를 짤 때 .NET 전환 매핑 표나 UI 컨벤션까지 전부 읽힐 필요는 없다. paths로 “지금 만지는 파일에 해당하는 규칙”만 로드되니, 같은 컨텍스트 예산으로 정작 중요한 원본 소스·SP 정의에 더 많은 자리를 내줄 수 있었다.

  2. 규칙 간 충돌·오적용 방지 — 가장 컸던 효과다. 예를 들어 .NET→Java 전환 규칙(decimal → Integer, Response.Redirect → redirect:)은 레거시 소스 경로에서 작업할 때만 의미가 있다. 이걸 신규 Java 코드에까지 들이밀면 엉뚱한 변환이 끼어든다. paths로 전환 규칙은 레거시 경로, 테스트 규칙은 src/test/**, 컨트롤러 규칙은 controller/** 로 못 박으니 “이 규칙이 여기서도 적용되나?”를 에이전트가 헷갈릴 일이 없어졌다.

  3. 규칙의 책임이 명확해짐 — 한 파일 = 한 관심사(네이밍, 에러 처리, 테스트 …). 규칙을 고칠 때 어디를 손대야 할지 바로 보이고, “이 규칙이 왜 여기 있지?” 하는 죽은 규칙이 줄었다.

  4. 점진적 도입이 쉬움 — 새 규칙을 추가할 때 paths를 좁게 잡아 특정 영역에만 시범 적용하고, 안정되면 범위를 넓히는 식으로 부작용 없이 굴릴 수 있었다.

대략 이런 매핑으로 운용했다.

paths 적용되는 규칙 의도
항상(스코프 없음) 추측 금지 모든 작업의 대전제
(레거시 소스)/** .NET→Java 전환 매핑 신규 Java엔 전환 규칙이 끼어들지 않게
*.java 네이밍·에러 처리·코드 컨벤션 자바 전반
controller/** URL 매핑·PRG·공통 모델 컨트롤러 한정
src/test/** 테스트 컨벤션 테스트 코드 한정
templates/**, static/** UI 컨벤션 뷰 한정

요약하면, paths는 “규칙을 끄고 켜는 스위치”가 아니라 “규칙이 엉뚱한 곳에 새지 않게 막는 칸막이” 였다. 규칙을 늘리면서도 에이전트의 혼란이 늘지 않은 건 이 칸막이 덕분이다.

실제로 운용한 규칙들을 성격별로 묶으면 이렇다.

규칙 한 줄 요약 적용 범위
추측 금지 모호하면 질문, 가정은 표면화 항상
네이밍 DTO/클래스/메서드 명명 규칙 *.java
에러 처리 전역 예외 + 에러코드 체계 *.java
컨트롤러/뷰 URL 매핑, PRG 패턴, 공통 모델 controller/**
레이어 패턴 레이어별 코드 패턴 + 멀티 DataSource *.java, *.html
.NET→Java 전환 타입/문법/아키텍처 매핑 (레거시 소스)/**
테스트 컨벤션 MockMvc / E2E / 단위 테스트 규칙 src/test/**
목표 기반 실행 검증 가능한 성공 기준 정의 src/**

가장 효과가 컸던 두 가지만 자세히 적는다.

추측 금지 규칙

“애매하면 멈추고 번호 붙여서 한 번에 질문한다”를 명문화했다. 그리고 추측해도 되는 것 / 절대 추측하면 안 되는 것을 분리했다.

  • ❌ 추측 금지: 테이블 간 관계(FK/JOIN 조건), 비즈니스 검증 규칙, 환경별 설정값, 토글에 없는 기능
  • ✅ 추측 허용: 패키지 구조·네이밍·에러 처리 (이미 별도 규칙으로 정의돼 있으니까)

핵심은 “규칙으로 정해둔 건 알아서 하고, 정보가 필요한 건 절대 지어내지 마라“는 경계선을 명확히 그은 것이다.

.NET → Java 전환 매핑

전환 작업의 결정 비용을 줄이려고 매핑 표를 규칙으로 박아 두었다. (아래는 일반적인 매핑 예시)

.NET Java
string / int? String / Integer
DateTime LocalDateTime
LINQ .Where().Select() Stream .filter().map()
string.IsNullOrEmpty(x) !StringUtils.hasText(x)
.aspx + .aspx.cs Controller + Thymeleaf 템플릿
code-behind 비즈니스 로직 Service 레이어로 이동
SP 호출(다중 결과셋) 공통 SpExecutor 유틸로 통일
Response.Redirect() return "redirect:/url" (PRG)

그리고 전환 절차의 1번을 “원본 분석 — 주석 처리된 기능은 구현 대상에서 제외”로 못 박았다. 죽은 페이지를 되살리는 문제는 이 한 줄로 거의 사라졌다.

2. Skills — 정해진 절차를 밟게 만들기

규칙이 “제약”이라면 스킬은 “이 작업은 반드시 이 순서로 하라“는 절차다. 특정 발화(트리거)에 반응해 활성화된다.

스킬 역할
dotnet-to-java 레거시 화면 한 개를 Java로 전환하는 전체 절차
check-sp SP의 존재/파라미터/결과셋을 DB에서 직접 확인 (+ 캐시)
write-test 테스트케이스 문서 기반 MockMvc/E2E 테스트 생성
git-commit 변경 분석 → 컨벤션에 맞는 커밋 메시지 작성
karpathy-guidelines LLM 코딩 안티패턴(과설계·광범위 수정) 억제

dotnet-to-java — 전환의 메인 파이프라인

이 스킬이 강제하는 순서가 프로젝트의 뼈대다.

1. 원본 분석   — .aspx / .aspx.cs를 읽어 실제 호출되는 SP 목록 확정
                 (주석 처리된 코드의 SP는 제외)
2. SP 정의 확인 — check-sp 스킬로 파라미터·결과셋 컬럼 확인
                 (SP 미확인 상태에서 Mapper 생성 금지)
3. 규칙 확인   — 전환/레이어/네이밍/에러 규칙 로드
4. 파일 생성   — DTO → Mapper → XML → Service → Controller → 템플릿
5. 검증        — 체크리스트로 자기 점검

여기서 배운 가장 중요한 교훈: “분석 → 확인 → 생성” 순서를 어기지 못하게 만드는 것. 에이전트는 방치하면 곧장 코드부터 쓴다. “원본을 먼저 읽고, SP를 DB에서 확인한 다음에야 코드를 쓴다”를 절차로 강제하니 환각이 급감했다.

check-sp — “추측 금지”를 현실화하는 장치

추측 금지 규칙이 선언이라면, check-sp는 그걸 실제로 가능하게 하는 도구다. SP명을 받으면 DB 메타데이터를 직접 조회해 파라미터 순서·타입·방향, 결과셋 컬럼을 가져온다. 그리고 조회 결과를 마크다운 캐시 파일에 적재해 다음부터는 DB를 다시 치지 않는다.

요청:  "USP_SAMPLE_LIST 프로시저 확인해줘"
동작:  1) 캐시 인덱스 확인 → 있으면 캐시 히트
       2) 없으면 DB 조회 → 파라미터/결과셋 추출
       3) 캐시에 append + 인덱스 갱신
결과:  파라미터(순서·타입·IN/OUT) + 결과셋 컬럼 표

⚠️ 실제 스킬에는 DB 접속 정보가 들어가는데, 접속 정보는 절대 스킬 파일에 평문으로 두면 안 된다. 환경변수나 시크릿 매니저로 분리하는 것을 권한다. (이 글의 예시에서는 전부 생략했다.)

이 캐시 덕분에 “SP 파라미터를 지어내는” 문제가 사라졌고, 뒤에 나오는 테스트의 “골든 시그니처” 근거 데이터로도 재사용됐다.

write-test — 테스트까지 절차화

테스트케이스 문서(TC-XX-YY 형식)를 기준으로 MockMvc / E2E 테스트를 생성하되, 같은 TC를 두 방식에 중복 배정하지 않기, 클래스 상단 Javadoc에 담당 TC 명시, 메서드명 TC_XX_YY_한글설명 같은 세부 컨벤션까지 스킬에 박아 두었다.

여기서 가장 마음에 들었던 장치가 SP 골든 시그니처 테스트다.

SP는 파라미터를 위치(인덱스)로 바인딩하기 때문에, Java가 만든 파라미터 배열의 순서·개수·타입이 SP 정의와 어긋나면 엉뚱한 컬럼에 값이 들어간다. 그런데 이건 목(mock) 테스트로는 안 잡힌다. 그래서 check-sp가 확인한 SP 시그니처를 “정답”으로 테스트에 박아 자동 대조했다.

// SP 캐시에서 확인한 파라미터 타입 순서 = 정답(골든)
private static final String[] INSERT_TYPES = { "CHAR", "VARCHAR", "TINYINT", /* ... */ };

@Test
@DisplayName("TC-XX-NN: INSERT 파라미터 개수·타입이 SP 시그니처와 정합")
void insert_시그니처_정합() {
    service.insertSample(fullDto(), "tester");
    Object[] actual = captureParams(SP_INSERT);     // 서비스가 만든 파라미터 배열 캡처

    assertThat(actual).hasSize(INSERT_TYPES.length); // 개수
    for (int i = 0; i < INSERT_TYPES.length; i++) {
        if (actual[i] == null) continue;
        boolean numeric = switch (INSERT_TYPES[i]) {
            case "INT", "TINYINT", "SMALLINT", "BIGINT" -> true;
            default -> false;
        };
        assertThat(actual[i]).isInstanceOf(numeric ? Number.class : String.class);
    }
}
  • 잡는 것: 파라미터 빠뜨림/추가, 숫자↔문자 위치 뒤바뀜
  • 못 잡는 것: 같은 타입끼리 뒤바뀜(VARCHAR↔VARCHAR) → 실 DB 통합 테스트로 보완
  • 한계: 캐시가 stale이면 골든도 틀림 → SP 변경 시 check-sp 재조회 필수

karpathy-guidelines — 과설계 억제

Andrej Karpathy가 정리한 LLM 코딩 함정을 행동 지침으로 옮긴 스킬이다. 요지는 네 가지.

  1. 코딩 전에 생각 — 가정 명시, 모호하면 질문
  2. 단순함 우선 — 요청 이상의 기능/추상화 금지
  3. 외과수술적 변경 — 시킨 것만 건드리고, 옆 코드 “개선” 금지
  4. 목표 기반 실행 — “동작하면 끝”이 아니라 검증 가능한 성공 기준 정의

“리팩토링해줘 → 리팩토링 전후 테스트가 동일하게 통과함을 확인한다” 처럼 모호한 요청을 검증 가능한 목표로 바꾸는 관점이 특히 유용했다.

실제 워크플로우 예시 (익명화)

화면 하나를 전환할 때 실제로 이렇게 흘러갔다.

나:   "샘플 목록 화면 자바로 전환해줘" + 레거시 파일 경로

AI:   [dotnet-to-java 스킬 활성화]
      1) .aspx / .aspx.cs 분석
         - 목록 조회 BindData() → USP_SAMPLE_LIST 호출 확인
         - 하단 '엑셀 다운로드' 버튼은 <!-- 주석 --> → 전환 제외
      2) [check-sp] USP_SAMPLE_LIST 조회
         → 파라미터(검색조건 5개), 결과셋(목록 + 전체건수) 확인
      3) 규칙 로드 (전환/레이어/네이밍/에러)
      4) DTO → Mapper → Service → Controller → 템플릿 생성
      5) 체크리스트 자기점검 (readOnly 트랜잭션, 페이징, 메뉴 활성화 …)

생성된 Service는 대략 이런 모양이 된다. (익명 예시)

@Service
@RequiredArgsConstructor
public class SampleService {

    private final SpExecutor conSpExecutor;   // DB별 SpExecutor 빈

    @Transactional(value = "conTransactionManager", readOnly = true)
    public PagedResult<SampleVo> getSamples(SampleSearchDto dto) {
        // .NET: ST_DT.Replace("-", "")
        String stDt = StringUtils.hasText(dto.getStDt()) ? dto.getStDt().replace("-", "") : "00000000";
        String edDt = StringUtils.hasText(dto.getEdDt()) ? dto.getEdDt().replace("-", "") : "99999999";

        SpResult r = conSpExecutor.execute("USP_SAMPLE_LIST", dto.getKeyword(), stDt, edDt, dto.getPageIdx());
        List<SampleVo> list = r.getList(0, SampleVo.class);   // ds.Tables[0]
        int total = r.getInt(1, "TOTAL_CNT");                 // ds.Tables[1]
        return PagedResult.of(list, total, dto.getPageIdx());
    }
}

핵심은 결과셋 인덱스(getTable(0), getTable(1))를 레거시 ds.Tables[n] 순서와 똑같이 맞추는 것 — 이걸 추측하지 않고 원본에서 확인하도록 절차로 강제한 게 안정성의 8할이었다.

효과를 본 핵심 장치 정리

돌아보면 잘 먹힌 건 화려한 게 아니라 단순한 원칙들이었다.

  1. 추측 금지 + 확인 도구의 짝 — “지어내지 마라”는 선언만으론 부족하다. check-sp처럼 사실을 확인할 도구를 같이 줘야 실제로 안 지어낸다.
  2. 순서를 강제 — 분석 → 확인 → 생성. 에이전트가 곧장 코드부터 쓰지 못하게 막는 것만으로 환각이 크게 줄었다.
  3. 규칙의 적용 범위 제한(paths) — 전체 규칙을 항상 들이밀지 않고 대상 파일에 맞는 것만 적용 → 컨텍스트 절약 + 충돌 감소.
  4. 자기 점검 체크리스트 — 작업 끝에 스스로 검증하게 하니 누락이 줄었다.
  5. 캐시 — 같은 SP를 반복 조회하지 않게 해 속도와 일관성을 동시에 챙겼다.

잘 맞았던 점 — 작은 단위로 쪼개 작업하기

아쉬운 점을 적었지만, 메뉴(화면) 단위로 잘게 쪼개 진행한 방식 자체는 AI 협업과 궁합이 아주 좋았다.

  • AI 생성이 간결해졌다. 화면 하나 = 한 작업으로 범위가 명확하니, AI가 한 번에 다뤄야 하는 컨텍스트가 작았다. 원본 파일 한 쌍(.aspx + .aspx.cs)과 관련 SP 정의만 보면 되므로 결과물이 산만해지지 않고, 환각이 끼어들 여지도 줄었다.
  • 리뷰가 가벼웠다. PR/변경 단위가 화면 하나로 떨어지니 리뷰어가 봐야 할 양이 적었다. “이 화면이 레거시와 같게 동작하나”만 집중해서 보면 됐다.
  • 여러 소스를 동시에 건드리지 않아도 됐다. 작업이 앞에 놓인 그 단위 안에서 닫혀 있었다. 한 화면을 전환하려고 전역 구조를 헤집거나 여기저기 파일을 같이 고칠 필요가 없었고, 그래서 변경 영향 범위를 그 단위만 확인하면 됐다. (이는 앞서 규칙으로 강조한 “외과수술적 변경”과도 맞닿는다.)

요약하면 “작게 자른 덕분에 AI도 사람도 한 번에 한 조각만 신경 쓰면 됐다.” 다만 이 장점의 이면이 바로 아래의 한계로 이어진다 — 조각마다 닫혀 있었기에 전역 중복을 놓쳤다. 같은 선택이 장점이자 약점이었던 셈이다.

한계와 배운 점

가장 아쉬운 점 — SP를 ORM으로 걷어내지 못했다

가장 아쉬운 부분은 Stored Procedure를 JPA 같은 ORM 기반 코드로 전환하지 못한 것이다.

처음 목표는 SP에 묶여 있는 로직을 Java 도메인 계층으로 끌어올려, 장기적으로 SP 의존을 줄이는 것이었다. 그런데 막상 열어 보니 SP 하나에 조회/검증/상태 변경뿐 아니라 감사 로깅, 부가 집계, 다른 테이블 동기화까지 너무 많은 로직이 한데 들어 있었다. 이걸 Java로 옮기면:

  • 운영 중인 다른 시스템도 같은 SP를 공유하고 있어, 로직을 Java로 빼는 순간 동작이 갈라질 위험이 있었고,
  • 로깅·집계 같은 부수효과까지 1:1로 재현·검증하는 비용이 전환 이득보다 훨씬 컸다.

그래서 방향을 바꿨다. SP를 전환하는 대신, 기존 SP가 “무엇을 하는지”를 AI에게 문서로 정리하게 했다. 입력 파라미터, 결과셋 구조, 내부에서 일어나는 주요 동작(어떤 테이블을 건드리고 어떤 로그를 남기는지)을 명세화한 문서를 만들어 두고, Java 레이어는 그 문서를 근거로 SP를 그대로 호출하도록 했다. 즉 “로직을 옮기기”가 아니라 “로직을 보존하되 명세를 확보하기”로 타협한 셈이다.

결과적으로 안정성은 챙겼지만, 레거시 SP 의존은 그대로 남았다. ORM 전환은 다음 단계의 숙제로 미뤄졌고, 이 부분이 이번 프로젝트에서 가장 아쉬운 지점이다. 다만 “SP 명세를 문서로 확보해 둔 것” 자체는 나중에 SP를 걷어낼 때 출발점이 될 수 있어, 완전히 손해는 아니라고 본다.

메뉴 단위로 나눠 작업하다 공통화를 놓쳤다

두 번째 아쉬운 점은 중복 기능을 공통화하지 못한 것이다.

전환은 화면이 많아 메뉴 단위로 여러 개발자가 나눠 진행했다. 각자 자기 메뉴를 AI로 전환했는데, AI는 기본적으로 “지금 이 메뉴” 컨텍스트 안에서만 일관성을 맞춘다. 그 결과:

  • 목록/페이징, 검색 필터 처리, 파일 업로드, 공통 코드 조회처럼 사실상 같은 기능이 메뉴마다 비슷하지만 미묘하게 다른 코드로 중복 생성됐다.
  • 각 메뉴 안에서는 깔끔했지만, 전체로 보면 같은 로직이 여기저기 흩어진 상태가 됐다. 좁은 컨텍스트 + 분산 작업의 전형적인 부작용이다.

결국 뒤늦게 공통 소스로 묶기 위해 AI로 전체 코드를 다시 훑어 중복 패턴을 찾아내고, 공통 컴포넌트/유틸로 추출하는 검증 작업을 한 번 더 돌려야 했다. “처음부터 공통화했으면 안 했을 일”을 사후에 한 셈이다.

교훈은 분명했다. 분산 작업에서는 “메뉴별 일관성”이 아니라 “전역 일관성”을 따로 챙겨야 한다. 만약 다시 한다면, 메뉴 작업에 들어가기 전에 공통 컴포넌트 후보 카탈로그(공통 목록/페이징/업로드 패턴 등)를 먼저 만들고, 전환 스킬이 “새로 만들기 전에 공통 후보부터 확인”하도록 절차에 넣었을 것이다. AI에게 좁은 작업을 맡길수록, 전역 관점은 사람이나 별도 검증 단계가 책임져야 한다.

그 밖에

  • 규칙/스킬도 유지보수 대상이다. SP가 바뀌면 캐시가 stale이 되고 골든 테스트가 거짓 안심을 준다. “규칙을 만들었다”로 끝이 아니라 갱신 루틴이 필요하다.
  • 목 테스트의 한계는 분명하다. 위치 바인딩·SP 내부 컬럼 매핑은 결국 실 DB 통합 테스트로만 잡힌다.
  • 비밀정보 분리는 처음부터. SP 정의를 DB에서 직접 조회하려다 보니, check-sp 스킬에 개발기(dev) DB 접속 정보를 넣어 두고 썼다. 어디까지나 운영이 아닌 개발 환경 계정이라 당시엔 편의상 그렇게 진행했지만 — 그래도 좋은 방식은 아니다. 개발기라도 접속 정보를 스킬/문서에 평문으로 박으면 그대로 저장소 히스토리에 남고, 개발 DB 역시 내부망·실데이터의 일부일 수 있다. 시작할 때부터 환경변수/시크릿 매니저로 빼고, 스킬에는 “참조 위치”만 두는 게 맞다.
  • 가장 큰 효과는 “AI를 똑똑하게 만드는 것”이 아니라 “AI가 헛짓할 여지를 좁히는 것” 이었다. 좋은 제약과 절차가 좋은 프롬프트보다 오래 간다.

정리

레거시 전환처럼 양 많고 규칙성 강한 작업에서 AI 에이전트는 분명히 큰 레버리지를 준다. 단, 그냥 시키면 위험하고, 규칙(Rules)과 절차(Skills)로 판단 영역을 좁혀줄 때 비로소 신뢰할 수 있는 협업자가 된다. 핵심을 한 줄로 요약하면:

“추측하지 말고 확인하게 하고, 순서를 강제하고, 끝나면 스스로 검증하게 하라.”

회사 코드는 한 줄도 공개하지 않았지만, 방법론 자체는 어떤 레거시 전환 프로젝트에도 그대로 적용할 수 있다고 생각한다.