자바 static 이란? 변수·메서드·블록·클래스 실전 예제


public static void main(String[] args) — 자바를 처음 배우는 날부터 매일 쓰지만, 왜 static이 붙어야 하는지 제대로 설명하는 사람은 생각보다 많지 않습니다. 자바 static 키워드는 단순히 “객체 없이 호출하는 것”이 아닙니다. 클래스가 JVM에 로딩되는 순간부터 메모리에 올라가 모든 인스턴스가 공유하는, 자바의 메모리 관리 핵심 메커니즘입니다. 이 글에서는 static이 JVM 메모리 어느 영역에 자리 잡는지부터 변수·메서드·블록·내부 클래스까지 각 쓰임새별 동작 원리, 실전 패턴, 그리고 절대 피해야 할 안티패턴까지 한 번에 완벽하게 정리합니다.


목차

  1. static이란 무엇인가: JVM 메모리 구조로 이해하는 본질
  2. static 변수(클래스 변수): 모든 인스턴스가 공유하는 메모리
  3. static 메서드: 객체 없이 호출하는 클래스 수준의 동작
  4. static 블록: 클래스 로딩 시점의 초기화 메커니즘
  5. static 내부 클래스와 static import 활용법
  6. static 실전 패턴, 안티패턴, 전문가 설계 원칙

1. static이란 무엇인가: JVM 메모리 구조로 이해하는 본질

static이라는 단어는 “정적인”, “고정된”이라는 뜻입니다. 자바에서 static이 붙은 멤버는 특정 인스턴스에 종속되지 않고 클래스 자체에 귀속됩니다. 이것이 무슨 의미인지 이해하려면 JVM이 메모리를 어떻게 관리하는지 먼저 알아야 합니다.

JVM 메모리 구조와 static의 위치

JVM은 실행 중에 메모리를 크게 세 영역으로 나눠 관리합니다.

┌─────────────────────────────────────────────────────────────┐
│                       JVM Memory                            │
│                                                             │
│  ┌──────────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   Method Area    │  │     Heap     │  │    Stack     │  │
│  │  (= Static Area) │  │              │  │              │  │
│  │                  │  │  new로 생성  │  │  메서드 호출 │  │
│  │ • static 변수    │  │  한 객체     │  │  프레임      │  │
│  │ • static 메서드  │  │  인스턴스    │  │  지역 변수   │  │
│  │ • 클래스 메타데이│  │              │  │              │  │
│  │   터(클래스명 등)│  │  Person p1   │  │  int x = 1   │  │
│  │                  │  │  Person p2   │  │  String s    │  │
│  │  클래스 로딩 시  │  │  Person p3   │  │              │  │
│  │  단 한 번 생성   │  │  ↑ 각각 독립│  │              │  │
│  └──────────────────┘  └──────────────┘  └──────────────┘  │
│          ↑ 모든 인스턴스가 이 영역을 공유                   │
└─────────────────────────────────────────────────────────────┘
영역저장 대상생성 시점소멸 시점
Method Areastatic 변수, static 메서드, 클래스 메타데이터클래스 로딩 시JVM 종료 시
Heapnew로 생성한 객체 인스턴스new 키워드 실행 시GC가 수거 시
Stack메서드 호출 프레임, 지역 변수, 매개변수메서드 호출 시메서드 종료 시

static 멤버는 **Method Area(메서드 영역)**에 저장됩니다. 이 영역은 JVM이 시작되고 클래스가 처음 로딩될 때 단 한 번 생성되며, JVM이 종료될 때까지 메모리에 유지됩니다. 반면 일반 인스턴스 변수는 Heap에 저장되고, new를 호출할 때마다 각자의 독립적인 공간이 생깁니다.

클래스 로딩(Class Loading)과 static 초기화 순서

1. JVM이 .class 파일을 Method Area에 로딩
        ↓
2. static 변수 기본값 초기화 (int → 0, boolean → false, Object → null)
        ↓
3. static 초기화 블록(static { }) 실행 (선언 순서대로)
        ↓
