먼저 static 이라는 용어를 해석해보면 말그대로 정적 혹은 고정된이란 의미를 가지고 있다.
자바에서는 static 키워드를 사용하여 static 변수와 static 메소드를 생성할 수 있고, 이 둘을 합쳐 정적 멤버라고 한다.
다른 의미로 static 키워드를 가지고 있는 변수, 메소드는 객체(new 키워드로 인스턴스화 된 클래스)에 소속된 멤버가 아니라 클래스 자체에 소속된 멤버이다.
그렇다면 static 키워드가 멤버들은 어떤 특징을 가지고 있을까?
static 키워드가 붙지 않는 변수나 메소드의 경우 객체가 생성될 때마다 호출되기 때문에 서로 다른 주소 값을 가지고 있다. 그렇기 떄문에 각 객체들에서 공통적으로 관리해야하는 경우는 static 키워드를 사용하는 것이 유용하다.
위에서 설명했듯이 static 멤버는 객체에 소속된 멤버가 아닌 클래스 자체에 소속된 멤버이기 때문에 new 키워드 없이 해당 멤버를 사용할 수 있다.
public class StaticTest {
public void test() {
System.out.println("인스턴스 메소드");
}
public static void main(String[] args) {
test();
}
}
안타깝게도, 위 코드는 아래의 이미지와 같이 컴파일 에러가 발생한다. 그 이유는 print 메소드가 static 메소드가 아니기 때문에 컴파일 조차 되지 않는다는 것을 알 수 있다.
static 키워드를 붙히면 사용가능해진다.
public class StaticTest {
public static void test() {
System.out.println("정적 메소드");
}
public static void main(String[] args) {
test();
}
}
결과
정적 메소드
static 멤버는 프로그램 실행과 동시에 메모리에 올라가기 때문에 static 메소드 안에 인스턴스 변수를 사용할 수 없다. 당연한 이야기지만 인스턴스 변수를 사용하기 위해선 객체를 생성해야만 사용이 가능하기 때문에 객체를 생성하기 전에 먼저 메모리에 올리는 static 멤버 안에선 사용할 수 없는 것이다.
public class StaticTest {
static int staticNum = 0;
int nonStaticNum = 0;
public static void test() {
nonStaticNum;
System.out.println("정적 메소드");
}
public static void main(String[] args) {
test();
}
}
이렇듯 static 키워드를 붙이면 객체 생성 없이 메소드나 변수를 사용할 수 있다.
프로그램이 시작되면 클래스가 메모리에 올라가게 되는데, 이때 static이 붙은 변수나 메소드는 클래스와 함께 메모리의 영역 중 static 영역에 자동으로 생성된다.
자동으로 메모리에 올라가기 때문에 객체 생성 없이 사용이 가능한 것이고, main 메소드 또한 static으로 구현되어 있기 때문에 우리가 별도의 객체 생성 없이 바로 실행이 가능한 것이다.
위에서 설명한대로, 인스턴스를 생성할 경우 각각의 인스턴스는 서로 다른 메모리를 주소를 값을 참조하고 있기 때문에 독립적이다. 그렇기 때문에 static 키워드는 인스턴스들이 공통적으로 값을 유지해야할 때 사용한다.
즉, 프로그램 전역에서 사용되는 유일한 클래스를 만들 때 사용된다.
static 키워드를 가장 많이 사용하는 실제 예시를 찾아보면 싱글턴 패턴과 정적 클래스가 있다.
먼저 싱글턴 패턴에 대해 간략히 설명하자면
싱글턴 패턴은 객체 인스턴스가 오로지 한 개만 생성 되도록 설계하는 패턴이다. 말 그대로 프로그램 내에서 인스턴스가 유일해야 하고, 대표적으로 로그를 기록하거나 캐싱할 때 사용된다. 그래서 프로그램 전역적으로 사용되면서 동시에 유일하게 존재할 때 사용할 수 있다.
public class Common {
private static Common common;
private Common() {
}
public static Common getInstance() {
if (common == null) {
common = new Common();
}
return common;
}
}
프로그램이 동작하는 동안 Common 인스턴스가 유일한지 테스트 해보면
public class App {
public static void main(String[] args) {
Common common1 = Common.getInstance();
Common common2 = Common.getInstance();
System.out.println(common1);
System.out.println(common2);
System.out.println(common1 == common2);
}
}
결과
me.jeongseok.javaplayground.Common@5e91993f
me.jeongseok.javaplayground.Common@5e91993f
true
같은 메모리 주소를 참조하고 있으며, == 연산자를 통해 비교를 해봐도 true 값을 반환하게 된다.
하지만 이 코드는 멀티쓰레드 환경에서 문제가 발생할 수 있다.
한마디로 은행에 번호표를 뽑지 않은 상태에서 여러 사람들이 하나의 은행 창구에 갔을 때 문제가 발생한다는 뜻이다.
은행에서는 이러한 문제를 해결하기 위해 번호표라는 것을 뽑게 되고 먼저 온 사람이 용무를 볼 수 있게 만들어놨다.
싱글턴 패턴에서 멀티쓰레드 문제를 해결하기 위해선 현실세계의 번호표와 비슷한 역할을 하는 기능이 필요하다. 바로 그 역할을 해주는 것이 synchronized 키워드이다.
public class Common {
private static Common common;
private Common() {
}
public static synchronized Common getInstance() {
if (common == null) {
common = new Common();
}
return common;
}
}
synchronized 사용한다고 한들, 모든 동기화 문제가 해결되진 않는다. 추가적인 문제들이 하나씩 발생하는데 이에 대한 내용은 추가적으로 찾아서 업로드할 예정이다.
자바에선 명시적으로 정적 클래스라는 것을 제공하지 않고, 편의상 정적 메소드만 가지는 클래스를 정적 클래스라고 부른다.
싱글턴 패턴과 동일한 점은 프로그램내에서 전역적으로 사용되고, 인스턴스를 생성하지 않기 때문에 유일성이 보장된다는 것이다. 다만 차이점으로는 인스턴스를 생성할 수 없기 때문에 클래스 메소드를 이용한다는 것이다.
public class StaticClass {
private StaticClass() {
}
public static void print() {
System.out.println("정적 클래스입니다.");
}
}
싱글턴 패턴과 정적 클래스를 공부하다보면 불연듯 이러한 질문을 스스로에게 던지거나, 다른 사람에게 받을 수 있는 내용이다. 정답을 내리기전 이 둘의 차이점을 먼저 정리해보면 아래의 표와 같다.
차이점 | 싱글턴 패턴 | 정적 클래스 |
---|---|---|
원리 | 하나의 인스턴스를 생성하여 재사용 | 인스턴스 생성 X |
인터페이스 구현 | 가능 | 불가능 |
Override | 가능 | 불가능 |
메모리 Load 시점 | 필요에 따라 Lazy Loading 가능 | static binding으로 빠르게 로딩 |
OOP | O | X |
결론은 프로젝트 혹은 내가 만드는 프로그램에 상황에 따라 다른 것 같다. 단지 누군가가 왜 싱글턴 패턴으로 사용하였나요? or 왜 정적 클래스를 사용했나요? 라고 물어볼 때, 이 둘의 장/단점이 무엇인지 빠르게 파악하고 어떤 이유 싱글턴 패턴 or 정적 클래스를 사용했는지를 명확하게 설명해주면 좋을 것 같다.