코딩관계론

static, final 어디까지 알아보고 왔는가? 본문

개발/Java

static, final 어디까지 알아보고 왔는가?

개발자_티모 2024. 6. 9. 00:00
반응형

자바 언어를 배우다보면 static, final 변수가 중요하다고는 배우지만 왜 중요한지 어떤 특성으로 인해서 공유가 가능한지, 수정이 불변한지는 배우지 않는다.

Static 변수

Static 변수는 모든 클래스 인스턴스에서 하나의 변수를 공유하기 위해서 사용됩니다. 하지만 어떻게 모든 인스턴스에서 하나의 변수 값을 참조할 수 있을까요? 컴파일 과정과 JVM의 메모리 영역을 생각해보면 답이 있다(2024.06.02 - [개발/Java] - 자바 어떻게 실행되는가?)

 

클래스 로더가 JVM의 바이트코드 파일을 읽어 메모리에 할당하면서 링킹 과정이 수행됩니다. 이때 static 변수가 초기화되며, 이후 초기화 과정을 통해 static 변수에 실제 값이 할당된다.

 

이 과정을 거치게 되면 static 변수는 메모리에 올라가 있다.하지만 모든 곳에서 접근이 가능하게 한다는 것은 별개의 문제이다. 왜냐하면 jvm에서는 각 thread만 가지는 메모리 영역, 모든 thread가 공유하는 메모리 영역이 있기 때문이다. (2024.06.08 - [개발/Java] - JVM 메모리 구조)

 

모든 인스턴스에서 접근하려면 heap영역 or method영역에 초기화가 되어야 한다. static의 경우에는 method 영역에 메모리를 올린다.

 

힙 영역은 객체의 생명 주기에 따라서 GC가 동작해서 메모리를 사용한다. 하지만 static 변수를 생각해보면 객체의 생명주기에 따라서 소멸되야 하는 변수가 아니기 때문에 method영역에 초기화가 된다.

장점만 있을까?

1. 메모리 낭비

앞서 봤듯이 이 변수의 생명주기가 굉장히 애매하다. 따라서 이 변수는 method area가 소멸되는 시점에서 소멸이 되는데 method area는 JVM이 종료될 때 소멸됨으로 메모리 낭비가 있을 수 있다.

2. 다중 스레드 환경

method area는 모든 Thread가 접근할 수 있다. 따라서 각 thread에서 동시에 해당 변수를 조작할 수 있기 때문에 sync처리가 필요하고, 부가적인 오버헤드를 유발할 수 있다.

 

static 클래스

 

Static 클래스는 주로 내부 클래스(inner class)로 사용되며, 외부 클래스 인스턴스에 대한 참조가 없도록 설계됩니다. 특이한 점은 static클래스를 인스턴스화 하면 힙메모리에 할당되게 됩니다. 따라서 GC의 대상이 됩니다

 

만약 아래 처럼 객체가 내부 객체를 new하면 이는 gc의 대상이 될까? 정답은 안된다. 왜냐하면 객체들이 서로 참조하고 있기 때문에 사라지지 않게되고, 이는 보이지 않는 메모리 누수로 이어질 가능성이 높다.

class OuterClass {
    private InnerClass innerObject;

    public OuterClass() {
        // 외부 클래스의 생성자에서 내부 객체를 생성
        innerObject = new InnerClass();
    }

    private class InnerClass {
        // 내부 클래스
    }

    public void createInnerObject() {
        // 메소드에서 내부 객체를 생성
        innerObject = new InnerClass();
    }
}

public class Main {
    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        
        // outer가 가비지 컬렉션의 대상이 아닌 한, innerObject도 대상이 되지 않음
        outer.createInnerObject();
        
        // outer를 null로 설정해도 innerObject가 GC의 대상이 되지는 않음
        outer = null;
        
        // 이 시점에서 outer와 innerObject는 여전히 메모리에 존재
        // outer가 null이 되더라도 innerObject가 외부 참조를 가지고 있기 때문에
        // outer와 함께 innerObject도 GC의 대상이 되지 않음
    }
}

=

 

 

Final

Final 변수는 한 번 할당되면 그 값을 변경할 수 없습니다. 주로 상수나 불변 객체를 표현하는 데 사용됩니다. 이 변수는 선언될 때 초기화되거나 생성자를 통해 초기화되어야 하며, 이후에는 그 값을 변경할 수 없습니다.

 

final 변수를 수정하려고 접근하면 컴파일 불가능한 것을 확인할 수 있습니다. 왜 실행오류가 아니라 컴파일 오류지라는 생각이 듭니다.

