공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 07 / 8

enum, 패키지와 import, static 과 final

섹션 7진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 상수의 진화: 매직 넘버 → static final → enum

java
if (order.status == 2) { ... }                         // ① 매직 넘버. 2 가 뭔지 코드만 봐서는 모른다
public static final int STATUS_PAID = 2;
if (order.status == STATUS_PAID) { ... }               // ② 이름은 생겼지만 status 에 99 를 넣어도 컴파일된다
public enum OrderStatus { CREATED, PAID, SHIPPED }
if (order.status == OrderStatus.PAID) { ... }          // ③ status 에는 세 값만 올 수 있다. 오타는 컴파일 오류

②와 ③의 차이가 핵심입니다. int 상수는 이름만 붙인 숫자라서 타입이 여전히 int 입니다. 상태 필드에 나이를 넣어도 컴파일러는 모릅니다. enum 은 새로운 타입이라 OrderStatus 변수에는 OrderStatus 값만 들어갑니다.

2.2 enum 의 실체: 인스턴스 개수가 고정된 클래스

enum 은 문법 설탕이 아니라 진짜 클래스입니다. 컴파일러가 이렇게 바꿉니다.

java
public final class OrderStatus extends Enum<OrderStatus> {
    public static final OrderStatus CREATED = new OrderStatus("CREATED", 0);   // 상수 하나 = 인스턴스 하나, 클래스 로드 때 생성
    public static final OrderStatus PAID    = new OrderStatus("PAID", 1);
    private OrderStatus(String name, int ordinal) { ... }                        // 생성자가 private 이라 외부에서 더 못 만든다
    public static OrderStatus[] values() { ... }
    public static OrderStatus valueOf(String name) { ... }
}

여기서 따라오는 성질이 있습니다.

성질 이유 결과
== 비교 가능 상수당 인스턴스가 하나뿐 equals 대신 == 를 써도 되고, null 이어도 NPE 가 안 난다
switch 가능 컴파일러가 상수 목록을 안다 화살표 switch 식에서 상수를 빠뜨리면 컴파일 오류
필드·메서드·생성자 가능 클래스니까 상태에 설명·코드·동작을 붙일 수 있다
상속 불가, 인스턴스 생성 불가 final 클래스 + private 생성자 "다섯 개뿐" 이 보장된다

기본 제공 메서드는 넷을 기억하면 됩니다. name() 은 선언한 이름 문자열, ordinal() 은 선언 순서(0부터), values() 는 전체 배열, valueOf(String) 은 이름으로 찾기(없으면 IllegalArgumentException).

2.3 enum 에 데이터와 동작을 넣는다

상태마다 화면 표시용 설명과 DB 저장용 코드가 필요하면 필드로 넣습니다.

java
public enum OrderStatus {
    CREATED("주문 접수", 10), PAID("결제 완료", 20), SHIPPED("배송 중", 30), DELIVERED("배송 완료", 40), CANCELLED("취소", 90);
    private final String label;
    private final int code;
    OrderStatus(String label, int code) { this.label = label; this.code = code; }   // 상수 목록 뒤에 세미콜론, 그다음 필드·생성자
    public static OrderStatus fromCode(int code) { for (OrderStatus s : values()) if (s.code == code) return s; throw new IllegalArgumentException(...); }
}

DB 에는 code 를 저장하고 ordinal() 은 절대 저장하지 않습니다. ordinal() 은 선언 순서라서 나중에 상수를 중간에 하나 끼우면 기존 데이터의 의미가 전부 밀립니다. name() 을 문자열로 저장하는 것은 괜찮지만 이름을 바꾸면 깨지므로, 바뀌지 않을 별도 코드를 두는 것이 실무 관례입니다.

동작도 넣을 수 있습니다. next() 처럼 자기 상태에 따라 분기하는 메서드는 enum 안에 두면 상태 전이 규칙이 한곳에 모입니다. 상수마다 동작이 완전히 다르면 추상 메서드를 선언하고 상수별로 구현합니다(예제 2 의 PaymentMethod). 결제 수단별 if-else 가 사라지고, 새 수단을 추가할 때 빠뜨린 곳이 컴파일 오류로 드러납니다.

2.4 EnumMap 과 EnumSet

enum 을 키로 쓰는 Map 은 HashMap 대신 EnumMap 을 씁니다. 내부가 ordinal() 인덱스의 배열이라 해시 계산이 없고, 출력 순서가 상수 선언 순서로 고정됩니다. 상태별 집계, 요일별 설정처럼 "키가 enum 전체" 인 경우 항상 이쪽입니다.

EnumSet 도 같은 원리로 비트 연산이라 contains 가 매우 빠르며, EnumSet.of(CREATED, PAID) 처럼 "취소 가능한 상태 집합" 을 표현할 때 씁니다.

2.5 패키지: 폴더이자 이름 공간

text
pkg/
├── PkgMain.java          (패키지 없음, 기본 패키지)
└── shop/
    └── Product.java      → 첫 줄에 package shop;

