개발자는 기록이 답이다

🐙 static 키워드를 잘못 사용했을 때 발생할 수 있는 문제와 올바른 사용법 본문

언어/Java

🐙 static 키워드를 잘못 사용했을 때 발생할 수 있는 문제와 올바른 사용법

slow-walker 2023. 12. 21. 17:20

 

static 키워드는 클래스의 멤버(변수 또는 메서드)에 적용되며, 해당 멤버가 클래스 자체에 속하도록 지정한다.

static 멤버는 클래스의 인스턴스와 관련이 없이 클래스 자체에 속하므로, 여러 인스턴스 간에 데이터를 공유하고 특정 작업을 수행하는 데 사용된다.

 

static키워드를 사용하는 이유?

주로 메모리 효율성과 프로그램 구조의 편의성을 높이기 위해서이다. 먼저, static 멤버는 클래스 수준에 속하므로 프로그램이 시작될 때 메모리에 할당되어 종료될 때까지 유지된다. 이로써 여러 인스턴스에서 동일한 값을 공유하거나 전역 상태를 유지할 때 메모리를 효율적으로 사용할 수 있다.

 

또한, static 멤버는 객체의 생성 없이 직접 클래스 이름으로 접근할 수 있어서, 클래스 수준에서 독립적인 기능을 구현할 수 있다. 이는 유틸리티 메서드나 상수값 정의와 같은 상황에서 매우 유용하다. 정적 메서드를 통해 객체 생성 없이도 호출이 가능하므로, 일반적으로 여러 인스턴스에 공통적인 작업을 수행하는 데에도 활용된다.

또한, 싱글톤 패턴과 같이 클래스당 하나의 인스턴스를 유지해야 하는 경우에도 static 멤버를 사용할 수 있다. 이는 객체의 생명 주기에 독립적으로 상태를 관리하고, 전역적인 기능을 제공하는 데에 큰 도움을 준다.

 

static멤버과 인스턴스 멤버의 비교

특징 static 멤버 인스턴스 멤버
소속 클래스 자체에 속함 개별 객체(인스턴스)에 속함
접근 방법 클래스 이름으로 직접 접근 가능 객체 인스턴스를 생성하여 접근
메모리 할당 시점 프로그램 시작 시 할당되고,
프로그램 종료 시 소멸
객체 인스턴스 생성 시 할당되고,
가비지 컬렉션에 의해 소멸
인스턴스와의 관계 인스턴스와 독립적 각 인스턴스마다 별도의 멤버를 가짐
인스턴스 변수 접근 직접 접근 불가능 직접 접근 가능
공유 데이터 여러 인스턴스에서 동일한 값 공유 각 인스턴스가 독립된 값을 가짐
this 키워드 사용 사용 불가능 사용 가능
메서드 호출 정적 메서드에서는 정적 메서드만 직접 호출 가능 모든 메서드 호출 가능
생성 시점 클래스 로딩 시 초기화 객체 인스턴스 생성 시 초기화
상속 관계에서의 동작 하위 클래스에서 오버라이딩 불가능 하위 클래스에서 오버라이딩 가능
사용 예시 공용 상수, 유틸리티 메서드, 싱글톤 패턴 객체의 상태 및 행위를 나타내는 변수와 메서드 등

 

 

잘못 사용했을 때 발생할 수 있는 문제

  1. 무분별한 Static의 사용은 Memory Leak의 원인이 됩니다.
    - Static으로 선언된 모든 것들은 프로그램이 시작될 때 메모리를 할당받습니다. 메모리를 해제하기 위해선 프로그램을 종료해야하는데, 만약 무분별하게 Static을 사용하게 된다면 GC가 메모리 해제를 하지 못해 static 필드에 데이터가 계속 쌓이고 메모리 부족현상(Out of Memory)이 발생될 수 있습니다.

  2. 객체지향적이지 않습니다.
    - 많은 개발자들이 static을 악으로 규정하고 있는 이유 중 하나입니다. 객체지향개발요소 중 하나인 캡슐화가 존재합니다. 캡슐화란 한 객체가 가지고 있는 데이터들은 외부에서 함부로 접근하여 수정할 수 없도록 하는 원칙입니다. 그러나 Static을 사용할 경우 어디서든 접근이 가능하기 때문에 캡슐화의 원칙을 위배하게 됩니다.


  3. Static은 재사용성이 떨어집니다.
    Static으로 선언된 메소드는 Interface를 구현하는데 사용될 수 없습니다. 따라서, 객체지향개발 특징 중 한가지인 재사용성을 살리지 못하기 때문에 객체지향개발에 방해가 됩니다.


  4. Static은 Thread-safe하지 않습니다.
    - Static은 프로그램 전역에서 사용되기 때문에 모든 스레드에서 static 필드를 공유하게 됩니다. 이때 한 스레드에서 값을 변경할 경우 다른 모든 스레드에 영향을 받아서 동시성 문제를 야기합니다. 이처럼 Thread-safe하지 않기 때문에 멀티 스레드 동작에서 충돌이 발생될 수 있습니다. 이를 해결하기 위해  synchronize를 사용해야 하는등 추가 작업이 필요합니다.

  5. 코드레벨에서 잘못 쓰는 경우, 정적 메소드에는 정적 변수만 접근할 수 있습니다.

 

 