4. static 변수 명시적 초기화 값 할당
        ↓
5. 클래스 사용 준비 완료 (이후 new로 인스턴스 생성 가능)

이 초기화 순서를 모르면 static 변수 간 참조 순서 버그나 static 블록에서 초기화되지 않은 값을 읽는 문제가 발생합니다. 아래에서 각 단계를 코드로 확인하겠습니다.


2. static 변수(클래스 변수): 모든 인스턴스가 공유하는 메모리

static이 붙은 필드를 static 변수 또는 **클래스 변수(Class Variable)**라고 부릅니다. static이 없는 일반 필드는 **인스턴스 변수(Instance Variable)**라고 합니다. 이 둘의 차이가 static의 핵심입니다.

인스턴스 변수 vs static 변수: 메모리 비교

java

public class Counter {

    // static 변수: 클래스당 하나, 모든 인스턴스가 공유
    private static int totalCount = 0;

    // 인스턴스 변수: 객체마다 독립적으로 존재
    private int instanceId;
    private String name;

    public Counter(String name) {
        totalCount++;                    // 공유 카운터 증가
        this.instanceId = totalCount;    // 각 인스턴스의 고유 ID
        this.name = name;
    }

    // static 변수는 클래스명으로 접근하는 것이 원칙
    public static int getTotalCount() {
        return totalCount;
    }

    @Override
    public String toString() {
        return name + "(id=" + instanceId + ")";
    }
}

java

public class CounterDemo {
    public static void main(String[] args) {
        System.out.println("초기 count: " + Counter.getTotalCount()); // 0

        Counter c1 = new Counter("첫번째");
        Counter c2 = new Counter("두번째");
        Counter c3 = new Counter("세번째");

        System.out.println(c1); // 첫번째(id=1)
        System.out.println(c2); // 두번째(id=2)
        System.out.println(c3); // 세번째(id=3)

        // totalCount는 세 객체 모두 동일한 값을 바라봄
        System.out.println("전체 count: " + Counter.getTotalCount()); // 3

        /*
         * 메모리 구조:
         * Method Area: totalCount = 3  ←┐
         *                               │ (공유)
         * Heap: c1(instanceId=1, name="첫번째") ──┘
         *       c2(instanceId=2, name="두번째") ──┘
         *       c3(instanceId=3, name="세번째") ──┘
         */
    }
}

static 변수의 대표 활용 패턴

패턴 1: 상수(Constant) 선언

static final을 조합하면 클래스 수준의 불변 상수가 됩니다. 관례상 대문자 스네이크 케이스로 명명합니다.

java

public class MathConstants {
    public static final double PI = 3.141592653589793;
    public static final double E  = 2.718281828459045;
    public static final int    MAX_RETRY_COUNT = 3;

    // 사용 시
    // double area = MathConstants.PI * radius * radius;
}

패턴 2: 공유 카운터·통계 수집

java

public class DatabaseConnection {
    private static int activeConnections = 0;
    private static final int MAX_CONNECTIONS = 100;

    public DatabaseConnection() {
        if (activeConnections >= MAX_CONNECTIONS) {
            throw new IllegalStateException(
                "최대 연결 수 초과: " + MAX_CONNECTIONS);
        }
        activeConnections++;
    }

    public void close() {
        activeConnections--;
    }

    public static int getActiveConnections() {
        return activeConnections;
    }
}

static 변수 초기화 순서 주의점

java

public class InitOrderDemo {

    // ① 선언 순서대로 초기화됨
    static int a = 10;
    static int b = a * 2;    // a가 먼저 초기화되므로 b = 20
    static int c = b + a;    // c = 30

    // ② 아래는 버그: d를 선언하기 전에 e가 d를 참조
    static int e = d + 1;    // d가 아직 기본값(0)이므로 e = 1 (의도와 다름)
    static int d = 5;        // 나중에 5로 초기화되지만 e는 이미 결정됨

