Chapter 18

AI에게 의도를 명확히 질문하기

AI 코딩 어시스턴트에게 정확하고 실용적인 Java 코드를 얻기 위한 컨텍스트 구성법, 오류 로그 전달 기법, 재현 가능한 최소 예제(MRE) 작성 원칙을 학습합니다.

난이도
초급
예상 학습
50분
Java 기준
Java 25 LTS
최종 검토

전체 22개 Chapter 확장 구조 중 Chapter 18입니다.

기능 구분

정식 Java 25에서도 쓸 수 있는 이전 버전 기능

Java SE 정식 기능 기준

AI 프롬프트에서 Java 25 기준을 명시하되 Preview 또는 Incubator API 예제는 사용하지 않습니다.

이 Chapter의 예제는 Preview 또는 Incubator API 실행 옵션 없이 Java 25 LTS 정식 기능 기준으로 읽을 수 있습니다.

Learning goals

오늘 배울 내용

  • 막연한 에러 호소 대신 AI가 즉시 원인을 파악할 수 있는 4단계 질문 구조 익히기
  • Java 25 LTS 환경과 JDK 버전, 의존성 라이브러리 정보를 컨텍스트로 제공하기
  • Stack Trace 로그에서 핵심 예외 메시지와 발생 위치를 추출해 전달하기
  • 군더더기를 제거하고 버그만 독립적으로 격리한 재현 가능한 최소 예제(MRE) 작성하기

모호한 질문이 실패하는 이유

현대 소프트웨어 개발 환경에서 AI 어시스턴트는 더 이상 단순한 검색 엔진의 대체재가 아닙니다. 메서드의 뼈대를 잡거나 난해한 정규식을 구성하고, 컴파일 오류의 근본 원인을 추적하는 강력한 페어 프로그래밍 파트너입니다. 하지만 수많은 초보 개발자가 “코드가 왜 안 되나요?”, “빨간 줄이 떠요”와 같이 맥락 없는 질문을 던지고, AI가 내놓은 부정확하거나 엉뚱한 코드에 실망하곤 합니다.

인공지능 대형 언어 모델(LLM)은 질문자가 제공한 **문맥(Context)**을 바탕으로 다음 토큰을 통계적으로 예측합니다. 사용자의 프로젝트가 Java 8 환경인지 최신 Java 25 LTS인지, 어떤 빌드 도구를 쓰는지, 메모리에 어떤 데이터가 들어갔을 때 에러가 터졌는지를 알려주지 않으면 AI는 가장 흔하고 평범한 일반론을 지어낼 수밖에 없습니다.

정보가 부족할 때 발생하는 대표적인 문제는 다음과 같습니다.

  1. 시대착오적 구버전 문법 제안: 레코드(Record)나 최신 패턴 매칭(Pattern Matching) 대신 10년 전 Java 7 스타일의 장황한 보일러플레이트 코드를 제시합니다.
  2. 존재하지 않는 가상 라이브러리 참조: 질문자의 의도를 과도하게 넘겨짚어 실제로 존재하지 않는 외부 메서드를 그럴듯하게 날조(환각, Hallucination)합니다.
  3. 핵심 원인 비켜가기: 런타임 데이터 불일치로 터진 예외인데 엉뚱하게 환경 변수나 IDE 캐시 문제라고 안내하여 디버깅 시간을 허비하게 만듭니다.

양질의 답변을 얻기 위해서는 질문 자체가 하나의 잘 작성된 버그 리포트(Bug Report)나 기술 문서처럼 구조화되어 있어야 합니다.

AI 질문의 4대 핵심 구성 요소 (C-G-E-A 패턴)

숙련된 실무 개발자는 AI에게 질문할 때 다음 네 가지 정보를 반드시 포함합니다. 이를 C-G-E-A 패턴이라고 부릅니다.

  1. 컨텍스트(Context): 사용 중인 언어 버전, 프레임워크, 라이브러리, 그리고 현재 작업 중인 도메인 배경
  2. 목표(Goal): 작성하려는 기능의 구체적인 입력과 기대 출력값
  3. 현재 상태와 오류(Error & Current State): 실제로 발생한 컴파일 에러 메시지나 런타임 Stack Trace 전문
  4. 시도한 해결책 및 제약 사항(Attempts & Constraints): 본인이 이미 확인한 가설, 그리고 수정 시 건드리면 안 되는 기존 클래스나 성능 제약 조건

이 네 가지를 정리해 프롬프트로 전달하면, AI는 불필요한 추측을 멈추고 문제의 정곡을 찌르는 코드와 해설을 제공합니다.

