Skip to main content

7. 패키지, 제네릭, 열거형, 애너테이션 이해하기

지금까지는 자바의 문법과 객체지향의 핵심 개념을 익히며, 프로그램을 "어떻게 동작하게 만들 것인가"에 집중해 왔다. 이제부터는 한 단계 더 나아가, "어떻게 정리하고 안전하게 확장할 것인가"를 생각해야 한다. 작은 예제에서는 클래스 몇 개만 있어도 충분하지만, 실제 프로그램은 파일이 늘어나고, 클래스가 많아지고, 여러 사람이 함께 작업하게 된다. 이때 중요한 것은 문법 자체보다 구조다.

이번 장에서 다룰 패키지, 제네릭, 열거형, 애너테이션은 바로 그 구조를 만드는 도구들이다. 패키지는 코드를 질서 있게 정리하게 해 주고, 제네릭은 타입을 더 안전하게 다루게 해 준다. 열거형은 의미 있는 상수 집합을 깔끔하게 표현하게 하고, 애너테이션은 코드에 추가 정보를 붙여 컴파일러나 프레임워크가 코드를 더 똑똑하게 이해하게 만든다.

처음에는 각각이 따로 노는 문법처럼 보일 수 있다. 하지만 규모가 커질수록 이 네 가지는 서로 연결되며 프로그램의 품질을 결정한다. 이번 장은 자바 학습에서 중요한 전환점이다.

패키지, 제네릭, 열거형, 애너테이션 이해하기

패키지란 무엇인가

패키지(package)는 관련된 클래스들을 묶어 두는 폴더이자 이름 공간이라고 생각하면 쉽다. 현실 세계에서 문서를 아무 이름 없이 한 방에 다 쌓아 두면 찾기도 어렵고, 같은 이름의 파일이 충돌하기도 한다. 자바에서도 클래스가 많아지면 같은 문제가 생긴다. 이때 패키지가 정리함 역할을 한다.

예를 들어 학생 정보를 다루는 클래스와 주문 정보를 다루는 클래스를 모두 한곳에 넣는 대신, 다음처럼 구분할 수 있다.

package com.example.school;

public class Student {
}
package com.example.shop;

public class Order {
}

패키지를 사용하면 클래스 이름이 같아도 구분할 수 있다. 예를 들어 com.example.school.Student와 com.example.academy.Student는 서로 다른 클래스다. 둘 다 Student라는 이름을 쓰지만, 전체 이름이 다르기 때문에 충돌하지 않는다.

자바에서 클래스의 전체 이름은 보통 다음처럼 구성된다.

패키지명 + 클래스명

이 전체 이름을 정규화된 이름 또는 완전한 이름이라고 생각하면 된다.

package 선언과 파일 위치의 관계

자바에서는 소스 파일의 첫 부분에 package를 선언한다.

package com.example.app;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello Java");
    }
}

이 선언은 단순한 메모가 아니다. 실제 파일 구조와도 연결된다. 보통 위 클래스는 다음과 같은 폴더 구조에 놓인다.

com/
  example/
    app/
      Main.java

즉, 패키지명은 폴더 구조와 맞춰 가는 것이 기본이다. 이 규칙을 지키면 프로젝트가 커져도 파일을 찾기 쉬워진다.

패키지 이름은 보통 모두 소문자로 작성한다. 회사나 조직의 도메인 이름을 뒤집어서 시작하는 관례도 자주 사용한다. 예를 들어 com.company.project 같은 형태다. 이렇게 하면 다른 프로젝트와 이름이 겹칠 가능성을 줄일 수 있다.

import는 왜 필요한가

다른 패키지에 있는 클래스를 사용하려면 그 클래스를 어디서 가져오는지 알려 주어야 한다. 이때 사용하는 것이 import다.

package com.example.app;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
    }
}

Scanner는 java.util 패키지에 있기 때문에, 그냥 이름만 쓰려면 import java.util.Scanner;가 필요하다.