    public static void main(String[] args) {
        System.out.println("a=" + a); // 10
        System.out.println("b=" + b); // 20
        System.out.println("c=" + c); // 30
        System.out.println("d=" + d); // 5
        System.out.println("e=" + e); // 1 (버그: 5+1=6이 아님)
    }
}

static 변수는 선언 순서대로 초기화됩니다. 서로 의존하는 static 변수가 있을 때 선언 순서를 잘못 배치하면 예상치 못한 초기화 버그가 생깁니다.


3. static 메서드: 객체 없이 호출하는 클래스 수준의 동작

static이 붙은 메서드는 인스턴스를 생성하지 않고 클래스명으로 직접 호출할 수 있습니다. Math.sqrt()Collections.sort()String.valueOf() 등 JDK 유틸리티 메서드 대부분이 static 메서드입니다.

static 메서드의 핵심 제약: this와 인스턴스 멤버 접근 불가

java

public class RestrictionsDemo {

    private int instanceVar = 10;         // 인스턴스 변수
    private static int staticVar = 20;    // static 변수

    // 인스턴스 메서드: static·인스턴스 멤버 모두 접근 가능
    public void instanceMethod() {
        System.out.println(instanceVar);  // ✅ 가능
        System.out.println(staticVar);    // ✅ 가능
        System.out.println(this);         // ✅ 가능 (this = 현재 인스턴스)
        staticMethod();                   // ✅ 가능
    }

    // static 메서드: static 멤버만 접근 가능
    public static void staticMethod() {
        // System.out.println(instanceVar); // ❌ 컴파일 에러
        // System.out.println(this);        // ❌ 컴파일 에러 (this 없음)
        System.out.println(staticVar);      // ✅ 가능
        // instanceMethod();                // ❌ 컴파일 에러
    }
}

왜 static 메서드에서 인스턴스 멤버에 접근할 수 없을까요?

static 메서드는 인스턴스 없이 클래스명.메서드명()으로 호출합니다. 이 시점에 어떤 인스턴스가 존재하는지, 심지어 인스턴스가 하나도 없는지도 JVM은 알 수 없습니다. this가 존재하지 않으니 인스턴스 멤버에 접근하는 것 자체가 불가능한 것입니다.

static 메서드가 적합한 세 가지 상황

상황 1: 유틸리티·헬퍼 메서드

인스턴스 상태가 전혀 필요 없고 입력값만으로 결과를 반환하는 순수 함수형 메서드에 적합합니다.

java

public class StringUtils {

    // 외부에서 new StringUtils() 없이 사용 가능
    public static boolean isNullOrEmpty(String s) {
        return s == null || s.isEmpty();
    }

    public static String capitalize(String s) {
        if (isNullOrEmpty(s)) return s;
        return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
    }

    public static String repeat(String s, int count) {
        if (isNullOrEmpty(s) || count <= 0) return "";
        return s.repeat(count); // Java 11+
    }
}

// 사용
String result = StringUtils.capitalize("hello"); // "Hello"
boolean empty = StringUtils.isNullOrEmpty(null); // true

상황 2: 팩토리 메서드(Factory Method)

생성자 대신 의미 있는 이름으로 객체를 생성하는 패턴입니다.

java

public class Money {

    private final long amount;
    private final String currency;

    // private 생성자: 외부에서 new Money() 직접 생성 불가
    private Money(long amount, String currency) {
        if (amount < 0) {
            throw new IllegalArgumentException("금액은 0 이상이어야 합니다.");
        }
        this.amount = amount;
        this.currency = currency;
    }

    // static 팩토리 메서드: 의미 있는 이름으로 생성
    public static Money ofWon(long amount) {
        return new Money(amount, "KRW");
    }

    public static Money ofDollar(long amount) {
        return new Money(amount, "USD");
    }

    public static Money zero(String currency) {
        return new Money(0, currency);
    }

    @Override
    public String toString() {
        return amount + " " + currency;
    }
}

// 사용
Money price   = Money.ofWon(50000);
Money salary  = Money.ofDollar(3000);
Money initial = Money.zero("KRW");