구분 나쁜 질문 예시 좋은 질문 예시
환경 명시 “자바에서 맵 쓸 때 에러 나요.” “Java 25 LTS 환경이며 Gradle 프로젝트입니다. Map<String, List> 구조를 다루고 있습니다.”
오류 전달 “배열 인덱스 오류 같은 게 뜹니다.” IndexOutOfBoundsException: Index 5 out of bounds for length 5Inventory.java:42에서 발생합니다.”
요구사항 “인벤토리 좀 깔끔하게 짜주세요.” “최대 9칸 슬롯을 가지며, 동일 아이템은 최대 64개까지 중첩(Stack)되는 불변 레코드 기반 인벤토리 메서드가 필요합니다.”
제약 사항 “그냥 알아서 고쳐주세요.” “기존의 VoxelItem 불변성을 해치지 않아야 하며, 외부 서드파티 라이브러리 없이 Java 표준 컬렉션만 사용해야 합니다.”

스택 트레이스(Stack Trace)의 해부와 올바른 전달

프로그램 실행 중 예외가 발생하면 콘솔에 수십 줄의 붉은 텍스트가 쏟아져 나옵니다. 초보 개발자는 이 압도적인 글자 수에 당황하지만, 실제로 버그 추적에 결정적인 정보는 상단의 2~3줄과 본인 소스 코드의 위치입니다.

스택 트레이스에서 반드시 추출하여 AI에게 넘겨주어야 하는 핵심 요소는 세 가지입니다.

  1. 예외 클래스 이름: java.lang.NullPointerException, java.lang.IllegalArgumentException 등 문제의 본질
  2. 상세 메시지: Cannot invoke "String.toLowerCase()" because "slotName" is null과 같이 런타임이 친절하게 가리키는 원인
  3. 발생 위치 파일과 줄 번호: at VoxelInventory.addItem(VoxelInventory.java:38)처럼 내가 작성한 패키지의 첫 번째 지점

외부 프레임워크나 런타임 내부 메서드가 수십 번 연속 호출된 부분(at java.base/java.util...)은 핵심만 남기고 생략해도 무방하지만, 처음 등장하는 에러의 시작점과 Caused by: 절은 절대 누락해서는 안 됩니다.

재현 가능한 최소 예제(MRE) 작성 원칙

프로젝트 전체의 수십 개 파일을 그대로 프롬프트 창에 쏟아붓는 것은 컨텍스트 윈도우를 낭비할 뿐만 아니라 AI의 주의(Attention)를 분산시켜 환각을 유발합니다.

질문하기 전, 버그가 일어나는 핵심 로직만 30~50줄 이내의 단일 public class Main 파일로 격리하는 과정을 **MRE(Minimum Reproducible Example: 재현 가능한 최소 예제)**라고 합니다.

MRE를 만들 때 지켜야 할 세 가지 수칙은 다음과 같습니다.

  1. 단일 클래스화: 외부 파일 참조나 복잡한 패키지 의존성을 없애고 하나의 .java 파일 안에서 즉시 컴파일되도록 만듭니다.
  2. 정적 데이터 사용: 데이터베이스 연결이나 사용자 키보드 입력 대신 하드코딩된 리스트나 배열로 에러 상황을 강제 재현합니다.
  3. 불필요한 기능 제거: 로그인, 사운드, 그래픽 렌더링 같은 곁가지 로직을 모두 걷어내고 순수 자바 데이터 처리만 남깁니다.

아래는 voxel 블록 어드벤처 게임에서 인벤토리에 아이템을 보관하고 정렬하는 과정 중 용량 초과와 중첩 병합 예외가 발생하는 상황을 MRE로 정제한 예제입니다.

