static의 활용

October 31, 2022

static 변수와 메서드

static 키워드를 통해서 생성한 변수와 메서드는 모든 인스턴스가 공유하며 객체화하지 않고도 사용할 수 있다는 특징을 가진다.

public final class Math {
    public static final double PI = 3.14159265358979323846;

    private Math() {}

    public static double getAreaOfCircle(double radius) {
        return radius * radius * PI;
    }

    ...
}

위 예시에서 Math 클래스는 객체화 될 필요가 없다. PI라는 상수는 수학적 불변으로 객체마다 달라서는 안되는 상수이고, 원의 넓이를 구하는 메서드 또한 Math 객체의 존재 유무가 상관 없기 때문이다. 이처럼 static은 상수 변수를 정의한다거나 유틸리티 성격의 메서드를 구현하는데 있어 많이 사용된다.

static 멤버들은 일반적인 객체의 라이프사이클과는 궤를 달리한다. 일반적인 객체들이 특정 시점에 생성되어 메모리 상 힙 영역에 저장된다면, 이 놈들은 애플리케이션이 시작될 때 관련 정보가 별도의 메모리 영역에 할당된다. 예전 자바8 이전의 자바라면 힙 영역의 Permanant 영역에 저장되며, 자바8 부터는 Native Memory의 Metaspace라는 영역에 저장된다.

이처럼 객체지향과는 다른 개념으로 동작하기 때문에 사용 시 주의해야하는 점도 많이 존재한다. 위에 명시한 것처럼 관련 데이터들이 애플리케이션의 시작될 때 메모리에 적재되고 애플리케이션이 종료하는 그 시점까지 공간을 차지하고 있다. 가비지 컬렉터에 의해 관리되는 영역이 아니기에 과도한 사용으로 인한 성능저하 또는 메모리 누수 등의 문제가 생길 수 있음을 유의해야 한다.

또한, 정적 멤버 변수 같은 경우는 특정 객체에 종속된 것이 아닌 공유되고 있는 리소스임을 명심해야 한다. 상수로 활용한다면 문제가 없겠지만 가변이라면 동기화에 대한 고려를 해야 한다.

그리고 정적 메서드 같은 경우는 오버라이딩할 수 없다. 다형성을 통해 오버라이딩한 메서드를 메서드를 호출하는 개념은 런타임에 결정되는데, 이 같은 경우에는 애플리케이션 시작 시점에서 강하게 결합되어버리기 때문이다. 따라서, 반드시 정적 메서드를 정의해놓은 클래스를 통해서만 호출할 수 있다.

static block

단순히 정적 변수에 값을 할당할 수도 있지만, 어느정도 복잡한 연산이 필요하다면 어떡해야할까? 이 때 사용할 수 있는 것이 static block이며 클래스가 로드되는 시점에 딱 한 번만 실행되게 된다.

public final class Math {
    public static final double PI;
    
    static {
        PI = 3.14159265358979323846;
    }

    private Math() {}

    public static double getAreaOfCircle(double radius) {
        return radius * radius * PI;
    }

    ...
}

Nested Class와 static

class A {
	class B {

	}
}
class A {
	static class B {

	}
}

Nested 클래스를 선언하는데 있어 위와 같이 static 유무에 따라 정적 멤버 클래스와 비정적 멤버 클래스로 나누어진다. 정적 멤버 클래스의 특징을 이해하기 전에 먼저 비정적인 멤버 클래스의 특징을 파악하는 것이 필요하다.

중첩 클래스를 비정적으로 선언하게 되면 A와 B는 강한 결합 관계에 놓이게 된다. 이는 개념상 A와 B가 독립적으로 존재할 수 없음을 표현하며, 감싸고 있는 클래스(A)의 객체 없이는 중첩 클래스(B) 객체를 생성할 수 없는 구조이다. 이 때, 비정적 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결되며 중첩된 클래스에서는 A.this와 같이 바깥 클래스의 메서드를 호출하거나 인스턴스의 참조를 가져올 수 있다.

결론부터 말하자면 꼭 필요한 경우가 아니라면 되도록 정적 멤버 클래스를 사용하는 것이 좋다. 위에서 명시된 특징으로 인해 두 객체 사이의 관계 정보는 어딘가에 저장되어 있음이 필요하다. 이는 비정적 멤버 클래스의 인스턴스에 저장되게 되는데 이 때문에 몇 가지 문제가 발생할 수 있다.

먼저, 관계를 표현하기 위해 별도의 저장 공간이 더 필요한 점. 그리고 이 관계 때문에 객체의 생성 속도가 느려질 수 있다는 것이다. 더 중요한 내용은 이 관계는 감싸는 클래스(A)에 대한 숨은 참조 형태로 발생되는데 잘못 사용하는 경우 이 때문에 가비지 컬렉터에 걸리지 않고 메모리 누수로 이어질 수 있다는 점이다. 그래서 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 반드시 아래 정적 멤버 클래스를 사용하는 것이 좋다.

반면 정적 멤버 클래스 같은 경우에는 둘 사이에 위 같은 결합이 존재하지 않는다. 감싸고 있는 클래스(A)의 private 멤버에 직접 접근할 수 있다는 것만 제외하고는 일반 클래스와 동일하며 A 객체 없이도 접근 가능하다. 또 다른 변수나 멤버 처럼 접근 제어자도 완전히 동일하게 동작한다.

정적 멤버 클래스의 사용 예시로는 감싸고 있는 클래스와 함께 사용될 때 유용한 역할을 하는 보조 클래스를 사용하는데 사용될 수 있다. 예를 들어, 계산기 객체인 Caculator가 존재한다고 가정했을 때 이 객체에서 제공하는 연산에 대한 열거 타입을 중첩 클래스로 정의할 수 있다. 클라이언트는 Calculator.Operation.PLUS와 같이 접근할 수 있는 것이다.

또한 private하게 사용할 수도 있는데 이 때는 주로 감싸고 있는 클래스에서 표현하는 객체의 부분을 나타낼 때 사용된다. 예를 들어, 많은 Map 구현체는 내부적으로 키-값 쌍을 가지는 Entry 객체를 가지고 있다. 하지만 이 엔트리의 메서드(e.g. getKey, getValue, setValue) 등을 직접 사용하지는 않기에 private 정적 멤버 클래스로 사용할 수 있다.


참고

  • Joshua Bloch, Effective Java, 프로그래밍인사이트

songmk 🙁