상황 3: 상수 및 Enum 관련 유틸리티

java

public class DateUtils {

    private static final String DEFAULT_FORMAT = "yyyy-MM-dd";

    public static String format(LocalDate date) {
        return date.format(DateTimeFormatter.ofPattern(DEFAULT_FORMAT));
    }

    public static LocalDate parse(String dateStr) {
        return LocalDate.parse(dateStr,
            DateTimeFormatter.ofPattern(DEFAULT_FORMAT));
    }

    public static boolean isWeekend(LocalDate date) {
        DayOfWeek day = date.getDayOfWeek();
        return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
    }
}

main 메서드가 static인 이유

java

public class Application {
    public static void main(String[] args) { ... }
}

JVM은 프로그램 시작 시 Application 클래스를 로딩하고 main을 호출합니다. 이 시점에 아직 어떤 Application 인스턴스도 생성되지 않았습니다. JVM이 인스턴스 없이 곧바로 호출할 수 있어야 하기 때문에 main은 반드시 static이어야 합니다.


4. static 블록: 클래스 로딩 시점의 초기화 메커니즘

**static 블록(Static Initializer Block)**은 클래스가 JVM에 처음 로딩될 때 단 한 번 실행되는 초기화 코드 블록입니다. 단순한 값 할당으로 처리하기 어려운 복잡한 초기화 로직을 담을 때 사용합니다.

static 블록의 기본 문법과 실행 순서

java

public class StaticBlockDemo {

    static int x;
    static int y;
    static Map<String, Integer> codeMap;

    // static 블록 1: 선언 순서대로 실행됨
    static {
        System.out.println("[블록1] 실행");
        x = 100;
        y = x * 2;
    }

    // static 블록 2: 블록 1 이후 실행
    static {
        System.out.println("[블록2] 실행");
        codeMap = new HashMap<>();
        codeMap.put("SUCCESS", 200);
        codeMap.put("NOT_FOUND", 404);
        codeMap.put("ERROR", 500);
    }

    static int z = x + y; // 블록 이후 실행: z = 100 + 200 = 300

    public static void main(String[] args) {
        // 출력:
        // [블록1] 실행
        // [블록2] 실행
        System.out.println("x=" + x);          // 100
        System.out.println("y=" + y);          // 200
        System.out.println("z=" + z);          // 300
        System.out.println(codeMap);           // {SUCCESS=200, NOT_FOUND=404, ERROR=500}
    }
}

static 블록의 실전 활용: JDBC 드라이버 로딩

가장 대표적인 활용 사례가 JDBC 드라이버 등록입니다. 예전 방식에서는 Class.forName()이 호출되면 해당 클래스의 static 블록이 실행되면서 드라이버가 DriverManager에 자동 등록되었습니다.

java

// MySQL JDBC 드라이버 내부 (개념적 표현)
public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {

    static {
        // 클래스 로딩 시점에 DriverManager에 자신을 등록
        java.sql.DriverManager.registerDriver(new Driver());
        System.out.println("MySQL JDBC Driver 등록 완료");
    }
}

// 사용 측 (구버전 방식)
Class.forName("com.mysql.cj.jdbc.Driver"); // 이 한 줄이 위 static 블록을 실행시킴
Connection conn = DriverManager.getConnection(url, user, pw);

static 블록으로 불변 컬렉션 초기화

java

public class HttpStatusMessages {

    // 수정 불가능한 HTTP 상태 코드 메시지 맵
    public static final Map<Integer, String> STATUS_MAP;

    static {
        Map<Integer, String> map = new HashMap<>();
        map.put(200, "OK");
        map.put(201, "Created");
        map.put(400, "Bad Request");
        map.put(401, "Unauthorized");
        map.put(403, "Forbidden");
        map.put(404, "Not Found");
        map.put(500, "Internal Server Error");
        STATUS_MAP = Collections.unmodifiableMap(map); // 불변 처리
        // Java 9+에서는 Map.of() 또는 Map.copyOf()로 대체 가능
    }

