AI 코드를 맹신할 수 없는 이유: 그럴듯한 거짓말과 결함
AI 어시스턴트는 초보 개발자가 수십 분 동안 헤맬 수 있는 복잡한 알고리즘을 단 몇 초 만에 완성해 줍니다. 들여쓰기도 완벽하고 주석까지 친절하게 달려 있어 초보자는 “AI가 완벽한 코드를 주었다”고 믿고 그대로 복사하여 프로젝트에 붙여넣기 쉽습니다.
하지만 소프트웨어 공학에서 가장 위험한 버그는 컴파일 에러가 나는 코드가 아니라, 정상 컴파일되어 돌아가지만 특정 조건에서 조용히 잘못된 결과를 산출하는 코드입니다.
대형 언어 모델은 코드의 논리적 무결성을 증명하는 수학적 증명기가 아닙니다. 인터넷에 존재하는 수억 줄의 공개 소스 코드를 학습하여 가장 그럴듯한 구문을 통계적으로 조합할 뿐입니다. 따라서 정상적인 흐름(Happy Path)에서는 잘 동작하지만, 조금만 비정상적인 입력이 들어오거나 경계 조건에 맞닥뜨리면 시스템 전체를 다운시키는 치명적인 취약점을 드러냅니다.
시니어 개발자와 주니어 개발자를 가르는 핵심 역량은 코드를 얼마나 빨리 타이핑하느냐가 아니라, AI가 건넨 코드의 결함을 짚어내고 엣지 케이스를 방어하도록 통제할 수 있는가입니다.
실무에서 반드시 걸러내야 하는 5대 엣지 케이스 (Edge Case)
AI가 제안한 코드를 검증할 때 개발자가 반드시 돋보기를 들이대야 하는 다섯 가지 핵심 취약 지점이 있습니다.
1. Null과 빈 컬렉션 (Empty Collection)
가장 빈번하게 시스템 장애를 일으키는 주범입니다. AI는 리스트나 맵에 항상 데이터가 존재한다고 가정하고 코드를 작성하는 경향이 있습니다.
list.get(0): 리스트가 비어 있을 때 즉각IndexOutOfBoundsException발생item.name().toUpperCase():name필드가null일 때NullPointerException발생map.get(key).calculate(): 존재하지 않는 키를 조회했을 때 null 참조 오류
2. 경계값 (Boundary Values): 0, 음수, 최대 용량
반복문이나 수식 계산에서 등호(<= vs <) 하나 차이로 버그가 발생합니다.
- 플레이어의 체력이 정확히
0이 되는 순간 사망 처리가 누락되거나 음수 체력으로 남는 경우 - 아이템 수량이나 경험치가
0또는 음수로 전달되어 인벤토리 개수가 역전되는 경우 - 배열 인덱스가 정확히
length에 도달하는 오프바이원(Off-by-one) 오류
3. 정수 오버플로우 (Integer Overflow)
int 형은 약 21억(2,147,483,647)을 넘어가면 음수로 뒤집힙니다. 대규모 경험치 누적, 게임 머니 정산, 타임스탬프 계산에서 int를 그대로 쓰면 심각한 데이터 오염이 일어납니다.
int totalExp = expA + expB: 두 큰 정수의 합이 음수가 되어 레벨이 1로 초기화되는 현상- 해결책: 대용량 수치는
long이나BigInteger를 사용하고Math.addExact()로 오버플로우를 감지합니다.
4. 불변성 파괴와 캡슐화 누출
객체 내부의 리스트나 배열을 게터(Getter) 메서드로 그대로 반환하면, 외부에서 getList().clear()나 add()를 호출해 내부 상태를 멋대로 변조할 수 있습니다.
- AI는 종종 단순 편의를 위해
return this.items;형태로 필드를 그대로 노출합니다. - 반드시
List.copyOf()나Collections.unmodifiableList()로 감싸 방어적 복사본을 제공해야 합니다.
5. 환각(Hallucination)에 의한 가상 API 호출
학습 데이터의 혼선으로 실제 JDK 표준 라이브러리에 없는 메서드를 마치 존재하는 것처럼 작성하는 현상입니다.
- 예:
List.of(1, 2).removeFirst()(Java 21의 Sequenced Collections 문법을 불변 리스트에 잘못 적용해 런타임UnsupportedOperationException유발) - 라이브러리의 공식 JavaDoc 문서를 통해 실제 호출 가능한 메서드인지 확인해야 합니다.
Java 25 최신 문법을 활용한 방어적 프로그래밍 기법
Java 25 LTS에서는 견고한 방어 코드를 훨씬 간결하고 명확하게 작성할 수 있는 현대적인 문법 장치들을 제공합니다.
- 레코드 컴팩트 생성자 (Compact Constructor): 별도의 필드 대입문 없이 생성 시점에 매개변수의 유효성을 즉시 강제합니다.
- Objects 유틸리티 메서드:
Objects.requireNonNull(val, "메시지")로 null 유입을 초기에 감지합니다. - 패턴 매칭 switch (Pattern Matching for switch): 복합 객체의 타입과 상태를 안전하고 완전하게 분기 처리합니다.
아래 실습 예제는 voxel 세계의 채굴(Mining) 시스템과 경험치 정산 로직입니다. AI가 초기에 작성했던 취약한 코드를 엣지 케이스 방어 로직으로 보강한 완성본입니다.
실행 가능한 엣지 케이스 방어 전체 코드
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Main {
// 1. 레코드 컴팩트 생성자로 유효성 및 경계값 방어
record VoxelOre(String oreType, int baseExp, int dropQuantity) {
public VoxelOre {
Objects.requireNonNull(oreType, "광물 종류는 null일 수 없습니다.");
if (oreType.isBlank()) {
throw new IllegalArgumentException("광물 이름이 비어 있습니다.");
}
if (baseExp < 0) {
throw new IllegalArgumentException("기본 경험치는 음수일 수 없습니다: " + baseExp);
}
if (dropQuantity <= 0) {
throw new IllegalArgumentException("채굴 드롭 수량은 1개 이상이어야 합니다: " + dropQuantity);
}
}
}
static class MiningSettlement {
private final String minerName;
private final List<VoxelOre> minedHistory;
private long accumulatedExp; // 정수 오버플로우 방지를 위해 long 채택
public MiningSettlement(String minerName) {
this.minerName = Objects.requireNonNull(minerName, "채굴자 이름은 필수입니다.");
this.minedHistory = new ArrayList<>();
this.accumulatedExp = 0L;
}
// 엣지 케이스 방어: null 광물 방어 및 안전한 연산
public void recordMining(VoxelOre ore, int toolBonusMultiplier) {
Objects.requireNonNull(ore, "채굴된 광물 정보가 null입니다.");
// 배율 엣지 케이스: 도구 보너스가 0 이하이면 기본 1배 적용
int effectiveMultiplier = Math.max(1, toolBonusMultiplier);
// 오버플로우 방지 계산
long earnedExp = (long) ore.baseExp() * ore.dropQuantity() * effectiveMultiplier;
this.accumulatedExp = Math.addExact(this.accumulatedExp, earnedExp);
this.minedHistory.add(ore);
}
// 캡슐화 누출 방지: 읽기 전용 불변 뷰 반환
public List<VoxelOre> getMinedHistory() {
return Collections.unmodifiableList(this.minedHistory);
}
public long getAccumulatedExp() {
return this.accumulatedExp;
}
public String getMinerName() {
return this.minerName;
}
}
public static void main(String[] args) {
System.out.println("=== AI 생성 코드 검증 및 엣지 케이스 방어 실습 ===");
MiningSettlement settlement = new MiningSettlement("Alex");
// [정상 케이스] 기본 채굴
VoxelOre coal = new VoxelOre("석탄", 5, 2);
VoxelOre diamond = new VoxelOre("다이아몬드", 50, 1);
settlement.recordMining(coal, 1);
settlement.recordMining(diamond, 2); // 50 * 1 * 2 = 100 경험치
System.out.printf("채굴자: %s%n", settlement.getMinerName());
System.out.printf("누적 경험치: %d EXP%n", settlement.getAccumulatedExp());
System.out.printf("기록된 채굴 건수: %d건%n", settlement.getMinedHistory().size());
// [엣지 케이스 검증 1] 음수 도구 배율 방어
System.out.println("\n[테스트 1] 음수 배율 입력 시 기본 1배 보정 검증:");
VoxelOre iron = new VoxelOre("철", 10, 3);
settlement.recordMining(iron, -5); // -5 대신 최소 1배 적용 -> 30 EXP 증가
System.out.printf("보정 후 누적 경험치: %d EXP (기대값: 140)%n", settlement.getAccumulatedExp());
// [엣지 케이스 검증 2] 생성자 유효성 검증 실패 (음수 경험치)
System.out.println("\n[테스트 2] 비정상 광물 데이터 투입 방어:");
try {
new VoxelOre("오염된 광물", -20, 1);
} catch (IllegalArgumentException ex) {
System.out.println("성공적으로 예외 차단: " + ex.getMessage());
}
// [엣지 케이스 검증 3] 외부 리스트 변조 시도 차단
System.out.println("\n[테스트 3] 반환된 내역 리스트 임의 수정 시도:");
try {
settlement.getMinedHistory().clear();
} catch (UnsupportedOperationException ex) {
System.out.println("불변 뷰로 외부 변조 시도 방어 확인!");
}
System.out.println("\n모든 엣지 케이스 검증 통과!");
}
}
예상 출력
=== AI 생성 코드 검증 및 엣지 케이스 방어 실습 ===
채굴자: Alex
누적 경험치: 110 EXP
기록된 채굴 건수: 2건
[테스트 1] 음수 배율 입력 시 기본 1배 보정 검증:
보정 후 누적 경험치: 140 EXP (기대값: 140)
[테스트 2] 비정상 광물 데이터 투입 방어:
성공적으로 예외 차단: 기본 경험치는 음수일 수 없습니다: -20
[테스트 3] 반환된 내역 리스트 임의 수정 시도:
불변 뷰로 외부 변조 시도 방어 확인!
모든 엣지 케이스 검증 통과!
AI에게 엣지 케이스를 역질문하는 고급 프롬프트 기법
AI에게 코드를 작성해 달라고만 하면 가장 평이한 코드만 제공합니다. 하지만 AI에게 **“이 코드의 허점을 네가 직접 찾아내 봐”**라고 역할을 부여하면, 놀라울 정도로 정교한 테스트 시나리오를 쏟아냅니다.
실무에서 활용할 수 있는 역질문 프롬프트 패턴은 다음과 같습니다.
### [AI 역질문 프롬프트 예시]
"아래 작성된 MiningSettlement 클래스를 면밀히 검토해 주세요.
당신은 악의적인 입력으로 시스템을 다운시키려는 QA 보안 테스터입니다.
1. 이 클래스에서 NullPointerException이나 ArithmeticException을 일으킬 수 있는 입력 시나리오를 3가지 지적하세요.
2. 빈 리스트나 0, 음수 값이 전달되었을 때 비즈니스 규칙이 깨지는 엣지 케이스를 찾으세요.
3. 이를 증명할 수 있는 JUnit 5 단위 테스트 케이스 3개를 @Test 메서드로 작성하세요."
이러한 프롬프트를 실행하면 AI는 자신이 방금 만든 코드의 맹점을 스스로 분석하여 경계값 테스트 케이스를 생성해 주며, 개발자는 이를 통해 누락된 방어 코드를 손쉽게 보강할 수 있습니다.
직접 해보기
제공된 실습 코드를 기반으로 광물 채굴 시스템에 새로운 엣지 케이스 방어 요구사항을 추가해 보세요.
public class Main {
// 엣지 케이스 검증 실습을 진행합니다.
public static void main(String[] args) {
System.out.println("AI 코드 검증 실습을 시작합니다.");
}
}
미션 과제
- 상황 분석: 채굴 도구의 내구도(Durability) 개념을 추가한다고 가정합니다. 채굴할 때마다 도구 내구도가 1씩 감소하며, 내구도가 0이 되면 채굴이 불가능해야 합니다.
- 엣지 케이스 도출: 내구도가 정확히 1인 상태에서 연속 2회 채굴 요청이 들어올 때의 경계 조건, 그리고 도구 내구도가 음수로 초기화되는 비정상 상황을 방어하는 검증 로직을 구상하세요.
- AI 프롬프트 작성: 앞서 배운 역질문 프롬프트 템플릿을 활용해 “도구 내구도 소진 시의 동시성 및 경계 조건 방어 코드”를 요청하는 프롬프트를 작성해 보세요.
- ToolPado 도구 연계: 채굴 로그 데이터의 타임스탬프가 유효한지 검증하기 위해 Timestamp Converter 도구를 활용해 밀리초 단위 시간 변환을 점검해 보세요.