이는 컴파일러가 final 변수에 대해서 아래와 같이 컴파일 시점에 검사를 진행하기 때문입니다.

 

  • 컴파일 타임 상수 (Compile-time Constant): final 변수가 기본형(primitive type)이거나 문자열(String) 타입일 경우, 해당 변수에 할당된 값을 컴파일 타임에 이미 알 수 있습니다. 따라서 이 값을 컴파일 시점에 해당하는 모든 곳에 직접 사용하여 컴파일된 코드에는 해당 변수의 값이 직접 삽입됩니다. 이로써 final 변수의 값이 수정되지 않도록 보장됩니다.
  • 컴파일러가 코드 검증: final 변수의 값을 변경하려고 하는 시도는 컴파일러가 코드를 분석하여 검증하고, 만약 final 변수가 이미 초기화되었다면 그 값을 변경할 수 없다는 오류를 발생시킵니다. 이는 Java 언어의 문법 및 규칙을 준수하도록 하는 역할을 합니다. 

Final 변수는 보통 선언 시에 초기화되며, 이후에는 값을 변경할 수 없으므로 프로그램의 안정성과 유지보수성을 향상시키는 데 도움이 됩니다. 이러한 특성으로 인해 상수나 불변성을 필요로 하는 상황에서 Final 변수를 주로 사용합니다.

 

마지막으로, Final 변수는 메모리 상에 고정된 값으로 존재하며, 다른 객체와 마찬가지로 힙 영역이나 메소드 영역에 저장될 수 있습니다. 하지만 한 번 할당되면 그 값을 변경할 수 없기 때문에 변경 불가능한 변수로써 메모리에 유지됩니다.

 

Final 클래스

final 키워드는 자바에서 다양한 용도로 사용되며, 이를 클래스에 사용하면 해당 클래스는 상속이 불가능하게 됩니다. 다시 말해, final 클래스를 다른 클래스가 확장할 수 없게 만드는 것입니다.

 

장점

  • 상속 불가능: final 클래스를 선언하면 다른 클래스에서 이를 상속받을 수 없습니다. 이는 클래스의 확장 및 유연성을 제한하지만, 클래스가 변경되지 않도록 보장합니다. 예를 들어, 보안상의 이유로 변경이 필요 없는 클래스나 상속을 통해 의도치 않은 동작 변화를 방지하고자 할 때 유용합니다.
  • 안정성: 클래스의 동작이 변경되지 않도록 보장하므로, 안정성을 높이는 데 기여할 수 있습니다. 특히, 라이브러리나 API 설계 시 중요한 클래스에 final을 적용하면 외부에서 해당 클래스를 확장하여 의도치 않은 버그를 발생시키는 일을 예방할 수 있습니다.

단점 

  • 제한된 유연성: final 클래스는 상속할 수 없기 때문에, 클래스를 재사용하거나 확장하려는 경우 유연성이 떨어집니다. 이는 상황에 따라 단점이 될 수 있습니다.
  • 테스트의 어려움: 유닛 테스트를 작성할 때 final 클래스는 모킹(mocking)이 어려울 수 있습니다. 테스트 프레임워크에 따라 final 클래스를 모킹하려면 추가적인 설정이나 도구가 필요할 수 있습니다. 

Final 메소드

final 키워드는 메소드에 사용될 때 해당 메소드가 하위 클래스에서 오버라이딩(overriding)되지 않도록 방지합니다. 다시 말해, final 메소드는 선언된 클래스 내에서만 사용되며, 이를 상속받는 클래스에서 기능을 재정의할 수 없습니다.

장점

  1. 변경 불가: final 메소드는 하위 클래스에서 변경될 수 없기 때문에, 클래스 설계자가 의도한 대로 메소드의 동작이 유지됩니다. 이는 중요한 메소드의 무결성을 보장하는 데 유용합니다.
  2. 안정성: 중요한 비즈니스 로직이나 보안 관련 메소드를 final로 선언하여, 하위 클래스에서 실수로 변경하거나 오버라이딩하여 발생할 수 있는 문제를 방지할 수 있습니다.
  3. 성능 최적화: 컴파일러와 JVM이 final 메소드를 최적화하는 데 유리합니다. final 메소드는 오버라이딩되지 않으므로, 호출 시 메소드의 실제 구현을 확실히 알 수 있어 인라인(inline) 최적화 등의 성능 향상이 가능합니다.

단점

  1. 제한된 유연성: final 메소드는 오버라이딩할 수 없기 때문에, 메소드의 기능을 변경하거나 확장할 수 없습니다. 이는 재사용성과 확장성을 제한할 수 있습니다.

Final 매개변수 

final 키워드는 매개변수에 사용될 때 해당 매개변수가 메소드내에서 수정되지 않도록 방지합니다. 다시 말해, final 매개변수는 메소드 내에서만 사용되며, 이를 수정하거나 재할당할 수 없습니다.

 

장점

  1. 불변성: final 매개변수는 메소드 내에서 변경될 수 없으므로, 매개변수의 값을 보존할 수 있습니다. 이는 코드의 가독성과 유지보수성을 높여줍니다.
  2. 명확성: 메소드가 호출될 때 전달된 인자의 값이 메소드 내에서 변경되지 않음을 명확히 하여, 코드의 동작을 쉽게 예측할 수 있게 합니다.
  3. 안전성: 특히 긴 메소드나 복잡한 로직을 다룰 때, final 매개변수는 실수로 인한 값 변경을 방지하여 코드의 안정성을 높여줍니다.

