Static이라는 키워드는 보통 변수나 메소드 앞에서 사용한다.

non-static 멤버와 static 멤버의 차이

1
2
3
4
5
6
7
class StaticSample{
int n; // non-static 필드
void g(){...} // non-static 메소드

static int m; // static 필드
static void f() {...} // static 메소드
}

non-static 멤버

  • 공간적 특성 : 멤버는 객체마다 별도로 존재한다.
    • 인스턴스 멤버라고 부른다.
  • 시간적 특성 : 객체 생성 시에 멤버가 생성된다.
    • 객체가 생성될 때 멤버도 생성된다.
    • 객체 생성 후 멤버 사용이 가능하다.
    • 객체가 사라지면 멤버도 사라진다.
  • 공유의 특성 : 공유되지 않는다.
    • 멤버는 객체 내에 각각의 고유한 공간을 유지한다.

static 멤버

  • 공간적 특성 : 멤버는 클래스당 하나가 생성된다.
    • 멤버는 객체 내부가 아닌 별도의 공간에 생성된다.
    • 클래스 멤버라고 부른다.
  • 시간적 특성 : 클래스 로딩 시에 멤버가 생성된다.
    • 객체가 생기기 전에 이미 생성된다.
    • 객체가 생기기 전에도 사용이 가능하다.(즉, 객체를 생성하지 않고도 static 멤버를 사용할 수 있다.)
    • 객체가 사라져도 static 멤버는 사라지지 않는다.
    • 멤버는 프로그램이 종료될 때 사라진다.
  • 공유의 특성 : 동일한 클래스의 모든 객체들에 의해 공유된다.

Static 변수

1
2
3
4
5
6
7
public class HouseLee{
String lastName = "이";
public static void main(String[] args) {
HousePark pey = new HouseLee();
HousePark pes = new HouseLee();
}
}

위와 같이 이씨 집안을 나타내는 클래스가 있다. 위의 클래스는 객체를 생성할 때 마다 객체 변수인 lastName을 저장하기 위한 메모리를 별도로 할당한다. 하지만, 이 경우 HouseLee 클래스의 lastName은 어떤 객체이던지 동일한 값인 "이"이어야 할 것이다. 이렇게 항상 값이 변하지 않는 경우라면 static을 사용하여 메모리의 낭비를 줄일 수 있는 이점을 가지고 있다.

다음은 static 변수를 이용해서 위의 코드를 바꾼 것이다.

1
2
3
4
5
6
7
public class HouseLee{
static String lastName = "이";
public static void main(String[] args) {
HousePark pey = new HouseLee();
HousePark pes = new HouseLee();
}
}

위와 같이 lastName 변수에 static 키워드를 붙이면 자바는 메모리 할당을 딱 한번만 하게 되어 메모리 사용에 이점을 가지게 된다.

만약 HouseLee 클래스의 lastName 값이 변경되지 않기를 바란다면 final 키워드를 사용하면 된다. final 키워드는 한 번 값이 설정되면 그 값을 변경하지 못하게 한다. (상수처럼) 이를 변경하려고 하면 예외가 발생한다.

static을 사용하는 또 한가지 이유로 공유의 개념을 들 수 있다. static으로 선언된 변수는 같은 클래스의 객체들이 하나의 static 변수를 두고 공유한다.

즉, static으로 설정하면 같은 곳의 메모리 주소만을 바라보기 때문에 static 변수의 값을 공유하게 되는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Counter  {
int count = 0;
Counter() {
this.count++;
System.out.println(this.count);
}

public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
}
}
// 결과
1
1

위의 코드를 살펴보자. c1,c2 객체 생성 시 count 값을 1씩 증가시키더라도 c1과 c2의 count는 각각 별도의 메모리 공간을 가리키고 있기 때문에 2가 아닌 1 1이 나오게 되는 것이다.

그렇다면 static 변수를 이용해서 이를 해결해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Counter  {
static int count = 0;
Counter() {
this.count++;
System.out.println(this.count);
}

public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
}
}
// 결과
1
2

count라는 변수는 static으로 선언되어 객체들이 static 변수를 공유하게 된다. 따라서 증가된 결과값이 나오게 된다.

보통 변수의 static 키워드는 프로그래밍 시 메모리의 효율보다는 두번째처럼 공유하기 위한 용도로 훨씬 많이 사용하게 된다.

static method

static이라는 키워드가 메소드 앞에 붙으면 이 메소드는 static 메소드가 된다. 그럼 아래의 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Counter  {
static int count = 0;
Counter() {
this.count++;
}

public static int getCount() {
return count;
}

public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();

System.out.println(Counter.getCount());
}
}

main 메소드에서 getCount() 메소드는 Counter.getCount() 와 같이 인스턴스를 만들지 않고 클래스를 통해 호출할 수 있다.(접근할 수 있다.)

getCount() 메소드는 static 메소드이고 이와 같은 static 멤버는 클래스당 하나만 존재하기 때문에 클래스 이름으로 바로 접근할 수 있다.
클래스명.static멤버

주의해야 할 점은 non-static 멤버는 클래스명.non-static멤버 방식으로 접근할 수 없다.

static 메소드 안에서는 인스턴스 변수 접근이 불가능하다. 위의 코드는 static 변수이기 때문에 static 메소드 안에서 접근이 가능한 것이다.

