Chapter 15

IntelliJ IDEA와 디버깅

실무 표준 IDE인 IntelliJ IDEA의 프로젝트 구성, 필수 생산성 단축키, 중단점과 단계별 디버거 활용법을 voxel 게임 로직으로 실습합니다.

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

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

기능 구분

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

정식 도구 설정

Java 25 SDK를 IDE에 연결해 기존 Java SE 정식 기능을 개발하는 환경을 다룹니다.

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

Learning goals

오늘 배울 내용

  • IntelliJ IDEA에서 Java 25 SDK 기반 새 프로젝트 만들기
  • 코드 탐색과 자동 완성을 돕는 필수 단축키 익히기
  • Breakpoint와 디버거 패널로 런타임 변수 상태 추적하기
  • 조건부 중단점으로 반복 루프 속 버그 격리하기

실무 개발자가 IDE를 선택하는 이유

텍스트 에디터와 터미널에서 javacjava 명령어로 코드를 컴파일하고 실행하는 방식은 언어의 기초 동작 원리를 이해하는 데 큰 도움이 됩니다. 하지만 프로젝트 규모가 커져 수십 개 이상의 클래스가 서로 관계를 맺기 시작하면 파일 간 이동, 오탈자 즉시 감지, 의존성 라이브러리 연동, 복잡한 런타임 상태 확인을 수작업으로 감당하기 어렵습니다.

IntelliJ IDEA(인텔리제이 아이디어)는 현대 Java 생태계에서 가장 널리 쓰이는 통합 개발 환경(IDE)입니다. Java 25 LTS의 최신 언어 스펙을 온전히 인식하여 문법 오류를 입력 즉시 붉은 밑줄로 경고하고, 객체 타입에 맞는 메서드 목록을 문맥에 따라 지능적으로 추천합니다. 무엇보다 프로그램이 멈추지 않고 지나쳐버리는 런타임 메모리 상태를 한 줄씩 멈춰 서서 들여다볼 수 있는 디버거(Debugger)는 개발자의 버그 수정 시간을 획기적으로 줄여줍니다.

프로젝트 생성과 Java 25 SDK 연동

IntelliJ IDEA를 처음 실행한 뒤 New Project 창을 열면 프로젝트의 기본 속성을 설정하게 됩니다. 이때 가장 중요한 점은 컴파일과 실행을 담당할 JDK(Java Development Kit) 버전을 명확히 지정하는 것입니다.

  1. Name과 Location 설정: 프로젝트 이름(예: VoxelAdventure)과 디렉터리 경로를 지정합니다.
  2. Language 및 Build System 선택: Language는 Java, Build system은 초기 학습 단계에서는 IntelliJ를, 이후 다룰 빌드 자동화 단계에서는 Gradle을 선택합니다.
  3. JDK 선택: 시스템에 이미 설치된 Java 25 LTS를 선택하거나, 드롭다운 메뉴의 Download JDK 항목을 눌러 Eclipse Temurin 또는 Oracle OpenJDK 25 버전을 직접 내려받습니다.
  4. 프로젝트 구조 확인: 좌측 Project 도구 창(단축키 Alt + 1 또는 Cmd + 1)을 열면 소스 코드가 위치하는 src 디렉터리가 보입니다. 이 폴더 안에 패키지와 자바 소스 파일(.java)을 생성합니다.

새 프로젝트가 준비되면 src 우클릭 후 New → Java Class를 선택해 클래스를 생성할 수 있습니다. IntelliJ는 파일명과 public class 이름이 일치해야 하는 자바 규칙을 자동으로 반영해 줍니다.

생산성을 2배로 높이는 필수 단축키와 템플릿

숙련된 개발자는 마우스에 손을 올리는 횟수를 최소화하고 키보드 단축키로 코드를 읽고 편집합니다. IntelliJ에서 가장 자주 쓰이는 단축키 체계를 익혀두면 코드 작성 속도가 비약적으로 향상됩니다.