만약 import를 쓰지 않으면 전체 이름을 매번 다 적어야 한다.

public class Main {
    public static void main(String[] args) {
        java.util.Scanner sc = new java.util.Scanner(System.in);
    }
}

이 방법도 틀린 것은 아니지만 길고 불편하다. 그래서 보통 import를 사용한다.

import의 종류와 주의점

자바에서 import는 크게 몇 가지 방식으로 사용된다.

import java.util.Scanner;
import java.util.ArrayList;

이처럼 필요한 클래스만 하나씩 가져오는 방식이 가장 읽기 쉽다.

import java.util.*;

이렇게 별표를 쓰면 java.util 패키지 안의 여러 클래스를 사용할 수 있다. 초반에는 편해 보이지만, 실제로는 어떤 클래스를 쓰는지 한눈에 보이지 않아 코드 가독성이 떨어질 수 있다. 학습 단계에서는 의미를 이해하는 데 도움이 될 수 있지만, 프로젝트가 커질수록 필요한 클래스만 명시적으로 import하는 습관이 좋다.

또 한 가지 주의할 점은, 패키지가 다르면 같은 클래스 이름이 충돌할 수 있다는 것이다.

import java.util.Date;
import java.sql.Date;

이런 식으로 같은 이름의 클래스를 동시에 가져오면 혼란이 생긴다. 이때는 하나를 import하고, 다른 하나는 전체 이름으로 쓰는 식으로 구분한다.

패키지와 접근 범위

패키지는 단지 정리용 폴더가 아니다. 접근 범위와도 연결된다. 자바의 접근 제어자 중에는 public, private, protected가 있고, 아무것도 쓰지 않는 기본 접근도 있다.

아무 접근 제어자도 붙이지 않으면 같은 패키지 안에서만 접근할 수 있다. 이를 보통 패키지 전용 접근, 또는 package-private이라고 부른다.

package com.example.store;

class ProductService {
    void printMessage() {
        System.out.println("같은 패키지에서만 접근 가능");
    }
}

이 클래스와 메서드는 같은 패키지 안에서는 사용할 수 있지만, 다른 패키지에서는 직접 접근할 수 없다. 즉, 패키지는 코드 정리뿐 아니라 "어디까지 공개할 것인가"를 나누는 경계가 되기도 한다.

규모가 있는 프로그램에서는 모든 클래스를 무조건 public으로 열어 두기보다, 밖에 보여 줄 것과 내부 구현을 구분하는 습관이 중요하다.

좋은 패키지 구조는 어떻게 만들까

처음에는 패키지를 대충 나눠도 잘 돌아간다. 하지만 클래스가 30개, 50개, 100개가 되면 그때부터 구조가 실력 차이로 드러난다.

패키지를 나눌 때는 보통 두 가지 관점이 있다. 하나는 역할별로 나누는 방식이고, 다른 하나는 기능별로 나누는 방식이다.

예를 들어 역할별로 나누면 이런 모습이 된다.

  • controller

  • service

  • repository

  • domain

기능별로 나누면 이런 식이 될 수 있다.

  • user

  • order

  • product

  • payment

학습 초기에는 어느 방식이 절대적으로 옳다고 단정할 필요는 없다. 중요한 것은 같은 기준으로 일관성 있게 나누는 것이다. 파일 위치만 보고도 "이 클래스가 어디에 속하는지" 감이 와야 한다.

제네릭이 필요한 이유

이제 구조를 정리했다면, 다음은 데이터를 더 안전하게 다루는 문제다. 제네릭(generic)은 클래스나 메서드가 다룰 타입을 미리 일반화해 두고, 사용할 때 구체적인 타입을 지정하게 만드는 기능이다.

처음 들으면 조금 딱딱하지만, 한마디로 말하면 "아무거나 담는 상자"를 "특정 타입만 담는 상자"로 바꾸는 기술이다.

