CHHB stroy

Claude Code로 레거시 PHP 리팩토링하기 — 실제로 해보고 정리한 워크플로우 본문

카테고리 없음

Claude Code로 레거시 PHP 리팩토링하기 — 실제로 해보고 정리한 워크플로우

CHHB 2026. 6. 5. 09:30

회사에 10년 묵은 PHP 프로젝트가 하나 있다. PHP 5 시절에 짠 코드가 그대로 살아있고, 함수 하나가 500줄씩 되고, 전역 변수가 사방에 깔려있고, SQL이 코드에 그냥 박혀있는 그런 코드. 손대기 무서워서 다들 피하던 녀석인데, Claude Code로 조금씩 리팩토링해봤더니 생각보다 할 만했다.

물론 "Claude한테 던지면 알아서 다 해주겠지" 하면 큰코다친다. 레거시 리팩토링은 AI한테도 어렵다. 근데 제대로 된 워크플로우로 접근하면 혼자 할 때보다 훨씬 빠르고 안전하게 할 수 있다. 오늘은 내가 시행착오 끝에 정리한 PHP 리팩토링 방법을 공유한다.


시작 전에: 리팩토링의 대전제

리팩토링의 핵심은 동작은 그대로 두고 구조만 바꾸는 것이다. 이게 안 지켜지면 그건 리팩토링이 아니라 그냥 코드 망가뜨리기다.

그래서 Claude Code로 리팩토링하기 전에 두 가지가 반드시 선행돼야 한다.

1. Git 커밋 — 깨끗한 상태에서 시작. 언제든 되돌릴 수 있게.
2. 테스트 — 리팩토링 전후 동작이 같은지 검증할 수단.

테스트가 없는 레거시 코드라면? 그게 대부분의 현실이긴 하다. 이 경우엔 리팩토링 전에 먼저 테스트부터 만드는 것이 순서다. 이것도 Claude Code가 도와줄 수 있다. 뒤에서 다룬다.


Step 0: CLAUDE.md부터 세팅

본격적으로 시작하기 전에, 프로젝트 루트에 CLAUDE.md를 만들어두자. 리팩토링은 일관성이 생명인데, Claude가 매번 다른 스타일로 고치면 오히려 코드가 더 엉망이 된다.

# CLAUDE.md

## 프로젝트
레거시 PHP 프로젝트를 점진적으로 현대화하는 중입니다.
현재 PHP 8.1에서 동작하지만 코드는 PHP 5 스타일이 많습니다.

## 리팩토링 원칙
- 동작을 절대 바꾸지 말 것 (기능 변경 금지, 구조만 개선)
- 한 번에 하나씩만 변경 (큰 변경을 여러 작은 변경으로 쪼갤 것)
- 변경 전 반드시 동작을 설명하고, 변경 후 무엇이 달라졌는지 명시

## 코딩 컨벤션
- PSR-12 코딩 스타일 준수
- 타입 힌트 적극 사용 (파라미터, 반환 타입)
- 함수명은 camelCase, 클래스명은 PascalCase
- mysqli 직접 호출 대신 준비된 구문(prepared statement) 사용
- 새 코드에는 PHPDoc 주석 추가

## 하지 말 것
- 검증 안 된 라이브러리 임의 추가 금지 (먼저 물어볼 것)
- 한 번에 여러 파일을 대대적으로 수정하지 말 것
- SQL 인젝션 위험이 있는 코드는 발견 즉시 알릴 것

이렇게 해두면 매번 "타입 힌트 넣어줘", "PSR-12로 해줘"를 반복하지 않아도 된다.


Step 1: 코드 파악부터 시키기

레거시 코드는 일단 뭐가 뭔지 모르는 게 문제다. 바로 고치라고 하지 말고, 먼저 분석부터 시킨다.

@legacy/UserManager.php 이 파일을 분석해줘.
- 이 클래스가 하는 일이 뭔지
- 주요 메서드와 각각의 역할
- 코드에서 보이는 문제점 (긴 함수, 중복, 보안 위험, 안티패턴 등)
- 리팩토링이 시급한 부분 우선순위

아직 코드는 수정하지 말고 분석만 해줘.

"수정하지 말고 분석만"이라고 못 박는 게 중요하다. 안 그러면 분석하다가 바로 코드를 뜯어고치기 시작한다. 우선 전체 그림을 파악하는 게 먼저다.

Claude가 분석 결과를 주면, 그걸 보고 어디부터 손댈지 내가 판단한다. 보통 이런 것들이 우선순위로 올라온다:

  • SQL 인젝션 같은 보안 위험 (최우선)
  • 500줄짜리 거대 함수
  • 같은 코드가 복붙된 중복
  • 전역 변수 남발

Step 2: 테스트부터 만들기 (테스트가 없다면)