작업 Windows / Linux macOS 실무 활용 상황
전체 파일·클래스 검색 Shift + Shift Shift + Shift 클래스나 설정 파일 이름을 즉시 찾아 열기
빠른 수정 및 제안 Alt + Enter Option + Enter 에러 수정, import 추가, 문법 개선 자동 반영
코드 자동 서식 정리 Ctrl + Alt + L Cmd + Option + L 들여쓰기, 줄바꿈, 연산자 공백 표준화
선언부 이동 및 사용처 추적 Ctrl + B 또는 클릭 Cmd + B 또는 클릭 메서드 정의부나 호출 위치로 단번에 점프
이름 일괄 변경 (Refactor) Shift + F6 Shift + F6 변수·클래스 이름을 참조처까지 안전하게 수정
실행 및 디버그 Shift + F10 / Shift + F9 Ctrl + R / Ctrl + D 현재 파일 즉시 실행 또는 디버그 모드 진입

또한 psvm 또는 main을 입력한 뒤 Tab 키를 누르면 public static void main(String[] args) 선언이 자동으로 완성되며, sout을 입력하면 System.out.println() 구문이 바로 채워지는 라이브 템플릿(Live Templates) 기능도 적극 활용할 수 있습니다.

버그를 추적하는 디버거의 기본 메커니즘

프로그램이 예기치 않게 종료되거나 계산 결과가 기댓값과 다를 때, 초보자는 종종 의심되는 위치마다 System.out.println()을 여러 줄 추가해 변수를 찍어보곤 합니다. 그러나 이 방식은 출력문을 추가하고 컴파일하는 반복 노동을 유발하며, 객체 내부 컬렉션이나 다단계 호출 흐름을 한눈에 파악하기 어렵습니다.

디버거를 사용하면 실행 중인 JVM(Java Virtual Machine) 프로세스를 특정 소스 코드 라인에서 일시 중지시키고, 그 순간의 스택 프레임(Stack Frame)과 힙(Heap) 메모리에 할당된 객체 인스턴스의 실제 필드 값을 실시간으로 검사할 수 있습니다.

실습을 위해 voxel 모험 게임에서 플레이어가 몬스터와 차례로 조우하며 장비 내구도와 체력을 소모하는 코드를 준비했습니다. 아래 코드는 겉보기에 정상 컴파일되지만, 루프 내부의 상태 변화를 면밀히 추적해 볼 필요가 있습니다.

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

public class Main {
    record VoxelMonster(String name, int attackPower, int expReward) {}

    static class PlayerState {
        private final String name;
        private int health;
        private int armorDurability;
        private int totalExp;

        public PlayerState(String name, int health, int armorDurability) {
            this.name = name;
            this.health = health;
            this.armorDurability = armorDurability;
            this.totalExp = 0;
        }

        public void takeDamage(int rawDamage) {
            int absorbed = Math.min(this.armorDurability, rawDamage / 2);
            int finalDamage = Math.max(1, rawDamage - absorbed);

            this.armorDurability -= absorbed;
            this.health = Math.max(0, this.health - finalDamage);
        }

        public void gainExp(int exp) {
            this.totalExp += exp;
        }

        public boolean isAlive() {
            return this.health > 0;
        }

        @Override
        public String toString() {
            return String.format("Player[%s, HP=%d, Armor=%d, EXP=%d]",
                this.name, this.health, this.armorDurability, this.totalExp);
        }
    }

    public static void main(String[] args) {
        PlayerState player = new PlayerState("Steve", 100, 30);
        List<VoxelMonster> encounters = List.of(
            new VoxelMonster("Zombie", 16, 25),
            new VoxelMonster("Skeleton", 24, 40),
            new VoxelMonster("Creeper", 50, 80),
            new VoxelMonster("Enderman", 35, 60)
        );

        System.out.println("=== 모험 시작 ===");
        System.out.println("초기 상태: " + player);

        List<String> battleLogs = new ArrayList<>();

        for (int i = 0; i < encounters.size(); i++) {
            VoxelMonster monster = encounters.get(i);
            player.takeDamage(monster.attackPower());
            player.gainExp(monster.expReward());

            String log = String.format("Round %d vs %s -> 남은 HP: %d, 방어구: %d",
                i + 1, monster.name(), player.health, player.armorDurability);
            battleLogs.add(log);

            if (!player.isAlive()) {
                System.out.println("전투 중 쓰러졌습니다: " + monster.name());
                break;
            }
        }

        System.out.println("=== 모험 종료 ===");
        System.out.println("최종 상태: " + player);
        System.out.println("전투 기록 수: " + battleLogs.size());
    }
}