    public static String getMessage(int statusCode) {
        return STATUS_MAP.getOrDefault(statusCode, "Unknown Status");
    }
}

static 블록에서의 예외 처리 주의사항

java

public class RiskyStaticInit {

    static {
        try {
            // 외부 리소스 로딩 같은 실패 가능한 작업
            Properties props = new Properties();
            props.load(new FileInputStream("config.properties"));
        } catch (IOException e) {
            // static 블록에서 Checked Exception을 밖으로 던질 수 없음
            // ExceptionInInitializerError로 래핑되어 전파됨
            throw new ExceptionInInitializerError(e);
        }
    }
}

static 블록에서 예외가 발생하면 ExceptionInInitializerError가 발생하고, 이후 해당 클래스에 접근하려 하면 NoClassDefFoundError가 연속 발생합니다. static 블록 내 예외는 반드시 처리하거나, 실패 시 명확한 에러 메시지를 남겨야 합니다.


5. static 내부 클래스와 static import 활용법

static 키워드는 변수·메서드·블록 외에도 **중첩 클래스(Nested Class)**와 import 구문에도 사용됩니다.

static 내부 클래스(Static Nested Class)

자바에서 클래스 안에 클래스를 선언할 수 있습니다. static이 붙은 중첩 클래스와 붙지 않은 내부 클래스(Inner Class)는 동작이 전혀 다릅니다.

java

public class Outer {

    private int outerInstanceVar = 10;
    private static int outerStaticVar = 20;

    // ① static 중첩 클래스: 외부 클래스 인스턴스 불필요
    public static class StaticNested {
        public void show() {
            // outerInstanceVar 접근 불가 (인스턴스 없음)
            // System.out.println(outerInstanceVar); // ❌ 컴파일 에러
            System.out.println(outerStaticVar);      // ✅ static은 접근 가능
        }
    }

    // ② 비 static 내부 클래스: 외부 클래스 인스턴스 필요
    public class Inner {
        public void show() {
            System.out.println(outerInstanceVar);    // ✅ 외부 인스턴스 변수 접근 가능
            System.out.println(outerStaticVar);      // ✅ 가능
        }
    }
}

java

public class NestedClassDemo {
    public static void main(String[] args) {

        // static 중첩 클래스: 외부 클래스 인스턴스 없이 생성
        Outer.StaticNested staticNested = new Outer.StaticNested();
        staticNested.show();

        // 비 static 내부 클래스: 반드시 외부 클래스 인스턴스 필요
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }
}

static 중첩 클래스가 선호되는 이유:

비 static 내부 클래스는 외부 클래스 인스턴스에 대한 암묵적 참조를 항상 보유합니다. 이 참조가 GC를 방해해 메모리 누수의 원인이 될 수 있습니다. 이펙티브 자바(Item 24)는 멤버 클래스가 외부 인스턴스를 참조할 필요가 없다면 반드시 static으로 선언하라고 권고합니다.

Builder 패턴에서의 static 중첩 클래스

실무에서 static 중첩 클래스가 가장 많이 쓰이는 패턴이 바로 빌더(Builder) 패턴입니다.

java

public class Person {

    private final String name;
    private final int age;
    private final String email;
    private final String phone;