예를 들어 리스트를 생각해 보자. 문자열 목록을 다루고 싶은데 아무 타입이나 들어가면 어떻게 될까?

ArrayList list = new ArrayList();
list.add("사과");
list.add(100);

이 코드는 여러 타입이 뒤섞일 수 있다. 그러면 나중에 값을 꺼낼 때 문자열이라고 믿고 사용했다가 문제가 생길 수 있다.

제네릭을 사용하면 이런 실수를 줄일 수 있다.

ArrayList<String> list = new ArrayList<>();
list.add("사과");
// list.add(100);  // 오류 발생

ArrayList<String>은 문자열만 담는 리스트라는 뜻이다. 컴파일 단계에서 잘못된 타입을 막아 주므로 더 안전하다. 이처럼 제네릭의 핵심 가치는 타입 안정성이다.

제네릭 문법의 기본 모양

제네릭은 보통 꺾쇠 괄호 <> 안에 타입을 적는 형태로 사용한다.

List<String> names;
Map<String, Integer> scores;

여기서 String, Integer 같은 것이 실제 타입 인자다. 그리고 T, E, K, V 같은 문자는 타입을 대신하는 기호로 자주 사용된다.

관례적으로 많이 쓰는 글자는 다음과 같다.

  • T: Type의 약자, 일반적인 타입

  • E: Element의 약자, 컬렉션 요소 타입

  • K: Key의 약자

  • V: Value의 약자

이 글자들은 문법상 반드시 이걸 써야 하는 것은 아니지만, 자바 개발자들 사이에서 널리 쓰이기 때문에 익숙해지는 것이 좋다.

제네릭 클래스 만들기

직접 제네릭 클래스를 만들 수도 있다. 예를 들어 어떤 값 하나를 보관하는 상자를 만든다고 해 보자.

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

T는 아직 구체적인 타입이 아니다. 이 클래스를 사용할 때 비로소 정해진다.

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setItem("자바");
        String value = stringBox.getItem();

        Box<Integer> intBox = new Box<>();
        intBox.setItem(100);
        int number = intBox.getItem();
    }
}

같은 Box 클래스인데도 문자열 상자, 정수 상자로 각각 사용할 수 있다. 코드 재사용성과 타입 안정성을 동시에 얻는 셈이다.

제네릭 메서드

클래스 전체가 아니라 메서드 하나만 제네릭으로 만들 수도 있다.

public class Utility {
    public static <T> void printValue(T value) {
        System.out.println(value);
    }
}

사용할 때는 이렇게 쓴다.

Utility.printValue("Hello");
Utility.printValue(123);
Utility.printValue(true);

이 메서드는 특정 타입 하나에 묶이지 않고, 전달받은 값의 타입에 맞게 동작한다. <T>는 "이 메서드에서 타입 매개변수 T를 사용하겠다"는 선언이다.

처음 보면 public static <T> void의 위치가 낯설다. 하지만 익숙해지면 자연스럽다. 반환형 앞의 <T>가 핵심이다.

왜 제네릭이 중요한가

제네릭은 단지 문법을 멋있게 보이게 하는 장식이 아니다. 실수를 미리 막는 안전장치다.

첫째, 잘못된 타입이 들어가는 것을 컴파일 단계에서 막아 준다. 실행 중에 터지는 오류보다, 작성 중에 바로 잡는 오류가 훨씬 낫다.

둘째, 형변환을 줄여 준다. 예전 방식처럼 Object로 다루면 값을 꺼낼 때마다 캐스팅이 필요하지만, 제네릭을 쓰면 그 부담이 줄어든다.

셋째, 코드의 의도가 더 분명해진다. List<String>만 봐도 "문자열 목록이구나" 하고 바로 이해된다.

즉, 제네릭은 프로그램이 커질수록 더 빛난다. 작은 예제에서는 없어 보여도 괜찮아 보이지만, 큰 프로그램에서는 안전벨트처럼 필수에 가깝다.