리팩토링하려는데 테스트가 없으면, 리팩토링이 코드를 망가뜨렸는지 알 방법이 없다. 그래서 먼저 현재 동작을 고정하는 테스트(characterization test)를 만든다.

@legacy/PriceCalculator.php 의 calculate() 메서드에 대한 테스트를 작성해줘.

지금 이 메서드가 어떻게 동작하는지를 그대로 검증하는 테스트가 필요해.
(코드가 "올바른지"가 아니라, "현재 어떻게 동작하는지"를 고정하는 목적)

- PHPUnit 사용
- 정상 케이스, 경계값, 0이나 음수 같은 엣지 케이스 포함
- 외부 의존성(DB 등)은 모킹 처리

이 테스트의 목적은 코드가 "맞는지"를 검증하는 게 아니라, 현재 동작을 그대로 박제하는 거다. 리팩토링 후에 이 테스트가 깨지면 동작이 바뀐 거니까 뭔가 잘못된 거다.

# 테스트 실행해서 통과 확인
./vendor/bin/phpunit tests/PriceCalculatorTest.php

테스트가 통과하는 걸 확인하고 나서야 리팩토링을 시작한다. 이게 안전망이다.


Step 3: 작게, 하나씩 리팩토링

여기서 제일 중요한 원칙. 한 번에 하나씩. "이 파일 전체를 현대적으로 리팩토링해줘"라고 하면 Claude가 100군데를 한꺼번에 바꿔놓는데, 뭐가 잘못됐는지 추적이 안 된다.

예시 1: 거대 함수 쪼개기

@legacy/OrderProcessor.php 의 processOrder() 메서드가 320줄이야.
이걸 작은 메서드들로 분리해줘.

- 각 메서드는 한 가지 일만 하도록
- 검증 / 재고 확인 / 결제 / 알림 같은 논리적 단위로 나눠줘
- 동작은 절대 바꾸지 말고 구조만 분리
- 분리 후 processOrder()는 각 메서드를 순서대로 호출하는 형태로

변경이 끝나면 diff를 확인하고, 테스트를 돌려본다.

./vendor/bin/phpunit

테스트 통과하면 커밋. 안 되면 되돌리고 다시.

git add .
git commit -m "refactor: processOrder 메서드를 논리 단위로 분리"

커밋을 자주 하는 게 핵심이다. 변경 하나 = 커밋 하나. 그래야 문제 생겼을 때 정확히 어디서 깨졌는지 알 수 있다.

예시 2: SQL 인젝션 제거

레거시 PHP에서 제일 흔하고 위험한 패턴이다.

@legacy/SearchController.php 에서 SQL 인젝션 위험이 있는 쿼리를 찾아서
준비된 구문(prepared statement)으로 바꿔줘.

- mysqli 또는 PDO의 prepared statement 사용
- 사용자 입력이 직접 쿼리에 들어가는 부분을 전부 파라미터 바인딩으로
- 동작은 동일하게 유지

이런 건 Claude가 정말 잘 잡아준다. 문자열 연결로 만든 쿼리를 파라미터 바인딩 방식으로 깔끔하게 바꿔준다.

// Before (위험)
$query = "SELECT * FROM users WHERE name = '" . $_GET['name'] . "'";
$result = mysqli_query($conn, $query);

// After (안전)
$stmt = $conn->prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $_GET['name']);
$stmt->execute();
$result = $stmt->get_result();

예시 3: 타입 힌트 추가

PHP 5 코드에는 타입 선언이 없다. PHP 8 시대에는 타입을 명시하는 게 좋다.

@src/Service/ 폴더의 모든 메서드에 타입 힌트를 추가해줘.
- 파라미터 타입과 반환 타입 모두
- nullable이 필요한 경우 ?type 사용
- 기존 동작을 깨지 않는 선에서만 (확신 안 서면 표시해줘)
- strict_types declare 추가

"확신 안 서면 표시해줘"가 포인트다. 레거시 코드는 같은 변수에 여러 타입이 들어가는 경우가 있어서, 무작정 타입을 박으면 오히려 깨진다. Claude가 애매한 부분을 표시해주면 내가 직접 판단한다.


Step 4: 중복 코드 제거

레거시 PHP의 단골 문제. 복붙으로 떡칠된 코드.

프로젝트 전체에서 중복되는 코드 패턴을 찾아줘.
특히 DB 연결, 입력 검증, 응답 포맷팅 같은 게 여러 곳에 반복되는지.

찾으면 공통 함수나 클래스로 추출할 수 있는지 제안해줘.
(바로 수정하지 말고 먼저 제안만)

제안을 보고 괜찮으면 하나씩 추출한다.

방금 제안한 것 중에서 DB 연결 코드를 Database 클래스로 추출해줘.
싱글톤 패턴으로, 한 곳에서 연결을 관리하도록.
기존에 직접 연결하던 코드들은 이 클래스를 쓰도록 바꿔줘.