    // private 생성자
    private Person(Builder builder) {
        this.name  = builder.name;
        this.age   = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    // static 중첩 Builder 클래스
    public static class Builder {
        // 필수 파라미터
        private final String name;
        private final int age;

        // 선택 파라미터 (기본값 설정)
        private String email = "";
        private String phone = "";

        public Builder(String name, int age) {
            this.name = name;
            this.age  = age;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    @Override
    public String toString() {
        return "Person{name=" + name + ", age=" + age
             + ", email=" + email + ", phone=" + phone + "}";
    }
}

// 사용 — new Person.Builder("홍길동", 30).email("hong@mail.com").build()
Person person = new Person.Builder("홍길동", 30)
        .email("hong@example.com")
        .phone("010-1234-5678")
        .build();

static import: 가독성을 위한 선택적 활용

static import를 사용하면 클래스명 없이 static 멤버를 직접 참조할 수 있습니다.

java

// 일반 import
import java.lang.Math;
double r1 = Math.sqrt(16);
double r2 = Math.PI * Math.pow(3, 2);

// static import 사용
import static java.lang.Math.sqrt;
import static java.lang.Math.PI;
import static java.lang.Math.pow;

double r1 = sqrt(16);       // Math. 생략
double r2 = PI * pow(3, 2); // 수식이 더 간결해짐

java

// JUnit 5 테스트에서 static import 활용 (가장 일반적인 실무 사례)
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Test
void 나이_음수_입력시_예외_발생() {
    assertThrows(IllegalArgumentException.class,
        () -> new Person.Builder("테스트", -1).build());
}

@Test
void 빌더_정상_생성() {
    Person p = new Person.Builder("홍길동", 30).email("hong@test.com").build();
    assertThat(p.toString()).contains("홍길동");
}

단, static import는 과도하게 사용하면 어떤 클래스의 멤버인지 파악이 어려워집니다. JUnit·AssertJ의 assert* 메서드, Math 클래스 상수, Collections 유틸리티 등 명확히 익숙한 맥락에서만 선택적으로 사용하는 것이 원칙입니다.


6. static 실전 패턴, 안티패턴, 전문가 설계 원칙

static은 강력하지만 잘못 사용하면 테스트 어려움, 메모리 누수, 동시성 버그를 유발합니다. 실무에서 반드시 알아야 할 패턴과 안티패턴을 정리합니다.

싱글톤 패턴과 static

가장 대표적인 static 활용 패턴이 싱글톤입니다. JVM 전체에서 인스턴스를 하나만 유지합니다.

java

// 스레드 안전한 싱글톤: 정적 내부 클래스(Holder) 방식 — 실무 권장
public class AppConfig {

    private AppConfig() {}  // 외부 생성 차단

    // JVM이 클래스 로딩을 스레드 안전하게 보장
    // AppConfig.getInstance() 최초 호출 시에만 Holder 클래스가 로딩됨
    private static class Holder {
        private static final AppConfig INSTANCE = new AppConfig();
    }

    public static AppConfig getInstance() {
        return Holder.INSTANCE;
    }

    public String getDbUrl() {
        return "jdbc:mysql://localhost:3306/mydb";
    }
}

// 사용
AppConfig config = AppConfig.getInstance();

java

// Enum 싱글톤 — 조슈아 블로크(Effective Java Item 3) 최강 권장 방식
// 직렬화·리플렉션 공격에도 안전
public enum AppConfigEnum {
    INSTANCE;

    private final String dbUrl = "jdbc:mysql://localhost:3306/mydb";

    public String getDbUrl() {
        return dbUrl;
    }
}

// 사용
String url = AppConfigEnum.INSTANCE.getDbUrl();

static이 유발하는 주요 안티패턴

안티패턴 1: 가변 static 변수로 인한 동시성 버그

java

// 위험: 여러 스레드에서 동시에 접근하면 레이스 컨디션 발생
public class RequestCounter {
    public static int count = 0; // ❌ 가변 + public static = 위험 조합

    public static void increment() {
        count++; // 원자적 연산이 아님! 멀티스레드 환경에서 오류
    }
}

// 안전한 대안: AtomicInteger 사용
public class SafeRequestCounter {
    private static final AtomicInteger count = new AtomicInteger(0); // ✅

    public static void increment() {
        count.incrementAndGet(); // 원자적 연산 보장
    }

    public static int getCount() {
        return count.get();
    }
}

안티패턴 2: static 메서드 남용으로 인한 테스트 불가 코드

java

// 나쁜 설계: 모든 비즈니스 로직을 static으로 구현
public class OrderValidator {
    public static boolean validate(Order order) {         // ❌
        return DatabaseHelper.check(order.getUserId()); // static DB 접근
    }
}

// 테스트 시 DatabaseHelper를 Mock으로 교체할 수 없음!
// static 메서드는 오버라이딩도 불가 → 다형성 활용 불가

// 좋은 설계: 인스턴스 메서드 + 의존성 주입
public class OrderValidator {
    private final UserRepository userRepository;

    public OrderValidator(UserRepository userRepository) {  // DI
        this.userRepository = userRepository;
    }

    public boolean validate(Order order) {                  // ✅
        return userRepository.existsById(order.getUserId());
    }
}

// 테스트 시 Mock 주입 가능
OrderValidator validator = new OrderValidator(mockUserRepository);

안티패턴 3: static 컬렉션으로 인한 메모리 누수

java

// 위험: static List에 계속 추가만 하고 제거하지 않으면 OOM 발생
public class EventLogger {
    private static final List<String> logs = new ArrayList<>(); // ❌

    public static void log(String message) {
        logs.add(message); // 쌓이기만 하고 비워지지 않음
    }
}

// 안전한 대안: 크기 제한 또는 외부 로깅 시스템 사용
public class BoundedEventLogger {
    private static final int MAX_LOGS = 1000;
    private static final Deque<String> logs = new ArrayDeque<>(); // ✅

    public static synchronized void log(String message) {
        if (logs.size() >= MAX_LOGS) {
            logs.pollFirst(); // 가장 오래된 로그 제거
        }
        logs.addLast(message);
    }
}

static 사용 결정 체크리스트

static 변수를 쓸 것인가?
  ✅ 모든 인스턴스가 반드시 공유해야 하는 값인가?
  ✅ 상수(final)로 선언할 수 있는가?
  ❌ 인스턴스마다 다를 수 있는 값이라면 → 인스턴스 변수로

static 메서드를 쓸 것인가?
  ✅ 인스턴스 상태(필드)가 전혀 필요 없는가?
  ✅ 유틸리티·팩토리·헬퍼 성격인가?
  ❌ 오버라이딩이 필요한가?           → 인스턴스 메서드로
  ❌ 테스트 시 Mock 교체가 필요한가?  → 인스턴스 메서드 + DI로

static 중첩 클래스를 쓸 것인가?
  ✅ 외부 클래스 인스턴스 참조가 불필요한가?
  ✅ 그렇다면 반드시 static으로 선언 (메모리 누수 방지)

JDK와 스프링의 static 활용 사례 정리

클래스static 활용유형
Math.sqrt()순수 계산 유틸리티static 메서드
Collections.unmodifiableList()불변 컬렉션 래핑static 메서드
Optional.of()Optional.empty()팩토리 메서드static 메서드
Integer.MAX_VALUE불변 상수static final 변수
System.outJVM 전역 출력 스트림static 변수
Lombok @Builder빌더 패턴 자동 생성static 중첩 클래스
Spring BeanDefinition클래스 메타데이터 관리static 활용

결론

자바 static 키워드는 단순히 “객체 없이 쓰는 것”이 아닙니다. 클래스가 JVM Method Area에 로딩되는 순간 단 한 번 생성되어 모든 인스턴스가 공유하는, 자바 메모리 모델의 핵심 메커니즘입니다. static 변수는 공유 상태에만, static 메서드는 인스턴스 상태가 필요 없는 순수 동작에만, static 블록은 복잡한 클래스 초기화에만 목적에 맞게 사용하는 것이 설계의 원칙입니다. 오늘부터 static을 쓸 때마다 “이 멤버가 정말로 클래스 전체 수준에서 공유되어야 하는가?”라고 스스로 물어보세요.


⚠️ 기술 적용 면책 고지: 본 포스트의 코드 예제는 Java 11–17 LTS 기준으로 작성된 교육용 예제입니다. 싱글톤·동시성 패턴은 실제 서비스 환경(스프링 컨텍스트, 멀티스레드)에 따라 적용 방식이 달라질 수 있으므로, 반드시 공식 JDK 문서와 팀 컨벤션을 함께 참고하시기 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다