보통 static method는 유틸리티성 메소드를 작성할 때 많이 사용된다고 한다. 예를 들어, 오늘의 날짜 구하기, 숫자에 콤마 추가하기 등의 메소드를 작성할 때에는 클래스 메소드를 사용하는 것이 유리하다.

또한, static 메소드는 오직 static 멤버만 접근할 수 있다.
static 메소드는 객체가 생성되지 않은 상황에서도 사용이 가능하므로 객체에 속한 인스턴스 메소드, 인스턴스 변수 등을 사용할 수 없다.

  • static 멤버들만 사용이 가능하다.
  • 그러나 인스턴스 메소드는 static 멤버들을 모두 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
class StaticMethod {
int n;
void f1(int x) { n = x; } // 정상
void f2(int x) { n = x; } // 정상

static int m;
static void s1(int x) { n = x; } // 컴파일 오류. static 메서드는 non-static 필드 사용 불가
static void s2(int x) { f1(3); } // 컴파일 오류. static 메서드는 non-static 메서드 사용 불가

static void s3(int x) { m = x; } // 정상. static 메서드는 static 필드 사용 가능
static void s4(int x) { s3(3); } // 정상. static 메서드는 static 메서드 호출 가능
}

마지막으로 static 메소드에서는 this 키워드를 사용할 수 없다.

  • this는 호출 당시 실행 중인 객체를 가리키는 레퍼런스이다.
  • 따라서 객체가 생성되지 않은 상황에서 클래스 이름을 이용하여 호출이 가능한 static 메소드는 this를 사용할 수 없다.
1
2
3
4
5
6
7
8
9
class StaticAndThis{
int n;
static int m;
void f1(int x){ this.n = x;} // 정상
void f2(int x){ this.m = x;} // non-static 메소드에서 static 멤버 접근 가능

static void s1(int x){ this.n = x;}
// 컴파일 오류. static 메소드에서는 this 사용 불가.
}

주의

  • 실제 static 멤버의 생성 시점은 JVM(자바 가상 기계)에 따라 다르다.
  • 그러나 일반적으로 static 멤버가 포함된 클래스가 로딩하는 시점에 static 멤버가 생성된다고 볼 수 있다.
  • JVM은 많은 경우 처음부터 필요한 대부분의 클래스를 로딩하기 때문에 static 멤버의 생성 시점은 JVM이 시작되는 시점이라고 할 수 있다.

싱글톤 패턴

디자인 패턴 중 하나인 싱글톤에 대해서 알아보자. 우리는 지금 static이라는 키워드에 대해서 알고 있기 때문에 어렵지 않게 접근할 수 있다.

Singleton은 단 하나의 객체만을 생성하게 강제하는 패턴이다. 즉 클래스를 통해 생성할 수 있는 객체는 Only One. 즉, 한 개만 되도록 만드는 것이 싱글톤이다.

다음의 예를 보자.

1
2
3
4
5
6
7
8
9
10
class Singleton {
private Singleton() {
}
}

public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = new Singleton();
}
}

위와 같은 코드를 작성하면 컴파일 에러가 발생한다. 왜냐하면 Singleton 클래스의 생성자에 private 키워드로 외부 클래스에서 Singleton 클래스의 생성자로의 접근을 막았기 때문이다. 이렇게 생성자를 private으로 만들어 버리면 외부 클래스에서 Singleton 클래스를 new를 이용하여 생성할 수 없게 된다.

new를 이용하여 무수히 많은 객체를 생성한다면 싱글톤의 정의에 어긋난다. 그래서 일단 new로 객체를 생성할 수 없도록 private 접근 제어자를 이용해서 막은 것이다.

다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
private Singleton() {
}

public static Singleton getInstance() {
return new Singleton();
}
}

public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
}
}

위의 코드는 이제 getInstance라는 static 메소드를 이용하여 Singleton 객체를 돌려받을 수 있다. 하지만 getInstance를 호출할 때마다 새로운 객체가 생성되게 된다. 하지만 그렇다면 싱글톤 패턴이 아니다. 어떻게 해야할까??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton {
private static Singleton one;
private Singleton() {
}

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

public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
// 결과
true

Singleton 클래스에 one이라는 static 변수를 두고 getInstance 메소드에서 one 값이 null인 경우에만 객체를 생성하도록 하여 one 객체가 단 한번만 만들어지도록 했다.

getInstance의 동작원리

최초 getInstance가 호출되면 one이 null이므로 new에 의해서 객체가 생성이 된다. 이렇게 한번 생성이 되면 one은 static 변수이기 때문에 모든 객체가 공유한다. 따라서 객체가 생성된 이후로는 null이 아니게 된다. 그런 후에 다시 getInstance 메소드가 호출되면 이제 one은 null이 아니므로 이미 만들어진 싱글톤 객체인 one을 항상 리턴하게 된다.

main 메소드에서 getInstance를 두번 호출하여 각각 얻은 객체가 같은 객체인지 확인해보았다. 결과는 역시 "true"가 나온다.

싱글톤 패턴은 static에 대한 이해를 하고 있다면 어렵지 않은 패턴 중 하나이다.

위의 예제로 든 싱글톤은 Thread Safe 하지는 않다. 쓰레드 환경에서 안전한 싱글톤을 만드는 방법은 추후에 공부해보자.

참고