이런 작업은 여러 파일에 걸치니까, 변경 후 반드시 전체 테스트를 돌려야 한다.


Step 5: 점진적 현대화

한 방에 다 바꾸려 하지 말고, PHP 버전 기능을 활용해서 점진적으로 개선한다.

@src/Model/User.php 를 현대적인 PHP 8 스타일로 개선해줘.

- 생성자 프로퍼티 승격(constructor property promotion) 활용
- 적절한 곳에 readonly 프로퍼티
- match 표현식으로 바꿀 수 있는 switch가 있으면 변경
- enum으로 바꿀 수 있는 상수 그룹이 있으면 제안
- 단, 동작은 동일하게

PHP 8의 기능들(생성자 프로퍼티 승격, match, enum, named arguments 등)을 활용하면 코드가 훨씬 간결해진다. Claude가 이런 변환을 잘한다.

// Before (PHP 5 스타일)
class User {
    private $name;
    private $email;
    public function __construct($name, $email) {
        $this->name = $name;
        $this->email = $email;
    }
}

// After (PHP 8 스타일)
class User {
    public function __construct(
        private readonly string $name,
        private readonly string $email,
    ) {}
}

실전 워크플로우 요약

내가 실제로 도는 사이클은 이렇다.

1. 분석 시키기 (수정 금지, 분석만)
2. 테스트 없으면 → 현재 동작 고정 테스트 작성
3. 작은 단위 하나 리팩토링 요청
4. diff 확인
5. 테스트 실행
6. 통과하면 커밋 / 실패하면 되돌리고 재시도
7. 다음 단위로 (3번부터 반복)

이 사이클을 짧게 반복하는 게 핵심이다. 큰 변경 하나보다, 작은 변경 열 개가 훨씬 안전하다.


Plan Mode를 적극 활용하자

큰 리팩토링은 Claude Code의 Plan Mode로 계획을 먼저 받는 게 좋다. 바로 코드를 고치는 게 아니라 "이렇게 할 거다"라는 계획을 보여주니까, 방향이 틀어지기 전에 잡을 수 있다.

@legacy/ReportGenerator.php 전체를 리팩토링하고 싶어. (Plan Mode)

먼저 어떤 순서로 어떻게 리팩토링할지 계획을 세워줘.
실행은 내가 승인한 다음에.

계획을 보고 "3번은 빼고", "이건 순서 바꿔서" 같은 조정을 한 뒤에 실행시키면 된다.


주의할 점들

1. 무조건 diff를 확인하자

AI가 아무리 잘해도 무조건 수락은 금물이다. 특히 레거시 코드는 겉보기엔 이상한데 실제론 의도된 동작인 경우가 있다(이상한 if문이 사실은 특정 버그 회피용이라든지). diff를 보면서 "이거 왜 이렇게 바꿨지?" 싶으면 물어보자.

방금 이 조건문을 제거했는데, 원래 코드에서 이게 있던 이유가 뭐였을 것 같아?
혹시 특정 엣지 케이스를 처리하던 건 아닐까?

2. 테스트 없이 리팩토링하지 말자

다시 강조한다. 테스트 없는 리팩토링은 눈 감고 운전하는 거다. 최소한 핵심 로직만이라도 테스트를 만들고 시작하자.

3. 한 번에 너무 많이 시키지 말자

"전체 프로젝트 리팩토링해줘"는 재앙의 지름길이다. 파일 하나, 메서드 하나 단위로 끊어서 진행하자. 컨텍스트도 관리되고, 문제 추적도 쉽다.

4. 동작 변경과 구조 변경을 섞지 말자

리팩토링(구조 변경)과 기능 추가(동작 변경)를 같은 커밋에서 하면 안 된다. 리팩토링 먼저 끝내고 테스트로 검증한 다음, 별도로 기능을 추가하자. 섞으면 문제 생겼을 때 원인 파악이 지옥이 된다.

5. 커밋 메시지를 Claude한테 맡겨도 된다

지금까지 변경한 내용으로 커밋해줘. 
conventional commit 형식으로, refactor: 접두사 사용해서.

마무리

레거시 PHP 리팩토링은 여전히 신경 쓸 게 많은 작업이지만, Claude Code와 함께라면 훨씬 수월해진다. 핵심은 "AI한테 다 맡기기"가 아니라 "AI를 잘 부리기"다.

정리하면:

  • CLAUDE.md로 리팩토링 원칙과 컨벤션을 미리 박아두자
  • 바로 고치지 말고 분석부터 시키자
  • 테스트 없으면 현재 동작 고정 테스트부터 만들자
  • 작게, 하나씩, 자주 커밋
  • 큰 작업은 Plan Mode로 계획 먼저
  • diff는 항상 확인. 무조건 수락 금지
  • 구조 변경과 기능 변경을 섞지 말자