자바의 static 키워드 이해하기

Tony·2024년 4월 19일
post-thumbnail

static 키워드는 객체 지향 프로그래밍에서 중요한 역할을 수행합니다. 클래스 인스턴스 간에 공유되는 속성이나 메서드를 정의해서 메모리를 효율적으로 사용할 수도 있지만, 이것은 static 키워드가 사용된 값이 변경되면 모든 인스턴스에 영향을 미친다는 뜻이 됩니다. 하나의 사이드 이펙트도 서비스 장애를 일으킬 수 있기 때문에 신중하게 인지하고 사용하는 것이 중요합니다. 아래 내용을 통해 static을 언제, 어떻게 활용하면 좋을지 생각해볼 수 있기를 기대합니다.


1. static이란

static은 자바에서 인스턴스들이 공유하는 클래스 레벨의 정의에 사용되는 키워드입니다. 주로 static 변수, static 메서드를 선언하는데 사용되며 [클래스 변수, 정적 변수, static 변수], [클래스 메서드, 정적 메서드, static 메서드] 모두 같은 의미로 사용됩니다. 클래스가 로드될 때 단 한 번만 메모리에 할당되고, 이후 생성되는 모든 인스턴스에서 공유되기 때문에 상수값이나 공유해야 할 자원을 관리할 때 유용합니다.

2. static 생명 주기

static 키워드가 사용된 코드는 클래스 로더에 의해 JVM 버전에 따라 Method Area(Java 8 이전) 또는 Metaspace(Java 8 이후)에 적재됩니다.
이후 클래스가 JVM에서 언로드 될 때 메모리를 반환하는데, 일반적으로 JVM이 종료될 때까지(프로그램 종료) 메모리에 유지됩니다.

3. static 초기화

클래스 로더는 다음과 같은 상황에서 static 영역을 초기화합니다.

  • static 키워드가 사용된 클래스의 인스턴스 생성
  • static 변수에 접근
  • static 메서드 호출

4. static 키워드 활용의 장점

  • 메모리 효율성
    • static 변수는 클래스가 로드될 때 한번만 메모리에 할당되고, 이를 모든 인스턴스가 공유해서 사용하기 때문에 이러한 상황에서는 메모리를 효율적으로 사용할 수 있습니다.
  • 실행 시간 단축
    • static 변수와 메서드는 클래스 로딩 시점에 초기화되므로, 첫 사용 시 약간의 지연이 발생할 수 있지만 유틸리티 클래스의 메서드를 여러 곳에서 사용할 때 실행 시간 측면에서 더 유리합니다.
  • 유지보수성 향상
    • static 키워드를 사용한 코드는 클래스 전체와 관련된 것이라는 뜻을 명확하게 표현할 수 있어서 유지보수가 용이해집니다.

5. static 사용 시 주의사항

  • 캡슐화 위배
    • 인스턴스 생성 없이도 전역에서 접근할 수 있기 때문에 이는 각 객체의 데이터를 외부로부터 보호하려는 객체지향 원칙인 캡슐화 특성에 위배될 수 있습니다.
  • Method Hiding - 다형성 위배
    • static 메서드는 상속받은 후에도 Override 되지 않습니다.
    • 자식 클래스에서 부모 클래스의 static 메서드에 @Override 어노테이션 사용 시 컴파일 에러가 발생합니다.
    • @Override 어노테이션을 삭제하고 컴파일하면 마치 오버라이딩 된 것처럼 보이지만, 부모와 자식 클래스에서 동일 이름의 메서드가 독립적으로 존재하는 method hiding이 발생합니다.
    • 이러한 method hiding은 코드의 가독성을 저하시키고 런타임 다형성을 불가능하게 합니다.
  • Memory Leak
    • Method(또는 Metaspace) 영역에 적재되기 때문에 GC의 대상이 되지 않습니다.
    • 따라서 static 변수가 collection 내 불필요한 데이터의 지속적인 보관 등 부적절한 구조에서는 Memory Leak이 발생할 수 있습니다.
  • 동시성 문제
    • 전역에서 접근할 수 있기 때문에 여러 스레드가 동일한 static 변수를 동시에 수정할 때 데이터 무결성 문제가 발생하는 동시성 문제가 발생할 확률이 높습니다. Atomic 클래스, Lock 클래스, synchronized 구문 등을 통한 대응이 필요합니다.