단점

  1. 제한된 유연성: final 매개변수는 값을 변경할 수 없으므로, 특정 상황에서는 유연성이 떨어질 수 있습니다. 예를 들어, 매개변수를 수정하여 중간 계산을 수행해야 하는 경우 final 매개변수를 사용할 수 없습니다.
  2. 장황함: 모든 매개변수를 final로 선언하는 것은 코드가 장황해 보일 수 있으며, 꼭 필요한 경우에만 사용하는 것이 좋습니다.

 

 

 

테스트는 어렵지 않을까?

  1. Static 변수:
    • 전역 상태: static 변수는 클래스 수준에서 공유되기 때문에 전역 상태를 가지게 됩니다. 이는 테스트 간에 상태가 공유될 수 있음을 의미하며, 테스트가 서로 간섭하게 될 수 있습니다.
    • 초기화 문제: static 변수는 클래스 로드 시 한 번 초기화되기 때문에 테스트마다 초기 상태를 유지하기 어렵습니다. 이를 해결하려면 매 테스트 전에 초기화하거나 리플렉션을 사용해야 할 수 있습니다.
    • 의존성 주입 어려움: static 변수는 의존성 주입(DI)을 통해 쉽게 교체할 수 없습니다. 이는 테스트 시 모의 객체(mock)를 사용하여 의존성을 대체하기 어렵게 만듭니다.

만약 static변수를 선언한 클래스를 동시에 여러가지 형식을 테스트하고 싶을 때를 생각해보라. 아래의 코드처럼 테스트에 실패하는 상황이 나올 것이다.

public class ConfigurationManager {
    private static String configuration = "default";

    public static void setConfiguration(String config) {
        configuration = config;
    }

    public static String getConfiguration() {
        return configuration;
    }

    public static void resetConfiguration() {
        configuration = "default";
    }
}


import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Execution(ExecutionMode.CONCURRENT)
public class ParallelConfigurationManagerTest {

    @Test
    public void testCustomConfiguration1() throws InterruptedException {
        ConfigurationManager.setConfiguration("parallel_custom_1");
        Thread.sleep(100); // 일부러 지연시켜 상태 변경 가능성 증가
        assertEquals("parallel_custom_1", ConfigurationManager.getConfiguration());
    }

    @Test
    public void testCustomConfiguration2() throws InterruptedException {
        ConfigurationManager.setConfiguration("parallel_custom_2");
        Thread.sleep(100); // 일부러 지연시켜 상태 변경 가능성 증가
        assertEquals("parallel_custom_2", ConfigurationManager.getConfiguration());
    }
}

 

  1. Final 변수:
    • 불변성: final 변수는 한 번 초기화되면 값을 변경할 수 없습니다. 이는 일반적으로 좋은 특성이지만, 테스트 환경에서 특정 값을 변경하고 싶을 때 어려움을 줄 수 있습니다.
    • 설정 어려움: 생성자나 초기화 블록에서 초기화된 final 변수는 테스트할 때마다 재설정할 수 없습니다. 이는 테스트 환경 설정을 복잡하게 만들 수 있습니다.

특정 데이터가 10만개 이상이 들어온다면 fail이 발생하는 로직이 있다고 한다. 이 fail처리가 올바르게 작동하는지 확인해보고 싶어 10개만 들어와도 오류처리가 되는지 체크해보는 상황인데 이 때 10만이 fianl변수로 선언되어 있다면 코드를 수정해야 하기 때문에 좋지 못한 테스트가 된다. 

public class DataProcessor {
    private static final int MAX_SIZE = 100000;
    private int[] data;

    public void processData(int[] data) throws Exception {
        if (data.length > MAX_SIZE) {
            throw new Exception("Data size exceeds maximum limit");
        }
        this.data = data;
        // 데이터 처리 로직
    }
    
    public int getDataLength() {
        return data.length;
    }
}

따라서 아래의 코드로 수정을 해야한다.

public class DataProcessor {
    private int maxSize;
    private int[] data;

    public DataProcessor(int maxSize) {
        this.maxSize = maxSize;
    }

    public void processData(int[] data) throws Exception {
        if (data.length > maxSize) {
            throw new Exception("Data size exceeds maximum limit");
        }
        this.data = data;
        // 데이터 처리 로직
    }

    public int getDataLength() {
        return data.length;
    }
}
반응형

'개발 > Java' 카테고리의 다른 글

Heap 메모리의 청소부 - Garbage Collection  (0) 2024.06.09
Thread Safety하게 만들자  (0) 2024.06.09
JVM 메모리 구조  (0) 2024.06.08
자바 어떻게 실행되는가?  (0) 2024.06.02
[실무 역량 과제] 신규 유형 (BE)  (0) 2024.05.30