제네릭의 제한 사항

제네릭은 강력하지만 만능은 아니다. 초반에 자주 헷갈리는 제한이 몇 가지 있다.

가장 대표적인 것은 기본형을 직접 사용할 수 없다는 점이다.

// List<int> numbers;  // 불가능
List<Integer> numbers;

제네릭에는 int, double, boolean 같은 기본형 대신 Integer, Double, Boolean 같은 래퍼 클래스(wrapper class)를 사용해야 한다. 래퍼 클래스는 기본형을 객체처럼 다룰 수 있게 만든 클래스다.

또한 타입 매개변수로 객체를 새로 생성하는 것도 바로는 할 수 없다.

public class Box<T> {
    // T item = new T();  // 불가능
}

왜냐하면 실행 시점에는 T가 정확히 어떤 타입인지 직접 생성할 수 없는 경우가 있기 때문이다. 이런 제약은 나중에 리플렉션이나 팩토리 패턴 같은 주제를 배우면 더 자연스럽게 이해된다. 지금은 "제네릭은 타입을 안전하게 다루게 하지만, 모든 타입 관련 작업을 자유롭게 하게 해 주지는 않는다" 정도로 기억하면 충분하다.

와일드카드 맛보기

제네릭을 배우다 보면 ? 기호를 볼 때가 있다. 이것을 와일드카드라고 한다. 의미는 "어떤 타입인지 정확히 모르지만, 뭔가 타입이 있다"에 가깝다.

public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

이 메서드는 List<String>도 받고, List<Integer>도 받을 수 있다. 즉, 타입이 무엇이든 리스트를 출력하는 용도다.

와일드카드는 제네릭을 더 유연하게 만들어 주지만, 처음부터 깊게 파고들 필요는 없다. 지금 단계에서는 다음 정도만 잡으면 충분하다.

  • T는 "내가 타입을 정의해서 사용"

  • ?는 "정확한 타입은 모르지만 받아서 사용"

이 차이를 감으로 익혀 두면 이후 컬렉션과 스트림을 배울 때 훨씬 덜 낯설다.

열거형(enum)은 왜 쓰는가

열거형(enum)은 서로 관련 있는 상수들을 하나의 타입으로 묶는 기능이다. 상수는 변하지 않는 값이다. 예를 들어 요일, 계절, 주문 상태, 사용자 권한 같은 값들은 정해진 후보 안에서만 움직이는 경우가 많다.

초보 단계에서는 보통 문자열로 처리하고 싶어진다.

String status = "READY";

하지만 이 방식은 오타에 취약하다.

String status = "REDDY";

이런 실수는 컴파일러가 잡아 주지 못한다. 문자열은 그냥 문자열일 뿐이기 때문이다. 반면 enum을 사용하면 정해진 값만 선택할 수 있다.

public enum OrderStatus {
    READY, SHIPPED, DELIVERED, CANCELED
}
OrderStatus status = OrderStatus.READY;

이제 READY, SHIPPED, DELIVERED, CANCELED 중 하나만 사용할 수 있다. 오타나 잘못된 값 입력 가능성이 크게 줄어든다.

enum은 상수 묶음 그 이상이다

처음에는 enum을 단순히 상수 모음이라고 생각하기 쉽다. 하지만 자바의 enum은 사실 특별한 클래스에 가깝다. 그래서 필드, 생성자, 메서드를 가질 수 있다.

예를 들어 주문 상태마다 사용자에게 보여 줄 한글 메시지를 넣고 싶다고 해 보자.

public enum OrderStatus {
    READY("준비 중"),
    SHIPPED("배송 중"),
    DELIVERED("배송 완료"),
    CANCELED("주문 취소");

    private final String message;