실행 가능한 MRE 전체 코드

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {
    record VoxelItem(String name, int quantity, int maxStack) {
        public VoxelItem {
            if (quantity < 0) {
                throw new IllegalArgumentException("수량은 음수일 수 없습니다: " + quantity);
            }
        }

        public VoxelItem withQuantity(int newQuantity) {
            return new VoxelItem(this.name, newQuantity, this.maxStack);
        }
    }

    static class InventoryChest {
        private final int capacity;
        private final List<VoxelItem> slots;

        public InventoryChest(int capacity) {
            this.capacity = capacity;
            this.slots = new ArrayList<>();
        }

        public void store(VoxelItem incoming) {
            // 1단계: 이미 동일한 아이템이 보관된 슬롯이 있다면 중첩 수량 추가 시도
            for (int i = 0; i < slots.size(); i++) {
                VoxelItem current = slots.get(i);
                if (current.name().equals(incoming.name())) {
                    int spaceLeft = current.maxStack() - current.quantity();
                    if (spaceLeft >= incoming.quantity()) {
                        slots.set(i, current.withQuantity(current.quantity() + incoming.quantity()));
                        return;
                    }
                }
            }

            // 2단계: 기존 슬롯에 합치지 못했다면 새 슬롯에 추가
            if (slots.size() >= capacity) {
                throw new IllegalStateException(
                    String.format("상자 공간 부족! 최대 수용 슬롯: %d, 현재 사용 중: %d", capacity, slots.size())
                );
            }
            slots.add(incoming);
        }

        public List<VoxelItem> getSlots() {
            return Collections.unmodifiableList(slots);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Voxel 상자 보관 시뮬레이션 ===");
        InventoryChest chest = new InventoryChest(3);

        // 정상 보관: 3개의 서로 다른 광물 블록 적재
        chest.store(new VoxelItem("다이아몬드 블록", 10, 64));
        chest.store(new VoxelItem("금 블록", 32, 64));
        chest.store(new VoxelItem("다이아몬드 블록", 20, 64)); // 기존 슬롯에 병합됨

        System.out.println("현재 슬롯 상태:");
        for (VoxelItem item : chest.getSlots()) {
            System.out.printf(" - %s: %d개 (최대 %d개)%n", item.name(), item.quantity(), item.maxStack());
        }

        // 의도적인 공간 초과 유발: 4번째 서로 다른 아이템 투입
        System.out.println("\n초과 적재 시도:");
        try {
            chest.store(new VoxelItem("철 블록", 16, 64));
            chest.store(new VoxelItem("청금석 블록", 8, 64)); // 여기서 IllegalStateException 발생
        } catch (IllegalStateException ex) {
            System.out.println("[예외 포착] " + ex.getMessage());
        }
    }
}

예상 출력

=== Voxel 상자 보관 시뮬레이션 ===
현재 슬롯 상태:
 - 다이아몬드 블록: 30개 (최대 64개)
 - 금 블록: 32개 (최대 64개)

초과 적재 시도:
[예외 포착] 상자 공간 부족! 최대 수용 슬롯: 3, 현재 사용 중: 3

프롬프트 체이닝(Prompt Chaining)과 점진적 질문 기법

복잡한 Java 시스템을 구축할 때 모든 요구사항을 한 번의 거대한 프롬프트에 담아 “한 번에 다 짜주세요”라고 요청하면, AI는 코드 중간을 주석(// TODO: 여기에 나머지 구현)으로 생략하거나 디테일한 예외 처리를 빠뜨리기 쉽습니다.

이를 극복하기 위해 단계별로 질문을 이어가는 프롬프트 체이닝(Prompt Chaining) 기법을 활용합니다.

  1. 1단계: 데이터 모델링 질문: “Java 25 record를 사용해 게임의 아이템, 플레이어, 인벤토리 도메인의 인터페이스와 필드 구조만 먼저 정의해 줘.”
  2. 2단계: 비즈니스 규칙 메서드 구현: “방금 정의한 모델을 바탕으로, 중첩 제한(Stack Limit)을 고려해 아이템을 슬롯에 분할 삽입하는 핵심 메서드 알고리즘을 작성해 줘.”
  3. 3단계: 경계값 단위 테스트 요청: “이 알고리즘에서 인벤토리가 가득 찼거나 아이템 수량이 음수일 때를 검증하는 JUnit 5 단위 테스트 3가지를 작성해 줘.”
  4. 4단계: 성능 및 클린코드 리팩토링: “현재 O(N) 순회 방식을 HashMap을 결합해 O(1) 조회가 가능하도록 최적화해 줘.”

이렇게 단계를 쪼개어 검증하면서 살을 붙여 나가면 개발자가 중간중간 코드를 직접 실행해 보며 통제권을 쥐고 갈 수 있습니다.

보안과 정보 보호: AI 질문 시 절대 보내지 말아야 할 것들

AI 도구와의 협업이 일상화되면서 무심코 회사나 개인의 민감 정보가 프롬프트로 전송되는 보안 사고가 빈번하게 발생하고 있습니다.

질문을 전송하기 전 다음 체크리스트를 반드시 확인하세요.

  • API Key 및 비밀번호: 데이터베이스 비밀번호, 클라우드 접근 토큰, OAuth 시크릿 키는 반드시 DUMMY_KEYYOUR_SECRET_HERE와 같은 더미 값으로 치환합니다.
  • 실제 개인정보(PII): 고객의 실명, 전화번호, 이메일, 계좌번호는 “홍길동”, test@example.com 등 가상 데이터로 바꿉니다.
  • 내부 서버 주소 및 사내 IP: 10.x.x.x 사내망 IP나 내부 도메인 주소는 api.example.com과 같은 공개 도메인 형식으로 변경합니다.
  • 전체 독점 소스 코드: 기업의 핵심 영업 비밀이 담긴 전체 모듈을 통째로 올리지 말고, 순수 알고리즘이나 자료구조 문제로 추상화하여 질문합니다.

AI 질문을 위한 실무 프롬프트 템플릿

실제 개발 현장에서 버그를 문의하거나 새 기능을 요청할 때는 아래의 표준 템플릿을 복사해 사용하면 가장 높은 품질의 응답을 이끌어낼 수 있습니다.

### [개발 환경]
- JDK: Java 25 LTS
- 빌드 도구: Gradle 8.10
- 제약 사항: 외부 라이브러리 없이 표준 Java SDK만 사용, 불변 레코드(Record) 지향

### [해결하려는 문제 및 비즈니스 규칙]
Voxel 아이템을 보관 상자에 넣을 때, 단일 슬롯의 최대 중첩 수량(maxStack)을 초과하는 
큰 수량(예: 64개 제한인 다이아몬드를 한 번에 100개 넣음)이 들어오면 분할 적재되지 못하고 있습니다.

### [현재 코드 (MRE)]
```java
// 핵심 재현 코드 30줄 내외
```

### [발생한 오류 또는 런타임 결과]
- 발생 에러: IllegalStateException: 상자 공간 부족
- 원인 분석: 기존 슬롯의 잔여 공간을 채우고 남은 수량을 새 슬롯으로 넘기는 분할 로직 누락

### [요청 사항]
100개의 다이아몬드가 들어오면 첫 번째 슬롯에 64개를 채우고, 
남은 36개는 다음 빈 슬롯을 찾아 자동으로 나누어 저장되도록 리팩토링해 주세요.
만약 상자 전체 공간이 부족하다면 채울 수 있는 만큼만 채우고 남은 잉여 수량을 반환값으로 알려주세요.

직접 해보기

제공된 Main 클래스의 상자 보관 시스템을 바탕으로 AI와 대화하며 코드를 개선하는 훈련을 진행해 보세요.

public class Main {
    // 위 실습 코드를 바탕으로 분할 적재(Split Stacking) 요구사항을 구현합니다.
    public static void main(String[] args) {
        System.out.println("AI 협업 질문 실습을 시작합니다.");
    }
}

미션 과제

  1. 상황 설정: chest.store(new VoxelItem("레드스톤", 80, 64));를 실행했을 때 한 슬롯에 80개가 전부 들어가 maxStack 규칙을 위반하는 결함을 가정해 보세요.
  2. 프롬프트 작성: C-G-E-A 원칙에 따라 이 결함을 설명하고, 초과된 16개를 새 슬롯에 나누어 담도록 리팩토링해 달라는 200자 이상의 구조화된 질문 문안을 마크다운으로 직접 작성해 보세요.
  3. AI 응답 비판적 검토: AI가 제안한 코드가 기존의 불변 레코드(record VoxelItem) 성질을 훼손하지 않았는지, capacity를 초과할 때 안전하게 예외나 잔여량을 처리하는지 확인해 보세요.
  4. ToolPado 도구 연계: 아이템 데이터 규격을 공유하거나 API 통신을 모사할 때 JSON Formatter 도구를 활용해 복합 아이템 리스트의 직렬화 형태를 정돈해 보세요.

FAQ

자주 묻는 질문

AI에게 "코드가 안 돌아가요, 고쳐주세요"라고 질문하면 왜 엉뚱한 답변이 나오나요?

AI는 사용자의 프로젝트 설정, 사용 중인 Java 버전, 정확한 입력값과 예외 스택 트레이스를 알 수 없습니다. 정보가 비어 있으면 AI는 확률적으로 가장 흔한 일반적 가정을 세워 답변하므로 실제 원인과 무관한 코드를 제시할 가능성이 매우 높습니다.

오류 메시지가 수십 줄에 달할 때 전부 다 복사해서 전달해야 하나요?

첫 줄의 예외 타입과 설명(예: NullPointerException, IndexOutOfBoundsException), 그리고 프로젝트 소스 코드가 직접 가리키는 호출 줄 번호가 담긴 최초 3~5줄의 스택 트레이스 프레임이 가장 중요합니다. 라이브러리 내부의 무관한 수십 줄은 생략하거나 핵심만 추려 전달하면 AI의 분석 정확도가 올라갑니다.

Java 25 최신 문법을 사용할 때 AI 질문에서 유의할 점은 무엇인가요?

LLM 모델의 학습 데이터 기준일에 따라 최신 Java 버전 문법을 아직 미리보기(Preview)로 오해하거나 구버전 문법(Java 8~17 스타일)으로 강등하여 답할 수 있습니다. 따라서 프롬프트 서두에 "Java 25 LTS 표준 문법을 준수하고 레코드와 패턴 매칭을 활용해 주세요"처럼 기술 기준을 명시해야 합니다.

작성·편집 ToolPado

콘텐츠 유형 Java 학습 가이드

Java 기준 Java 25 LTS

마지막 검토