안동민 개발노트

본문 시작

static 메서드와 정적 import

static 메서드가 인스턴스 상태에 직접 접근할 수 없는 이유를 확인하고 무상태 회원 저장소 유틸리티와 정적 import의 사용 범위를 판단합니다.

static 메서드는 특정 객체 참조 없이 클래스 이름으로 호출됩니다.

따라서 어느 인스턴스의 필드를 사용할지 정할 수 없습니다.

입력만으로 결과를 만드는 무상태 계산이나 클래스 전체 상태를 다루는 행동에 적합하며, 객체별 상태를 바꾸는 행동은 인스턴스 메서드로 둡니다.


static 문맥의 접근 제한

lab/StaticInstanceAccess.java
public final class StaticInstanceAccess {
    private int size;

    public static void main(String[] args) {
        System.out.println(size);
    }
}
컴파일 실패 관찰
error: non-static variable size cannot be referenced from a static context

이 예제의 main은 static으로 선언했으므로, StaticInstanceAccess 객체를 만들지 않고 호출됩니다.

size는 StaticInstanceAccess 객체마다 다른 인스턴스 필드인데, 현재 코드에는 어떤 객체를 뜻하는 this도 없습니다.

참조를 명시하면 접근할 수 있습니다.

StaticInstanceAccess instance = new StaticInstanceAccess();
System.out.println(instance.size);

static이 인스턴스 기능을 절대 사용할 수 없다는 뜻이 아닙니다.

특정 객체 참조를 매개변수나 지역 변수로 확보한 뒤 그 객체의 공개 메서드를 호출할 수 있습니다.

직접적인 암묵적 수신자 this가 없다는 뜻입니다.


정적·인스턴스 호출 방향

src/StaticCallRules.java
public final class StaticCallRules {
    private static int globalCount;
    private int localCount;

    public static void main(String[] args) {
        StaticCallRules first = new StaticCallRules();
        StaticCallRules second = new StaticCallRules();

        first.register();
        second.register();
        second.register();

        System.out.println(first.localSummary());
        System.out.println(second.localSummary());
        System.out.println(globalSummary());
    }

    private void register() {
        localCount++;
        globalCount++;
    }

    private String localSummary() {
        return "local=" + localCount + ", global=" + globalCount;
    }

    private static String globalSummary() {
        return "all=" + globalCount;
    }
}
local=1, global=3
local=2, global=3
all=3

인스턴스 메서드 register와 localSummary에는 this가 있어 인스턴스 필드와 static 필드 모두 사용할 수 있습니다.

static globalSummary는 globalCount만 직접 읽습니다.

인스턴스 메서드는 클래스 공유 상태에 접근할 수 있지만, 결합이 필요한지 판단해야 합니다.

인스턴스별 localCount와 공유 globalCount를 읽는 호출

StaticCallRules에서 register를 first에1번 second에2번 호출한 뒤 객체별 localCount는1과2이고 클래스 공유 globalCount는3이다. 인스턴스 요약은 자기 객체와 공유 상태를 읽고 정적 요약은 공유 상태만 읽는다.

StaticCallRules에서 first.register()는 한 번, second.register()는 두 번 호출한 뒤의 상태입니다.

세 요약 호출의 수신 객체, 읽는 값, 실제 출력
호출수신 객체와 읽는 값출력
first.localSummary()this는 first가 가리키는 객체입니다.

localCount = 1, 공유 globalCount = 3

local=1, global=3
second.localSummary()this는 second가 가리키는 객체입니다.

localCount = 2, 공유 globalCount = 3

local=2, global=3
globalSummary()static 호출이므로 수신 객체와 this가 없습니다.

공유 globalCount = 3만 읽습니다.

all=3
  1. first.localSummary()

    this는 first가 가리키는 객체입니다. 그 객체의 localCount = 1과 공유 globalCount = 3을 읽어 local=1, global=3을 출력합니다.

  2. second.localSummary()

    this는 second가 가리키는 객체입니다. 그 객체의 localCount = 2와 공유 globalCount = 3을 읽어 local=2, global=3을 출력합니다.

  3. globalSummary()

    static 호출이므로 수신 객체와 this가 없습니다. 공유 globalCount = 3만 읽어 all=3을 출력합니다.