6. static 변수

해당 클래스에서 생성된 인스턴스 전체가 공유하는 변수가 필요할 때 사용합니다.

public class Employee {
    private static int employeeCount = 0; // static 변수, 생성자를 호출할 때마다 1씩 증가
    private String name;

    public String getName() {
        return name;
    }

    public Employee(String name) {
        this.name = name;
        employeeCount++;
    }

    public int getTotalEmployeeCount() {
        return employeeCount;
    }

    public static void main(String[] args) {
        Employee kim = new Employee("Kim");
        Employee park = new Employee("Park");

        System.out.println("[" + kim.getName() + "] " + "Total Employees: " + kim.getTotalEmployeeCount());
        System.out.println("[" + park.getName() + "] " + "Total Employees: " + park.getTotalEmployeeCount());
    }
}
# 실행결과

[Kim] Total Employees: 2
[Park] Total Employees: 2

Process finished with exit code 0

7. static 메서드

static 메서드는 인스턴스 생성 없이 호출할 수 있는 메서드입니다.
static으로 선언된 클래스 속성이 아닌 인스턴스의 속성에는 접근할 수 없고, 값을 외부에서 주입할 수밖에 없기 때문에 객체지향 원칙과 상충되는 부분이 존재합니다.
하지만 java.lang.Math 등 개발에 유용한 유틸리티성 클래스를 만드는데 사용할 수 있습니다.
유틸리티 클래스는 재사용성, 모듈성을 가지고 있기 때문에 적절하게 사용하면 개발 효율을 높일 수 있습니다.

public class Calculator {
    public static int add(int number1, int number2) { // static 메서드, 두 수의 합 반환
        return number1 + number2;
    }

    public static void main(String[] args) {
        System.out.println("Sum: " + Calculator.add(10, 20));
        System.out.println(Math.max(10, 20)); // 유틸리티 클래스, 더 큰 수를 반환
    }
}
# 실행결과

30
20

Process finished with exit code 0

8. static 블록

static 블록은 클래스가 JVM의 클래스 로더를 통해 최초 메모리 적재 시점에만 한번 호출되는 블록입니다.
DB 연결, 특정 라이브러리 초기화 등 어플리케이션에서 한번만 호출해주면 되는 상황에 사용할 수 있습니다.

public class Employee {
    public static int employeeCount;

    static { // static 블록, 처음 한번만 호출됨
        System.out.println("클래스 최초 로딩");
        employeeCount = 100;
    }

    public static void main(String[] args) {
        System.out.println("bookCount: " + Employee.employeeCount);
        System.out.println("bookCount: " + Employee.employeeCount);
        System.out.println("bookCount: " + Employee.employeeCount);
    }
}
# 실행결과

클래스 최초 로딩
bookCount: 100
bookCount: 100
bookCount: 100

Process finished with exit code 0

9. static final

static final은 상수를 선언할 때 사용되는 키워드입니다.
static 초기화와는 다르게 static final로 선언한 기본형 또는 문자열 리터럴 상수는 이미 컴파일 타임에 값으로 변환되어 코드에 들어갑니다.
따라서 이러한 상수는 런타임에 클래스 로더에 의한 별도의 초기화가 필요하지 않습니다.

public class Speaker {
	public static final int MAX_VOLUME = 100;
    
    public static void main(String[] args) {
        System.out.println("MAX_VOLUME: " + MAX_VOLUME);
    }
}
# 실행결과

MAX_VOLUME: 100

Process finished with exit code 0

정리

이렇듯 static 키워드는 적절하게 사용했을 때 프로그램의 메모리 효율성, 유지보수성 향상, 실행 속도 단축 등의 효과를 얻을 수 있습니다.
하지만 캡슐화, 다형성에 위배될 수 있고 메모리 릭이나 동시성 문제가 발생할 수 있는 등 오히려 사용을 지양하는 것이 나을 수 있습니다.
프로그램 구조와 객체지향에 대한 정확한 이해를 바탕으로 static을 사용해야 합니다.

참고

profile
기술 자체를 목적으로 하지 않고, 사용자 경험과 비즈니스 가치를 최우선으로 생각합니다.

0개의 댓글