올바른 static 키워드 사용법

위의 내용들을 보고, static을 무조건 쓰면 안된다고 생각할 수 있겠지만, 아래와 같은 상황에서는 유용하게 쓰인다고 한다.

 

1. 상수 정의

Static 키워드를 사용하여 상수를 정의하는 경우, 여러 곳에서 공유하고 일관성 있게 사용할 수 있습니다.

public class Constants {
    public static final int MAX_VALUE = 100;
}

 

2. 틸리티 메서드

정적 메서드를 사용하여 유틸리티 함수를 구현하는 경우, 객체의 인스턴스를 생성하지 않고도 사용할 수 있습니다.

 

public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }
}

 

3. 팩토리 메서드

정적 메서드를 사용하여 객체의 생성을 담당하는 팩토리 메서드를 구현하는 경우, 객체 생성을 캡슐화하고 유연성을 확보할 수 있습니다.

public class CarFactory {
    public static Car createCar(String model) {
        // 객체 생성 로직
    }
}

 

4. 싱글톤 패턴

클래스당 하나의 인스턴스를 유지해야 하는 경우, 정적 변수를 활용하여 싱글톤 패턴을 구현할 수 있습니다.

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 생성자 로직
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 


 

🤔 느낀점

 

 

우아한 테크코스 미션을 하면서 static을 굉장히 많이 썼던 것 같다.

개발하기 편리하단 이유로 static을 남발하면서 사용했었는데, 이번 기회로 왜 객체 지향적인 코드가 아니고 절차 지향적인 코드라는지 이해가 된다.

 

static을 남발했던 위치와 이유에 대해서 얘기해보자.객체 생성시 static 메소드로 생성자를 반환하는 코드를 많이 사용했었다. 또한, 테스트 코드만들때마다 특정 메소드에 접근하기 위해 객체를 생성하는게 번거로워서 그랬던 건데, 그러지 말아야 겠다.

 

static키워드를 전 회사에서 사용했던걸 생각해보면 Bidecimal타입을 형변환하거나 계산하는 일부 기능은 Utils클래스에서 static메소드로 만들었는데, 다행히 올바르게 사용한것 같다.

또한 기억나는게, 에러메세지를 static final로 정의하느냐, enum열거형으로 정의하느냐도 고민이었는데,

결국 에러메세지는 static final로 상수로 정의했었다. 에러메시지 같은 경우에는 2개 중 어떤걸로 정의하는게 좋을까?

"우테코에서도 연관성이 있는 상수는 static final 대신 enum을 활용한다"고 알려줬었는데, 단순이 에러 메세지만 있는 경우라면 static final 상수로 선언해도 되고, 다른 status코드처럼 해당 상수에 대한 그룹화가 존재한다면 enum을 사용하는게 좋을 것 같다. 

public class ErrorMessages {
    public static final String INVALID_INPUT = "Invalid input. Please check your input data.";
    public static final String SERVER_ERROR = "Internal server error. Please try again later.";
}
public enum ErrorMessages {
    INVALID_INPUT("Invalid input. Please check your input data."),
    SERVER_ERROR("Internal server error. Please try again later.");

    private final String message;

    ErrorMessages(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

 

아래처럼 Enum으로 에러메세지를 두 가지 그룹으로 정의하고 있는 경우를 살펴보자.

첫 번째 그룹은 HTTP 상태 코드이고, 두 번째 그룹은 사용자 정의 에러 메세지이다.

각 상수는 코드와 메세지를 가지고 있으며, getFullMessage와 같은 추가 메서드를 제공하여 필요한 로직을 수행할 수 있는 경우 ENUM!!

public enum ErrorCode {
    // 그룹 1: HTTP 상태 코드
    HTTP_200_OK(200, "OK"),
    HTTP_404_NOT_FOUND(404, "Not Found"),
    HTTP_500_INTERNAL_SERVER_ERROR(500, "Internal Server Error"),

    // 그룹 2: 사용자 정의 에러 메세지
    USER_ERROR_1(1001, "사용자 정의 에러 1 발생"),
    USER_ERROR_2(1002, "사용자 정의 에러 2 발생");

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    // 각 상수에 대한 추가 메서드나 로직을 정의할 수 있음
    public String getFullMessage() {
        return "[" + code + "] " + message;
    }
}

 

static키워드의 올바른 사용법

static 사용을 피해야 하는 이유

자바 static, final, static final 차이