각 register()는 호출된 객체의 localCount와 클래스 공유 globalCount를 하나씩 늘립니다. static 메서드도 명시적인 객체 참조가 있으면 그 객체의 인스턴스 멤버에 접근할 수 있습니다.


무상태 유틸리티 클래스

src/MemberStatisticsUtility.java
public final class MemberStatisticsUtility {
    public static void main(String[] args) {
        int[] age = {40, 50, 30};

        System.out.println("sum=" + MemberStatistics.sum(age));
        System.out.println("average=" + MemberStatistics.average(age));
        System.out.println("max=" + MemberStatistics.max(age));
    }

    private static final class MemberStatistics {
        private MemberStatistics() {
            throw new AssertionError("no instances");
        }

        static int sum(int[] values) {
            int total = 0;
            for (int value : values) {
                total += value;
            }
            return total;
        }

        static double average(int[] values) {
            if (values.length == 0) return 0.0;
            return (double) sum(values) / values.length;
        }

        static int max(int[] values) {
            if (values.length == 0) throw new IllegalArgumentException("empty");
            int result = values[0];
            for (int index = 1; index < values.length; index++) {
                if (values[index] > result) result = values[index];
            }
            return result;
        }
    }
}
sum=120
average=40.0
max=50

MemberStatistics 메서드는 인수만으로 결과를 만들고 객체별 필드가 없습니다.

인스턴스를 생성할 이유가 없으므로 생성자를 private으로 막습니다.

이 패턴을 모든 행동에 적용하지 않습니다.

members와 size를 가진 MemberRegistry의 add를 static으로 바꾸면 상태를 매번 인수로 전달해 객체 책임을 잃습니다.

유틸리티 메서드도 입력 배열을 바꾸는지 여부를 계약에 명시해야 합니다.

위 계산들은 읽기만 하므로 호출자의 배열 상태를 바꾸지 않습니다.


정적 import

src/members/app/StaticImportDemo.java
package members.app;

import static java.lang.Math.max;
import static java.lang.Math.min;

public final class StaticImportDemo {
    public static void main(String[] args) {
        int age = 130;
        int normalized = max(14, min(age, 120));
        System.out.println("normalized=" + normalized);
    }
}
normalized=120

일반 호출은 Math.min(age, 120)처럼 클래스 소유자를 적습니다.

정적 import를 사용하면 min만 적어도 컴파일러가 java.lang.Math.min을 찾습니다.

수식에서 같은 클래스의 정적 함수를 반복해 소유자가 이미 명확할 때 읽기 쉬울 수 있습니다.

무분별한 정적 import는 메서드가 어느 클래스의 규칙인지 숨기고 이름 충돌을 만듭니다.

특히 프로젝트 도메인 메서드는 SignupPolicy.normalizeAge처럼 소유자를 남기는 편이 맥락을 줍니다.

테스트의 assertion이나 수학 함수처럼 관례가 강한 경우에 제한적으로 사용합니다.

별표 정적 import도 가능하지만 공개 정적 멤버가 추가될 때 이름 충돌 가능성이 커집니다.

import static java.lang.Math.*;

필요한 멤버를 개별 import하면 파일 의존성이 명시적입니다.


조립점으로서의 main

이 장은 객체 조립점에 public static void main(String[] args) 형식을 사용합니다. Java 25는 인스턴스 main도 지원하므로 모든 main이 반드시 static인 것은 아닙니다.

main은 필요한 객체를 만들고 협력을 연결한 뒤 인스턴스 메서드에 실행을 넘길 수 있습니다.

src/StaticMainComposition.java
public final class StaticMainComposition {
    public static void main(String[] args) {
        SignupApplication application = new SignupApplication(new MemberRegistry(2));
        application.run();
    }

    private static final class SignupApplication {
        private final MemberRegistry registry;

        SignupApplication(MemberRegistry registry) {
            this.registry = registry;
        }

        void run() {
            registry.add(40);
            registry.add(50);
            System.out.println("total=" + registry.total());
        }
    }

    private static final class MemberRegistry {
        private final int[] age;
        private int size;