예상 출력

=== 모험 시작 ===
초기 상태: Player[Steve, HP=100, Armor=30, EXP=0]
=== 모험 종료 ===
최종 상태: Player[Steve, HP=25, Armor=6, EXP=205]
전투 기록 수: 4

중단점(Breakpoint) 설정과 단계별 실행 기법

디버거를 가동하려면 먼저 실행을 일시 정지할 위치를 지정해야 합니다. 코드 에디터 좌측의 줄 번호(Line Number) 옆 빈 영역(Gutter)을 클릭하면 빨간색 원형 아이콘이 나타나며, 이것이 바로 **중단점(Breakpoint)**입니다.

중단점을 설정한 후 상단 도구 모음의 벌레 모양 아이콘을 누르거나 Shift + F9(macOS Ctrl + D)를 누르면 프로그램이 일반 실행 대신 디버그 모드로 구동됩니다. 실행 흐름이 중단점 줄에 도달하는 즉시 JVM은 일시 중지되며, IntelliJ 하단에 Debug 도구 창이 자동으로 열립니다.

디버그 도구 창에서 가장 핵심적인 제어 버튼은 다음 네 가지입니다.

  1. Step Over (F8): 현재 멈춰 선 소스 라인을 통째로 실행하고 바로 다음 줄로 이동합니다. 현재 줄에 다른 메서드 호출이 있어도 그 메서드 내부로 들어가지 않고 결과만 받아옵니다.
  2. Step Into (F7): 현재 줄에서 호출되는 사용자 정의 메서드 내부로 실행 포인터를 이동시켜 메서드 첫 줄부터 한 단계씩 검사합니다.
  3. Step Out (Shift + F8): Step Into로 들어간 메서드의 나머지 줄을 한 번에 실행하고, 자신을 호출했던 상위 메서드로 즉시 복귀합니다.
  4. Resume Program (F9): 다음 중단점을 만날 때까지 일시 정지를 풀고 프로그램을 계속 실행합니다. 더 이상 중단점이 없다면 정상 종료될 때까지 달립니다.

변수 검사와 조건부 중단점(Conditional Breakpoint)

디버그 창의 Variables 탭에는 현재 활성화된 스택 프레임의 모든 로컬 변수와 this 객체의 상태가 트리 구조로 표시됩니다. 컬렉션 객체를 펼치면 size와 각 인덱스 요소의 필드까지 실시간으로 확인할 수 있습니다.

변수 값 즉석 평가 (Evaluate Expression)

복잡한 계산식이나 메서드 반환값을 멈춰 있는 상태에서 미리 계산해 보고 싶다면 Alt + F8(macOS Option + F8)을 눌러 Evaluate Expression 창을 띄웁니다. 여기에 player.health > 50이나 encounters.get(2).attackPower() * 1.5 같은 자바 표현식을 입력하면 현재 런타임 메모리 상태를 토대로 즉각적인 평가 결과를 보여줍니다.

조건부 중단점(Conditional Breakpoint) 활용

만약 루프가 수천 번 도는 데이터 처리 중 특정 조건에서만 멈추고 싶다면 빨간색 중단점 아이콘을 우클릭합니다. 나타나는 팝업 메뉴의 Condition 입력란에 자바 불리언 식을 지정할 수 있습니다.

예를 들어 위 예제의 player.takeDamage(monster.attackPower()); 줄에 중단점을 걸고 조건식으로 다음을 입력할 수 있습니다.

monster.name().equals("Creeper")

이렇게 설정해 두면 Zombie나 Skeleton과 만날 때는 중단점이 멈추지 않고 지나가며, 오직 Creeper를 조우해 루프가 실행되는 순간에만 디버거가 자동으로 일시 중지됩니다. 무의미한 F8 연타를 피할 수 있어 대규모 배열이나 컬렉션을 다룰 때 필수적인 기법입니다.

