본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: 코드 마이그레이션 자동화
2026년 3월 15일·AI / ML·

6장: 코드 마이그레이션 자동화

LLM을 활용한 언어/프레임워크 마이그레이션 자동화를 학습합니다. Java에서 Kotlin, React Class에서 Hooks로의 전환과 의미 보존 검증 기법을 다룹니다.

16분983자8개 섹션
code-qualityaillmdevtools
공유
code-analysis6 / 10
12345678910
이전5장: LLM 기반 자동 리팩터링다음7장: 보안 취약점 분석과 자동 수정

학습 목표

  • 코드 마이그레이션의 유형과 각각의 도전 과제를 이해합니다
  • 언어 마이그레이션(Java에서 Kotlin, JavaScript에서 TypeScript)의 자동화 기법을 학습합니다
  • 프레임워크 마이그레이션(React Class에서 Hooks)의 패턴을 파악합니다
  • 의미 보존 검증(Semantic Preservation Verification) 방법을 이해합니다

코드 마이그레이션의 도전

코드 마이그레이션은 리팩터링의 특수한 형태입니다. 단순한 코드 개선이 아니라 언어, 프레임워크, API 버전 등 기반 기술 자체를 전환하는 작업입니다. 전통적으로 수 주에서 수 개월이 걸리던 마이그레이션이 LLM을 활용하면 72시간 안에 완료된 사례가 보고되고 있습니다.

마이그레이션의 네 가지 유형

유형난이도자동화 가능성핵심 과제
언어 마이그레이션높음높음관용구 차이, 타입 시스템
프레임워크 마이그레이션중간높음패러다임 전환, 상태 관리
API 업그레이드낮음매우 높음인터페이스 변경, 하위 호환성
패키지 마이그레이션낮음매우 높음API 매핑, 동작 차이

언어 마이그레이션

Java에서 Kotlin으로

Java에서 Kotlin 마이그레이션은 가장 성공적인 LLM 기반 마이그레이션 사례 중 하나입니다. Kotlin이 Java와 완전한 상호운용성을 가지므로 점진적 마이그레이션이 가능합니다.

UserService.java
java
// 마이그레이션 전: Java
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
 
    public UserService(UserRepository userRepository,
                       EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
 
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }
 
    public User createUser(String name, String email) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
 
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
 
        User saved = userRepository.save(user);
        emailService.sendWelcomeEmail(saved);
        return saved;
    }
 
    public List<User> findActiveUsers() {
        return userRepository.findAll().stream()
            .filter(User::isActive)
            .sorted(Comparator.comparing(User::getCreatedAt).reversed())
            .collect(Collectors.toList());
    }
}
UserService.kt
kotlin
// 마이그레이션 후: Kotlin
class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService,
) {
    fun findById(id: Long): User? =
        userRepository.findById(id).orElse(null)
 
    fun createUser(name: String, email: String): User {
        require(name.isNotBlank()) { "Name cannot be empty" }
        require(email.contains("@")) { "Invalid email" }
 
        val user = User(
            name = name,
            email = email,
            createdAt = LocalDateTime.now(),
        )
 
        return userRepository.save(user).also { saved ->
            emailService.sendWelcomeEmail(saved)
        }
    }
 
    fun findActiveUsers(): List<User> =
        userRepository.findAll()
            .filter { it.isActive }
            .sortedByDescending { it.createdAt }
}

LLM은 단순한 구문 변환을 넘어 Kotlin의 관용구(idiomatic expression)를 적용합니다.

  • Optional 대신 nullable 타입(User?) 사용
  • 생성자 매개변수를 클래스 선언에 포함
  • require 함수로 검증 간소화
  • also 스코프 함수 활용
  • 컬렉션 확장 함수(filter, sortedByDescending) 활용

JavaScript에서 TypeScript로

migration-pipeline.ts
typescript
interface MigrationConfig {
  sourceDir: string;
  targetDir: string;
  tsConfigPath: string;
  strictMode: boolean;
}
 
interface MigrationResult {
  file: string;
  status: "success" | "partial" | "failed";
  typesAdded: number;
  anyTypesRemaining: number;
  errors: string[];
}
 
async function migrateJsToTs(
  config: MigrationConfig,
  llmClient: LLMClient,
): Promise<MigrationResult[]> {
  const results: MigrationResult[] = [];
  const jsFiles = await glob(`${config.sourceDir}/**/*.js`);
 
  for (const jsFile of jsFiles) {
    const source = await readFile(jsFile, "utf-8");
    const tsFile = jsFile.replace(/\.js$/, ".ts");
 
    // 1단계: 기본 변환 (확장자 변경 + import 수정)
    let converted = convertBasicSyntax(source);
 
    // 2단계: LLM 기반 타입 추론
    const prompt = buildTypeInferencePrompt(converted, config.strictMode);
    const typed = await llmClient.generate(prompt);
 
    // 3단계: 타입 검증
    const validationResult = await validateTypes(typed, config.tsConfigPath);
 
    if (validationResult.errors.length > 0) {
      // 4단계: 오류 수정 시도
      const fixPrompt = buildFixPrompt(typed, validationResult.errors);
      const fixed = await llmClient.generate(fixPrompt);
      await writeFile(tsFile, fixed);
    } else {
      await writeFile(tsFile, typed);
    }
 
    results.push({
      file: tsFile,
      status: validationResult.errors.length === 0 ? "success" : "partial",
      typesAdded: countTypes(typed),
      anyTypesRemaining: countAnyTypes(typed),
      errors: validationResult.errors,
    });
  }
 
  return results;
}
Tip

