우리네 장
[ JAVA ] 적절한 Static 사용에 대해 고민해보자..! 본문
습관적으로 사용하던 static final을 왜 쓰는지 적절한 static practice에 대해 알아보자!
여러 자료를 바탕으로 작성한 글이기에 이의 제기는 아주 좋습니다
Static 변수
정의에 대해서 먼저 말해보자면,
임의의 클래스 A의 인스턴스들이 공유하여 접근하는 공유자원 이라고 말할 수 있다.
즉 그 인스턴스 들이 해당 변수에 대하여 모두 같은 주소 값을 가지는 것을 의미한다.
static 변수는 인스턴스 변수와 달리 인스턴스에 속하지 않고, 클래스에 더 가까운 변수이다.
그래서 클래스가 load 될 때 메모리를 할당 받으며 초기화 된다.
즉, 클래스의 생성자 실행 여부와 상관 없이 위 과정이 진행된다는 의미이다.
그럼 제목처럼 static 변수를 사용하기에 적절한 방법은 무엇일까...?
언제 우리가 static 변수를 사용하고자 할 지 생각해보자.
1 ) 임의의 클래스의 인스턴스들이 [ 특정 조건에 의해 ] 동일한 메모리의 데이터를 조작해야 하는 경우.
예를 들어, 임의의 클래스 객체를 만들 때마다 카운트 값을 올린다던가 하는 경우가 되겠다.
2 ) 임의의 클래스의 인스턴스들이 공통된 데이터 값을 공유하는 경우.
1 )의 경우,
비동기 환경에서 아무런 처리 없이 사용한다면 아주 문제가 생긴다.
클래스 변수는 메소드 영역에 자리하므로 여러 스레드들이 접근 할 수 있고, 동시에 여러 스레드가 해당 값을 조작하는 경우 race condition이 발생하기 딱 좋다.
그럼 사용자는 기대와 다른 결과 값을 받게 되기도 한다.
그럼 해당 상황의 경우, 데이터를 전역 변수가 아닌 DB에 저장하면 되지 않나??? 라고 생각을 하겠지만..
예를 들어보자.
티켓 예매 시스템을 생각해 볼 때, 예매가 이루어지기 전 시스템은 티켓 정보를 조회하여 1) 예매자가 있는지 조회한 후
예매자가 없으면 임의의 2) 사용자의 예매를 처리해 준다.
그런데 만약 두 개의 요청이 동시에 1)을 처리한다고 생각해보자.
두 요청의 조회 결과는 모두 예매자가 없다는 결과를 출력할 것이고, 이에 시스템은 두 사용자의 2) 을 모두 처리해 줄 것이다.
이러한 문제를 해결하기 위해 DBMS 단에서 여러 트랜잭션 동시성 제어 기법이 나오지만, 실제 적용이 어려운 경우도 있다.
나의 경우에는 1)과 2)을 Merge 문으로 처리한 경우 위와 같은 예외가 사용자가 많을 경우 한 달에 한 두번 꼴로 발생하였으나, select문과 insert문으로 나누어서 처리한 경우 그 수가 굉장히 많이 줄었다.
쨌든 이러한 데이터 동기화 문제를 확실히 해결하기 위해서는, java 코드 단에서 Lock을 사용하는 방법밖에는 없다는 생각이 든다...
Lock의 사용은 어쩔 수 없이 성능 상에 부담을 가져온다.
그러므로 임계 영역을 최소화 해야 하고, DB에 접근하는 부분이 있을 경우 대상 테이블에 index를 적절하게 생성해 줘야 하며, row의 수를 백업등을 이용해 줄여주는 것이 좋다.
1 ) 의 경우 + Spring을 생각해볼까?
왜 스프링에서 Bean에 상태 값을 갖는 값을 사용하지 말라고 할까??
Spring에서 singletone scope 의 class 변수는 static 변수와 의미적으로 일맥상통한다.
즉 prototype scope으로 사용자 호출 마다 다른 상태 값을 갖도록 하는 것이 아니라면, 비동기에 안전하지 않다는 것이다.
또한 Spring에서는 static 변수 사용을 지양하도록 되어있는데,
Spring은 대상 차원의 의존성 주입을 바탕으로 한다.
그러나 static은 기본적으로 인스턴스를 만들지 않아도 되고, 이는 컨테이너에게 생명 주기를 넘기는 것이 아니기 때문이다.
이번에는 2 ) 의 경우를 보자.
2)은 static final과 연관이 있다.
final에 대해 잠깐 확인하자면,
변수에 붙은 경우 해당 변수는 초기화 이후 다른 값 할당이 불가능하며,
메소드에 붙은 경우는 override가 불가능하고,
클래스에 붙은 경우는 상속이 불가능하다.
즉, 한 번 정의된 값에 대해 수정이 불가능하여 변수 상수라고 말하기도 한다.
실제로 메모리 상에서 static final vs static 같은 메소드 영역에 할당 되지만 내부적으로 그 위치가 다르다.
static final : class의 Meta data가 저장되는 constant pool 에 저장이 되지만,
static : Meta data와는 별개로 method area 에 저장된다. ( ~ java7 ) [!]
그럼 클래스의 인스턴스들이 모두 공유하여 사용하는 데이터 값을 static final로 선언하면 좋은 점이 무엇일까??
- 인스턴스가 아무리 많이 생성이 되어도 공유자원의 메모리는 한 번만 할당되어 메모리가 낭비되지 않는다
- final은 목적 자체가 읽기에 목적이 있어 비동기 환경에서도 안전하다
- final의 메모리 영역이 Method area이기 때문에 GC의 대상에서 제외되어, 한 번 메모리에 올리면 사라지지 않는다.
- 인스턴스 생성을 통해 접근하는 것이 아니므로, 변수 접근 시 속도면에서 더 유리하다.
그래서 데이터 변경이 일어나지 않을 것으로 예상되는 공유 데이터에는 static final을 붙이면 좋다.
[!] 에 대해서 살펴보자.
기존 자바 7까지 static 변수는 Method 영역에 생성되어 GC의 대상이 아니었다.
이때 static의 단점은 무지성으로 static 변수를 많이 생성하거나, static으로 Collection 객체를 생성한 경우 메모리가 정리가 되지 않아 heap 메모리가 고갈되어 OOM 이 발생하기에 좋다.
그래서인지, java 8에서 부터는 Object Static variable에 대해 heap 영역에 메모리를 할당하여 GC 대상이 되도록 변경되었다. ( static final은 class의 Meta data이므로 여전히 Native area에 저장되어 GC와는 상관 없다. )
근데 이게 문서에서 표현을 애매하게 정의 해서 좀 혼돈이 온다. 모든 static 변수에 대해 힙 영역에 저장하는 것인지 아니면 참조 변수 타입의 static 변수에 대해서만 힙 영역에 저장하는 것인지 헷갈린다..
아래 문서를 더 잘 이해한 사람이 있다면 알려주길 바란다 ㅠ
( https://openjdk.org/jeps/122 )
그래서 결론?
static 변수는 감당 가능할 때 사용하고, static final 변수 상수는 야무지게 사용하자~!
Static 메소드
static 메소드는 보통 util 클래스 같은 곳에서 많이 사용한다.
static 메소드는 언제 사용할까??
- 메소드 내부 로직을 파라미터 인자로만 처리가 가능할 때
- 메소드 내부 로직에서 인스턴스 변수를 사용하여 처리하지 않을 때
그럼 굳이 static 메소드를 사용하는 이유는 무엇인가?
- static 변수 사용과 상통한다. 인스턴스를 거쳐서 호출하지 않기 때문에 함수 호출 시 속도면에서 더 유리하다.
주의 사항은 클래스 메소드에서 인스턴스 변수나 메소드를 호출하기 위해서는
클래스 메소드 내부에서 해당 클래스의 인스턴스를 생성한 뒤, get으로 값을 획득하거나 public 메소드를 호출해야 한다는 점이 있다. 대충 아래와 같은 코드가 생기게 된다.
// static 메소드에서 사용 시
public static void a () {
A aaa = new A();
int stA = aaa.getStA();
aaa.instMethod();
....
}
// static 변수에 할당 시
private static int stA = new A().getStA();
사실 이 경우는 클래스 메소드로 빼기 적절하지 않은 상황이다.
그러나 역으로, 인스턴스 변수와 메소드는 마음대로 클래스 변수의 값을 대입할 수 있고, 클래스 메소드를 호출할 수 있다.
왜냐하면 인스턴스 변수와 메소드가 사용이 가능한 시점 ( 후 ) 에 클래스 변수와 메소드는 무조건 메모리 할당이 되어있기 때문이다. ( 선 )
[ 메모리 할당 및 초기화 시점 ]
- 클래스 변수 : 클래스가 load 될 때 method 영역에 할당
- 인스턴스 변수 : 클래스의 객체가 생성될 시점에 heap 영역에 할당
- 지역 변수 : 해당 함수가 호출될 때, stack 영역에 생성 및 함수 호출 종료 시 소멸
- final static 변수 : 클래스가 load 될 때 constant pool 영역에 할당
- 추가적으로 String!
- String 생성 및 초기화 시 2가지 방법이 있다.
- 1. String s1 = new String("s1");
- 위 경우, heap 메모리에 영역이 할당되어 초기화 된다.
- 2. String s2 = "s2";
- 위 경우, 내부적으로 String.intern() 메소드를 호출하게 되고 이때 String constant pool을 뒤져서 초기화 값과 동일한 literal이 있다면 그 주소값을 return하고 없다면 pool에 새로 메모리를 할당한 후 그 주소값을 return한다. 즉 이때는 method 영역에 메모리를 할당하게 된다.
[ Static 메소드가 오버라이드가 안돼는 이유 ]
static의 의미를 보자. 정적이라는 의미이다. 이는 변경이 불가능하다는 의미이므로 override로 재정의하는 것과 의미 부터가 맞지 않는다.
클래스 메소드와 인스턴스 메소드는 메모리에 올라가는 시점이 다르다.
클래스 메소드 )
컴파일 시점에 해당 메소드가 메모리에 올라간다.
그래서 컴파일러 단계에서 부터 어떤 메소드를 실행할지를 결정하게 되며,
메소드가 호출되어 JVM이 실행할 때, 객체가 아닌 컴파일 시점에 정의된 클래스 타입을 확인하고서 해당 클래스의 클래스 메소드를 호출한다.
인스턴스 메소드 )
런타임 시점에 해당 메소드가 메모리에 올라간다.
그래서 런타임 시에 override 된 메소드 중 어떤 메소드가 실행 될 지가 결정된다.
메소드가 호출되어 JVM이 실행할 때, 해당 메소드를 구현하고 있는 실제 객체를 찾아 해당 객체의 인스턴스 메소드를 호출한다.
( 출처 : https://hsik0225.github.io/java/2020/12/17/Static-Override/ )
'JAVA' 카테고리의 다른 글
ForkJoinPool 좀 보기! (1) | 2024.05.02 |
---|---|
[ JAVA ] byteCode 조작 - ByteBuddy! (2) | 2022.11.30 |