    OrderStatus(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

사용은 이렇게 한다.

public class Main {
    public static void main(String[] args) {
        OrderStatus status = OrderStatus.SHIPPED;
        System.out.println(status.getMessage());
    }
}

이렇게 하면 단순한 문자열 상수보다 훨씬 의미 있고 안전한 코드를 만들 수 있다. 주문 상태와 그 상태의 설명이 한곳에 모여 있으니 관리도 쉽다.

enum과 switch의 궁합

열거형은 switch 문과 함께 사용하기도 좋다. 이미 조건문을 배웠으니, 이제 enum과 결합하면 더 읽기 쉬운 분기 처리가 가능하다.

public class Main {
    public static void printStatus(OrderStatus status) {
        switch (status) {
            case READY:
                System.out.println("상품을 준비하고 있습니다.");
                break;
            case SHIPPED:
                System.out.println("상품이 배송 중입니다.");
                break;
            case DELIVERED:
                System.out.println("배송이 완료되었습니다.");
                break;
            case CANCELED:
                System.out.println("주문이 취소되었습니다.");
                break;
        }
    }
}

문자열로 비교하는 것보다 더 명확하고 안전하다. 상태가 추가되면 enum 정의와 분기 로직을 함께 점검할 수 있어서 유지보수에도 유리하다.

애너테이션은 무엇인가

애너테이션(annotation)은 코드에 붙이는 특별한 표시다. 자바 문법의 주인공처럼 보이진 않지만, 현대 자바 개발에서는 매우 자주 등장한다.

쉽게 말하면 애너테이션은 코드에 "추가 설명 태그"를 붙이는 것이다. 이 태그를 보고 컴파일러, 개발 도구, 프레임워크가 특별한 처리를 할 수 있다.

예를 들어 다음 코드를 보자.

@Override
public String toString() {
    return "Hello";
}

@Override는 이 메서드가 부모 클래스의 메서드를 재정의한다는 뜻이다. 이 애너테이션이 있으면, 만약 메서드 이름을 잘못 쓰거나 시그니처가 맞지 않을 때 컴파일러가 실수를 더 잘 잡아 준다.

즉, 애너테이션은 프로그램의 실행 로직을 직접 바꾸기보다는, 코드를 더 정확하게 이해하고 검사하게 만드는 표시라고 볼 수 있다.

자주 보는 기본 애너테이션

처음 학습 단계에서 꼭 알아 두면 좋은 애너테이션이 몇 가지 있다.

@Override

가장 먼저 익혀야 하는 애너테이션이다. 부모 클래스의 메서드를 재정의할 때 붙인다.

class Animal {
    public void sound() {
        System.out.println("동물 소리");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

만약 sound()를 실수로 souund()처럼 적으면 재정의가 아니라 새로운 메서드가 되어 버린다. 그런데 @Override가 붙어 있으면 컴파일러가 "이건 재정의가 아니야"라고 알려 준다. 초보자에게 특히 고마운 안전장치다.

@Deprecated

이제는 사용을 권장하지 않는 클래스나 메서드에 붙인다.

class OldCalculator {
    @Deprecated
    public void oldMethod() {
        System.out.println("이 방법은 더 이상 권장하지 않습니다.");
    }
}

이 애너테이션이 붙은 메서드를 사용하면 개발 도구가 경고를 보여 줄 수 있다. 즉, "이건 아직 남아 있지만 새 코드에서는 피하는 게 좋아"라는 메시지다.

@SuppressWarnings

컴파일 경고를 숨길 때 사용한다.

@SuppressWarnings("unchecked")
public void example() {
}

다만 이 애너테이션은 무턱대고 남발하면 안 된다. 경고는 대개 이유가 있어서 뜬다. 진짜로 상황을 이해하고, 불가피할 때만 제한적으로 쓰는 것이 좋다. 경고를 숨기는 것은 방 청소가 아니라 불을 끄고 경보기를 빼 버리는 것과 비슷할 때가 있다.

애너테이션을 왜 배우는가

초보자 입장에서는 애너테이션이 좀 뜬금없게 느껴질 수 있다. "코드가 실행되는 것도 아닌데 왜 중요하지?"라는 생각이 들기 쉽다. 하지만 실제 개발에서는 애너테이션이 엄청난 역할을 한다.

프레임워크는 애너테이션을 보고 클래스를 자동 등록하거나, 특정 메서드를 테스트 대상으로 인식하거나, 직렬화 규칙을 정하거나, 설정을 연결한다. 즉, 애너테이션은 사람이 읽는 힌트이면서 동시에 도구가 읽는 신호다.

특히 나중에 Spring을 배우면 애너테이션이 본격적으로 등장한다. @Controller, @Service, @RestController 같은 것들이 왜 가능한지 감을 잡으려면, 지금 애너테이션의 본질을 이해해 두는 것이 큰 도움이 된다.

직접 애너테이션 만들기 맛보기

자바에서는 사용자 정의 애너테이션도 만들 수 있다. 지금 단계에서는 가볍게 형태만 알아 두면 충분하다.

public @interface MyAnnotation {
}

이렇게 선언하면 @MyAnnotation을 코드에 붙일 수 있다.

@MyAnnotation
public class TestClass {
}

물론 실제로 이 애너테이션을 의미 있게 사용하려면 리플렉션 같은 추가 개념이 필요하다. 하지만 중요한 것은 애너테이션도 자바가 제공하는 하나의 타입이며, 코드에 부가 정보를 담는 역할을 한다는 점이다.

패키지, 제네릭, enum, 애너테이션은 어떻게 연결되는가

이 네 가지는 따로 배우지만, 실제 프로젝트에서는 함께 등장한다. 예를 들어 온라인 쇼핑몰 프로그램을 만든다고 해 보자.

패키지는 기능별로 클래스를 정리한다.

com.shop.order
com.shop.product
com.shop.user

주문 상태는 enum으로 표현한다.

public enum OrderStatus {
    READY, SHIPPED, DELIVERED, CANCELED
}

여러 주문을 저장하는 목록은 제네릭 컬렉션으로 관리한다.

List<Order> orders = new ArrayList<>();

부모 메서드를 재정의한 부분에는 애너테이션을 붙인다.

@Override
public String toString() {
    return "주문 정보";
}

이렇게 보면 네 가지는 각각 다른 문법이 아니라, 프로그램을 더 정리되고 안전하고 읽기 쉽게 만드는 공통 목적을 가진 도구들이다.

실전 감각을 키우는 작은 예제

간단한 회원 등급 관리 예제를 통해 한 번에 묶어 보자.

package com.example.member;

public enum Grade {
    BASIC, VIP, ADMIN
}
package com.example.member;

public class Member {
    private String name;
    private Grade grade;

    public Member(String name, Grade grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }

    @Override
    public String toString() {
        return name + "(" + grade + ")";
    }
}
package com.example.member;

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

public class MemberRepository {
    private List<Member> members = new ArrayList<>();

    public void add(Member member) {
        members.add(member);
    }

    public List<Member> getMembers() {
        return members;
    }
}
package com.example.app;

import com.example.member.Grade;
import com.example.member.Member;
import com.example.member.MemberRepository;

public class Main {
    public static void main(String[] args) {
        MemberRepository repository = new MemberRepository();

        repository.add(new Member("김자바", Grade.BASIC));
        repository.add(new Member("이코드", Grade.VIP));

        for (Member member : repository.getMembers()) {
            System.out.println(member);
        }
    }
}

이 예제 안에는 이번 장의 핵심이 모두 들어 있다. 패키지로 코드를 나누었고, import로 다른 패키지의 클래스를 가져왔으며, List<Member>로 제네릭을 사용했고, Grade를 enum으로 정의했으며, toString() 재정의에는 @Override를 붙였다. 실무 코드는 훨씬 커지지만, 기본 원리는 이와 같다.

초보자가 자주 하는 실수

이 장의 내용은 문법이 어렵다기보다, 익숙하지 않아서 헷갈리기 쉽다. 특히 다음 실수들을 자주 만난다.

패키지 선언과 폴더 구조가 맞지 않는 경우

파일은 member 폴더에 있는데 package com.example.user;라고 적어 두면 혼란이 생긴다. 개발 도구가 어느 정도 도와주긴 하지만, 패키지명과 폴더 구조를 일치시키는 습관을 초반부터 들이는 것이 좋다.

import와 package를 같은 것으로 오해하는 경우

package는 "나는 어디 소속인가"를 선언하는 것이고, import는 "다른 곳의 누구를 가져다 쓸 것인가"를 적는 것이다. 둘은 전혀 다른 역할이다.

제네릭 없이 컬렉션을 사용하는 경우

ArrayList list = new ArrayList();

이렇게 쓰면 당장은 돌아갈 수 있어도, 타입 안정성이 떨어진다. 특별한 이유가 없다면 제네릭을 붙이는 습관을 들이자.

enum 대신 문자열 상수를 남발하는 경우

상태 값이 정해져 있다면 문자열보다 enum이 훨씬 안전하다. 오타, 비교 실수, 관리 어려움을 줄일 수 있다.

@Override를 빼먹는 경우

재정의 메서드에는 가능하면 @Override를 붙이자. 이 작은 습관 하나가 버그를 눈에 띄게 줄여 준다.

이 장을 공부할 때의 핵심 관점

이번 장의 내용을 단순한 기능 목록으로 외우면 오래 남지 않는다. 대신 다음 관점으로 이해하면 훨씬 잘 정리된다.

패키지는 코드를 정리하는 도구다. 제네릭은 타입 실수를 줄이는 도구다. enum은 의미 있는 상수를 안전하게 표현하는 도구다. 애너테이션은 코드에 추가 정보를 붙여 컴파일러와 도구를 돕는 도구다.

즉, 공통점은 모두 "프로그램을 더 잘 관리하게 만든다"는 데 있다. 자바를 처음 배울 때는 문법 하나하나가 전부처럼 느껴지지만, 어느 순간부터는 구조를 잘 잡는 사람이 훨씬 좋은 코드를 만든다. 이번 장이 바로 그 시점이다.

정리

이번 장에서는 자바 프로그램을 더 체계적으로 만드는 네 가지 도구를 배웠다.

패키지는 클래스를 기능과 역할에 따라 정리하게 해 주며, 이름 충돌을 피하고 접근 범위를 나누는 데도 도움이 된다. import는 다른 패키지의 클래스를 편하게 가져와 사용할 수 있게 해 준다.

제네릭은 타입을 일반화하면서도 안전하게 유지하게 해 준다. 특히 컬렉션과 함께 사용할 때 진가가 드러난다. "아무거나 담는 코드"보다 "무엇을 담는지 분명한 코드"가 훨씬 좋은 코드다.

enum은 관련 있는 상수들을 하나의 타입으로 묶는다. 문자열보다 안전하고, 의미가 분명하며, 메서드와 필드를 함께 가질 수 있어 표현력도 좋다.

애너테이션은 코드에 설명 표지를 붙이는 기능이다. @Override처럼 컴파일러의 도움을 받게 하거나, 나중에는 프레임워크와 연결되는 중요한 기반이 된다.

여기까지 오면 자바는 더 이상 단순한 문법 연습이 아니다. 이제부터는 "어떤 코드를 쓰느냐"만큼이나 "어떻게 구조화하느냐"가 중요해진다. 다음 장에서는 예외 처리와 디버깅을 배우며, 프로그램이 예상대로 움직이지 않을 때 어떻게 문제를 다루고 고쳐 나가는지 익히게 된다. 잘 정리된 구조 위에서 오류를 다루는 습관까지 갖추면, 한층 더 개발자다운 시야가 생기기 시작한다.

책 목록

처음부터 실무까지 단계별로 배우는 JAVA 학습

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기