LLM을 활용한 언어/프레임워크 마이그레이션 자동화를 학습합니다. Java에서 Kotlin, React Class에서 Hooks로의 전환과 의미 보존 검증 기법을 다룹니다.
코드 마이그레이션은 리팩터링의 특수한 형태입니다. 단순한 코드 개선이 아니라 언어, 프레임워크, API 버전 등 기반 기술 자체를 전환하는 작업입니다. 전통적으로 수 주에서 수 개월이 걸리던 마이그레이션이 LLM을 활용하면 72시간 안에 완료된 사례가 보고되고 있습니다.
| 유형 | 난이도 | 자동화 가능성 | 핵심 과제 |
|---|---|---|---|
| 언어 마이그레이션 | 높음 | 높음 | 관용구 차이, 타입 시스템 |
| 프레임워크 마이그레이션 | 중간 | 높음 | 패러다임 전환, 상태 관리 |
| API 업그레이드 | 낮음 | 매우 높음 | 인터페이스 변경, 하위 호환성 |
| 패키지 마이그레이션 | 낮음 | 매우 높음 | API 매핑, 동작 차이 |
Java에서 Kotlin 마이그레이션은 가장 성공적인 LLM 기반 마이그레이션 사례 중 하나입니다. Kotlin이 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());
}
}// 마이그레이션 후: 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) 활용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;
}JavaScript에서 TypeScript 마이그레이션 시 strict: false로 시작하여 점진적으로 strict: true로 전환하는 전략이 효과적입니다. LLM에게도 "우선 컴파일이 되는 것"을 우선 목표로 설정하고, 이후 any 타입을 점진적으로 제거하도록 지시합니다.
React 생태계에서 가장 빈번한 마이그레이션 중 하나입니다. 클래스 기반 생명주기 메서드를 함수형 Hooks로 전환합니다.
// 마이그레이션 전: 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>
);
}
}// 마이그레이션 후: 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)입니다. 변환된 코드가 원본과 동일한 동작을 보장해야 합니다.
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])의미 보존 검증은 단위 테스트 통과만으로는 충분하지 않습니다. 엣지 케이스, 에러 처리, 타이밍 관련 동작까지 검증해야 합니다. 특히 비동기 코드의 마이그레이션에서는 실행 순서가 미묘하게 달라질 수 있으므로 주의가 필요합니다.
대규모 코드베이스를 한 번에 마이그레이션하는 것은 위험합니다. 점진적 접근이 안전합니다.
1단계: 유틸리티 모듈 -- 의존성이 적고 테스트가 잘 된 유틸리티부터 시작합니다.
2단계: 데이터 모델 -- 타입 정의와 데이터 구조를 마이그레이션합니다.
3단계: 서비스 계층 -- 비즈니스 로직을 포함한 서비스를 전환합니다.
4단계: UI 계층 -- 프레젠테이션 레이어를 마지막으로 전환합니다.
LLM 기반 마이그레이션의 실제 성과는 인상적입니다. 수동으로 수 주가 걸리던 프레임워크 마이그레이션이 LLM의 도움으로 72시간 안에 완료된 사례가 있습니다. 이는 LLM이 반복적인 패턴 변환을 자동화하고, 사람은 복잡한 비즈니스 로직 검증에 집중할 수 있기 때문입니다.
코드 마이그레이션은 언어, 프레임워크, API, 패키지의 네 가지 유형으로 분류되며, 각각 고유한 도전 과제를 가집니다. LLM은 단순한 구문 변환을 넘어 대상 언어/프레임워크의 관용구를 적용하며, 점진적 마이그레이션 전략과 결합하면 안전하고 효율적인 전환이 가능합니다.
의미 보존 검증은 마이그레이션의 핵심이며, 구문 검증부터 동작 동등성 테스트까지 다층 검증 전략이 필요합니다. 이를 통해 수 주가 걸리던 작업을 72시간으로 단축할 수 있습니다.
7장에서는 코드 분석의 특수 영역인 보안 취약점 분석과 자동 수정을 다룹니다. SAST와 LLM의 하이브리드 접근, OWASP Top 10 탐지, 취약점 자동 수정 제안, 그리고 CI/CD에 보안 게이트를 통합하는 방법을 학습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
SAST와 LLM을 결합한 보안 취약점 탐지, OWASP Top 10 자동 검출, 취약점 자동 수정 제안과 CI/CD 보안 게이트 구축을 학습합니다.
LLM을 활용한 자동 리팩터링의 패턴, 멀티에이전트 아키텍처, 검증 파이프라인을 학습합니다. 37%에서 98%로 정밀도를 끌어올리는 실전 기법을 다룹니다.
LLM을 활용한 아키텍처 분석, 순환 의존성 감지, 레이어 위반 탐지, 마이크로서비스 경계 제안과 아키텍처 다이어그램 자동 생성을 학습합니다.