리팩토링 기능으로 코드 품질 가꾸기

IntelliJ IDEA의 강력함은 단순한 오탈자 검출에 그치지 않고, 복잡하고 지저분해진 코드를 안전하게 구조화하는 리팩토링(Refactoring) 엔진에 있습니다.

  • Extract Method (Ctrl + Alt + M / Cmd + Option + M): 길어진 메서드에서 일부 로직 블록을 블록 지정한 뒤 단축키를 누르면, 매개변수와 반환 타입을 분석해 독립된 새 메서드로 자동 분리해 줍니다.
  • Introduce Variable (Ctrl + Alt + V / Cmd + Option + V): 복잡하게 얽힌 계산식의 결과를 알기 쉬운 이름의 지역 변수로 뽑아내 가독성을 높입니다.
  • Rename (Shift + F6): 변수나 클래스 이름을 바꿀 때 단순 문자열 치환이 아닌 구문 트리를 기반으로 해당 심볼을 참조하는 모든 파일의 위치를 한 번에 안전하게 갱신합니다.

직접 해보기

아래 실습 코드는 플레이어의 장비 강화와 몬스터 처치 로직을 담고 있습니다. 코드를 직접 입력하고 중단점과 디버거를 설정해 동작을 분석해 보세요.

public class Main {
    record Equipment(String partName, int level, int defenseBonus) {}

    public static int calculateTotalDefense(Equipment[] gear) {
        int sum = 0;
        for (Equipment eq : gear) {
            int effectiveBonus = eq.defenseBonus() + (eq.level() * 2);
            sum += effectiveBonus;
        }
        return sum;
    }

    public static void main(String[] args) {
        Equipment[] inventory = new Equipment[] {
            new Equipment("투구", 2, 5),
            new Equipment("갑옷", 3, 12),
            new Equipment("부츠", 1, 3)
        };

        int totalDefense = calculateTotalDefense(inventory);
        System.out.println("장착 장비 총 방어력: " + totalDefense);
    }
}

미션 과제

  1. calculateTotalDefense 메서드의 sum += effectiveBonus; 줄에 중단점을 배치하세요.
  2. 디버그 모드(Shift + F9)로 실행한 뒤 Variables 창에서 각 장비(eq)의 partName, level, defenseBonus, 그리고 계산된 effectiveBonus 값이 회차마다 어떻게 변하는지 F8(Step Over)을 누르며 확인하세요.
  3. 중단점 조건(Condition)에 eq.partName().equals("갑옷")을 입력한 뒤 재실행하여 오직 갑옷을 계산할 때만 실행이 일시 정지되는지 검증하세요.
  4. Shift + F6을 사용해 effectiveBonus 변수명을 calculatedDefense로 변경해 보고, 참조 위치가 모두 안전하게 자동 갱신되는지 확인하세요.

FAQ

자주 묻는 질문

무료 Community 버전으로도 Java 25 개발과 디버깅이 가능한가요?

네, IntelliJ IDEA Community 에디션은 Java 언어의 모든 최신 문법 지원과 강력한 디버거, 단위 테스트 러너를 기본 포함하므로 학습과 실무 기초 개발에 충분합니다.

println 출력으로 확인하는 것과 디버거를 사용하는 것은 어떤 차이가 있나요?

println은 코드를 매번 고치고 다시 컴파일해야 하지만, 디버거는 코드 수정 없이 원하는 시점에 실행을 멈추고 호출 스택(Call Stack)과 메모리 객체 필드를 입체적으로 검사할 수 있습니다.

조건부 중단점(Conditional Breakpoint)은 언제 쓰나요?

루프가 수백 번 반복되는 도중 특정 플레이어 체력이 0이 되거나 특정 아이템 ID가 나타나는 순간에만 멈추고 싶을 때 조건식을 지정해 불필요한 일시정지를 건너뜁니다.

작성·편집 ToolPado

콘텐츠 유형 Java 학습 가이드

Java 기준 Java 25 LTS

마지막 검토