규칙은 셋입니다. 파일의 package 선언과 폴더 경로가 일치해야 하고, 클래스의 완전한 이름은 shop.Product 이며, 다른 패키지에서 쓰려면 import shop.Product; 를 하거나 완전한 이름으로 씁니다.

실무에서는 com.company.project.order.service 처럼 도메인 역순 + 프로젝트 + 계층으로 깊게 나눕니다. 이 레슨 사이트의 다른 예제들이 패키지를 안 쓰는 것은 javac *.java && java Main 한 줄로 실행하려는 편의이고, 실무 코드에 기본 패키지는 없습니다.

import 는 세 가지가 있습니다.

형태 뜻 주의
import java.util.List; 클래스 하나 가장 명확. IDE 가 자동으로 넣어 준다
import java.util.*; 패키지 안 클래스 전부 하위 패키지는 포함 안 됨. 이름 충돌 시 컴파일 오류
import static java.lang.Math.max; 정적 멤버 Math.max 를 max 로. 테스트 코드의 assertEquals 가 이것

이름 충돌은 실무에서 한 번은 겪습니다. java.util.Date 와 java.sql.Date 를 한 파일에서 둘 다 쓰면 하나는 import 하고 다른 하나는 java.sql.Date 처럼 완전한 이름으로 써야 합니다. java.lang 패키지(String, Math, System)는 import 없이 항상 보입니다.

패키지는 접근 제어와 연결됩니다. 접근 제어자를 안 쓴 멤버(default)는 같은 패키지 안에서만 보입니다. 05 레슨의 캡슐화 표에서 protected 와 default 가 "같은 패키지" 를 기준으로 갈리는 이유가 여기 있고, 예제 5 에서 internalCode() 를 밖에서 부르면 컴파일 오류가 나는 것으로 확인합니다.

2.6 static: 클래스에 하나

인스턴스 필드는 객체마다 따로 있습니다. static 필드는 클래스에 하나만 있고 모든 객체가 공유합니다. 지금까지 만든 객체 수, 설정값, 캐시처럼 "객체가 아니라 클래스 전체의 것" 이 static 입니다.

java
public class Counter {
    private static int created = 0;   // Counter 클래스에 하나
    private final int id;             // Counter 객체마다 하나
    public Counter() { id = ++created; }
    public static int created() { return created; }   // 객체 없이 Counter.created() 로 호출
}

static 메서드는 인스턴스 필드에 접근할 수 없습니다. Counter.created() 를 부를 때 "어느 객체의 id" 인지 정해지지 않았기 때문입니다. main 이 static 이라 그 안에서 인스턴스 메서드를 바로 못 부르고 new 를 먼저 하는 이유도 같습니다. 유틸 메서드(Math.max, Arrays.toString)처럼 객체 상태가 필요 없는 것이 static 메서드의 자리입니다.

static { ... } 블록은 클래스가 처음 로드될 때 딱 한 번 실행됩니다. 설정 파일 읽기, 룩업 테이블 초기화에 씁니다. 예제 3 에서 첫 new Counter() 직전에 한 번만 찍히는 것을 볼 수 있습니다.

2.7 final: 다시 대입할 수 없다

final 은 붙는 자리에 따라 뜻이 다릅니다.

자리 뜻 예
변수·필드 한 번 대입하면 다시 못 바꿈 final int id; 생성자에서 딱 한 번 대입
메서드 하위 클래스가 오버라이드 못 함 상속 레슨에서 다룸
클래스 상속 못 함 String, 모든 enum, record

가장 흔한 오해는 final = 불변 입니다. final List<String> items 는 "items 변수가 다른 리스트를 가리킬 수 없다" 는 뜻이지, 리스트 안의 내용은 add 로 바뀝니다. 배열도 요소는 바뀝니다. 내용까지 얼리려면 List.copyOf 같은 불변 컬렉션이나 불변 클래스(String, BigDecimal)를 써야 합니다. 예제 4 가 이 둘을 나란히 보여줍니다.

public static final 이 상수인 이유는 이제 분리됩니다. static 으로 클래스에 하나만 두고, final 로 바뀌지 않게 하고, 이름은 MAX_RETRY 처럼 대문자와 밑줄로 씁니다. 지역 변수에도 final 을 붙일 수 있고, 람다에서 바깥 변수를 쓸 때 "사실상 final" 이어야 한다는 규칙이 고급 레슨에서 나옵니다.

핵심 원리
  • 2.1 상수의 진화: 매직 넘버 → static final → enum
  • 2.2 enum 의 실체: 인스턴스 개수가 고정된 클래스
  • 2.3 enum 에 데이터와 동작을 넣는다
  • 2.4 EnumMap 과 EnumSet
  • 2.5 패키지: 폴더이자 이름 공간
  • 2.6 static: 클래스에 하나
  • 2.7 final: 다시 대입할 수 없다
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제