JavaScript에서 TypeScript 마이그레이션 시 strict: false로 시작하여 점진적으로 strict: true로 전환하는 전략이 효과적입니다. LLM에게도 "우선 컴파일이 되는 것"을 우선 목표로 설정하고, 이후 any 타입을 점진적으로 제거하도록 지시합니다.


프레임워크 마이그레이션

React Class 컴포넌트에서 Hooks로

React 생태계에서 가장 빈번한 마이그레이션 중 하나입니다. 클래스 기반 생명주기 메서드를 함수형 Hooks로 전환합니다.

UserProfile.class.tsx
tsx
// 마이그레이션 전: Class 컴포넌트
interface UserProfileProps {
  userId: string;
}
 
interface UserProfileState {
  user: User | null;
  loading: boolean;
  error: string | null;
}
 
class UserProfile extends React.Component<UserProfileProps, UserProfileState> {
  state: UserProfileState = {
    user: null,
    loading: true,
    error: null,
  };
 
  componentDidMount() {
    this.fetchUser();
  }
 
  componentDidUpdate(prevProps: UserProfileProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser();
    }
  }
 
  componentWillUnmount() {
    this.abortController?.abort();
  }
 
  private abortController: AbortController | null = null;
 
  async fetchUser() {
    this.abortController?.abort();
    this.abortController = new AbortController();
 
    this.setState({ loading: true, error: null });
 
    try {
      const response = await fetch(`/api/users/${this.props.userId}`, {
        signal: this.abortController.signal,
      });
      const user = await response.json();
      this.setState({ user, loading: false });
    } catch (err) {
      if (err instanceof Error && err.name !== "AbortError") {
        this.setState({ error: err.message, loading: false });
      }
    }
  }
 
  render() {
    const { user, loading, error } = this.state;
 
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return null;
 
    return (
      <div>
        <h1>{user.name}</h1>
        <p>{user.email}</p>
      </div>
    );
  }
}
UserProfile.hooks.tsx
tsx
// 마이그레이션 후: Hooks 기반 함수형 컴포넌트
interface UserProfileProps {
  userId: string;
}
 
function useUser(userId: string) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    const abortController = new AbortController();
 
    async function fetchUser() {
      setLoading(true);
      setError(null);
 
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal,
        });
        const data = await response.json();
        setUser(data);
        setLoading(false);
      } catch (err) {
        if (err instanceof Error && err.name !== "AbortError") {
          setError(err.message);
          setLoading(false);
        }
      }
    }
 
    fetchUser();
    return () => abortController.abort();
  }, [userId]);
 
  return { user, loading, error };
}
 