        MemberRegistry(int capacity) {
            age = new int[capacity];
        }

        void add(int value) {
            age[size++] = value;
        }

        int total() {
            return MemberCalculations.sum(age, size);
        }
    }

    private static final class MemberCalculations {
        static int sum(int[] values, int size) {
            int total = 0;
            for (int index = 0; index < size; index++) {
                total += values[index];
            }
            return total;
        }
    }
}
total=90

main은 조립만 하고 애플리케이션 상태는 인스턴스가 소유합니다.

무상태 합계 계산만 static 유틸리티로 분리했습니다.

정적 main이 조립한 객체와 무상태 합계 호출

StaticMainComposition의 정적 main이 애플리케이션과 용량2 저장소를 조립한다. 두 인스턴스의 참조 관계와 int 배열40,50 및 size2를 읽는 정적 sum 호출을 구분한다.

StaticMainComposition의 협력 관계입니다. 저장소 값은 run()의 두 add를 마치고 합계를 구할 때를 표시합니다.

main의 객체 조립, registry 참조와 정적 합계 호출 정적 main이 용량2 MemberRegistry를 생성해 SignupApplication에 전달하고 run을 호출한다. application은 registry 참조로40과50을 추가한 뒤 total을 호출한다. 저장소의 int 배열40,50과 size2를 MemberCalculations.sum에 전달하면90을 반환한다. application.run() registry로 호출 add(40), add(50) total() sum(age, size) main · static new MemberRegistry(2) 그 참조로 application 생성 이 예제의 객체 조립점 SignupApplication 객체 final MemberRegistry registry 전달받은 저장소 참조를 보관 run()은 인스턴스 메서드 MemberRegistry 객체 age: int[] {40, 50} size = 2 total()은 age와 size를 전달 MemberCalculations 클래스 static sum(values, size) index: 0, 1 → 합계 90 이 호출은 배열 값을 읽기만 함
  1. 정적 main의 객체 조립

    new MemberRegistry(2)로 저장소를 만들고, 그 참조로 SignupApplication 객체를 생성한 뒤 application.run()을 호출합니다.

  2. SignupApplication의 저장소 참조

    final MemberRegistry registry 필드에 전달받은 참조를 보관합니다. 인스턴스 메서드 run()이 그 저장소의 add(40), add(50), total()을 차례로 호출합니다.

  3. MemberRegistry가 보관한 상태

    두 추가 뒤 age는 {40, 50}인 길이 2의 int[]를 가리키며 size = 2입니다. total()은 MemberCalculations.sum(age, size)를 호출합니다.

  4. MemberCalculations의 정적 계산

    클래스의 static sum(values, size)가 인덱스 0, 1의 값을 읽어 90을 반환합니다. 이 호출은 배열 값을 바꾸지 않습니다.

합계 90이 total()을 거쳐 run()으로 돌아가 total=90을 출력합니다. 이 저장소의 add에는 용량·나이 검증이 없으며, 위 상태는 원문 두 번의 추가에 한정합니다.


연습 문제

인스턴스를 만들 수 없는 MemberAgeRules 유틸리티를 만들고 isAdult(int age)와 group(int age)를 static으로 제공하세요.

14세 미만과 120세 초과는 거절하고, 18세 이상은 성인으로 판정하며 65세 이상은 senior로 분류합니다.

해설 보기
src/MemberAgeRulesUtility.java
public final class MemberAgeRulesUtility {
    public static void main(String[] args) {
        System.out.println(MemberAgeRules.isAdult(19));
        System.out.println(MemberAgeRules.group(65));
    }

    private static final class MemberAgeRules {
        private MemberAgeRules() {
            throw new AssertionError("no instances");
        }

        static boolean isAdult(int age) {
            validate(age);
            return age >= 18;
        }

        static String group(int age) {
            validate(age);
            if (age >= 65) return "senior";
            return age >= 18 ? "adult" : "teen";
        }

        private static void validate(int age) {
            if (age < 14 || age > 120) throw new IllegalArgumentException("age");
        }
    }
}
true
senior

두 결과는 인수에만 의존하고 저장할 인스턴스 상태가 없습니다.