function UserProfile({ userId }: UserProfileProps) {
  const { user, loading, error } = useUser(userId);
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

LLM은 다음과 같은 패턴 변환을 자동으로 수행합니다.

  • componentDidMount + componentDidUpdate 를 useEffect로 통합
  • componentWillUnmount 클린업을 useEffect 반환 함수로 전환
  • this.state / this.setState를 useState 훅으로 전환
  • 데이터 페칭 로직을 커스텀 훅(useUser)으로 추출
  • 클래스 인스턴스 변수를 useRef 또는 로컬 변수로 전환

의미 보존 검증

마이그레이션에서 가장 중요한 것은 의미 보존(Semantic Preservation)입니다. 변환된 코드가 원본과 동일한 동작을 보장해야 합니다.

다층 검증 전략

동작 동등성 테스트

equivalence_tester.py
python
import subprocess
import json
from dataclasses import dataclass
 
 
@dataclass
class EquivalenceResult:
    test_case: str
    original_output: str
    migrated_output: str
    is_equivalent: bool
    difference: str | None
 
 
class EquivalenceTester:
    """원본과 마이그레이션 코드의 동작 동등성 검증"""
 
    def __init__(
        self,
        original_runner: str,
        migrated_runner: str,
    ):
        self.original_runner = original_runner
        self.migrated_runner = migrated_runner
 
    def test_equivalence(
        self,
        test_cases: list[dict],
    ) -> list[EquivalenceResult]:
        results = []
 
        for case in test_cases:
            original_output = self._run(
                self.original_runner, case["input"]
            )
            migrated_output = self._run(
                self.migrated_runner, case["input"]
            )
 
            is_eq = self._compare_outputs(
                original_output, migrated_output,
                tolerance=case.get("tolerance", 0),
            )
 
            results.append(EquivalenceResult(
                test_case=case["name"],
                original_output=original_output,
                migrated_output=migrated_output,
                is_equivalent=is_eq,
                difference=None if is_eq else self._diff(
                    original_output, migrated_output
                ),
            ))
 
        return results
 
    def _run(self, runner: str, input_data: str) -> str:
        result = subprocess.run(
            [runner],
            input=input_data,
            capture_output=True,
            text=True,
            timeout=30,
        )
        return result.stdout
 
    def _compare_outputs(
        self,
        original: str,
        migrated: str,
        tolerance: float = 0,
    ) -> bool:
        if tolerance == 0:
            return original.strip() == migrated.strip()
 
        # 수치 비교 시 허용 오차 적용
        try:
            orig_val = float(original.strip())
            migr_val = float(migrated.strip())
            return abs(orig_val - migr_val) <= tolerance
        except ValueError:
            return original.strip() == migrated.strip()
 
    def _diff(self, original: str, migrated: str) -> str:
        orig_lines = original.splitlines()
        migr_lines = migrated.splitlines()
        diffs = []
        for i, (o, m) in enumerate(zip(orig_lines, migr_lines)):
            if o != m:
                diffs.append(f"줄 {i + 1}: '{o}' != '{m}'")
        return "\n".join(diffs[:10])
Warning

의미 보존 검증은 단위 테스트 통과만으로는 충분하지 않습니다. 엣지 케이스, 에러 처리, 타이밍 관련 동작까지 검증해야 합니다. 특히 비동기 코드의 마이그레이션에서는 실행 순서가 미묘하게 달라질 수 있으므로 주의가 필요합니다.


실전 마이그레이션 전략

점진적 마이그레이션

대규모 코드베이스를 한 번에 마이그레이션하는 것은 위험합니다. 점진적 접근이 안전합니다.

1단계: 유틸리티 모듈 -- 의존성이 적고 테스트가 잘 된 유틸리티부터 시작합니다.

2단계: 데이터 모델 -- 타입 정의와 데이터 구조를 마이그레이션합니다.

3단계: 서비스 계층 -- 비즈니스 로직을 포함한 서비스를 전환합니다.

4단계: UI 계층 -- 프레젠테이션 레이어를 마지막으로 전환합니다.

수 주에서 72시간으로

LLM 기반 마이그레이션의 실제 성과는 인상적입니다. 수동으로 수 주가 걸리던 프레임워크 마이그레이션이 LLM의 도움으로 72시간 안에 완료된 사례가 있습니다. 이는 LLM이 반복적인 패턴 변환을 자동화하고, 사람은 복잡한 비즈니스 로직 검증에 집중할 수 있기 때문입니다.


정리

코드 마이그레이션은 언어, 프레임워크, API, 패키지의 네 가지 유형으로 분류되며, 각각 고유한 도전 과제를 가집니다. LLM은 단순한 구문 변환을 넘어 대상 언어/프레임워크의 관용구를 적용하며, 점진적 마이그레이션 전략과 결합하면 안전하고 효율적인 전환이 가능합니다.

의미 보존 검증은 마이그레이션의 핵심이며, 구문 검증부터 동작 동등성 테스트까지 다층 검증 전략이 필요합니다. 이를 통해 수 주가 걸리던 작업을 72시간으로 단축할 수 있습니다.

다음 장 미리보기

7장에서는 코드 분석의 특수 영역인 보안 취약점 분석과 자동 수정을 다룹니다. SAST와 LLM의 하이브리드 접근, OWASP Top 10 탐지, 취약점 자동 수정 제안, 그리고 CI/CD에 보안 게이트를 통합하는 방법을 학습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#code-quality#ai#llm#devtools

관련 글

AI / ML

7장: 보안 취약점 분석과 자동 수정

SAST와 LLM을 결합한 보안 취약점 탐지, OWASP Top 10 자동 검출, 취약점 자동 수정 제안과 CI/CD 보안 게이트 구축을 학습합니다.

2026년 3월 17일·16분
AI / ML

5장: LLM 기반 자동 리팩터링

LLM을 활용한 자동 리팩터링의 패턴, 멀티에이전트 아키텍처, 검증 파이프라인을 학습합니다. 37%에서 98%로 정밀도를 끌어올리는 실전 기법을 다룹니다.

2026년 3월 13일·15분
AI / ML

8장: 아키텍처 분석과 시각화

LLM을 활용한 아키텍처 분석, 순환 의존성 감지, 레이어 위반 탐지, 마이크로서비스 경계 제안과 아키텍처 다이어그램 자동 생성을 학습합니다.

2026년 3월 19일·17분
이전 글5장: LLM 기반 자동 리팩터링
다음 글7장: 보안 취약점 분석과 자동 수정

댓글

목차

약 16분 남음
  • 학습 목표
  • 코드 마이그레이션의 도전
    • 마이그레이션의 네 가지 유형
  • 언어 마이그레이션
    • Java에서 Kotlin으로
    • JavaScript에서 TypeScript로
  • 프레임워크 마이그레이션
    • React Class 컴포넌트에서 Hooks로
  • 의미 보존 검증
    • 다층 검증 전략
    • 동작 동등성 테스트
  • 실전 마이그레이션 전략
    • 점진적 마이그레이션
    • 수 주에서 72시간으로
  • 정리
  • 다음 장 미리보기