static 키워드 (static 멤버 / 메모리 할당 순서 / 싱글톤 디자인 패턴)
목차
1-3. static 멤버와 인스턴스 멤버의 메모리 할당 순서
📍 JAVA 프로그램 실행 과정
0. 소스 코드 작성 및 컴파일 후 클래스 실행
→ .java파일을 만들어서 javac라는 명령어로 컴파일
1. 클래스 로딩
→ static변수 및 메서드가 먼저 메모리에 로딩됨.
2. main() 메서드 호출 (실행)
3. 인스턴스 생성
→ 인스턴스 변수(= field = 멤버변수) 및 메서드가 메모리에 로딩됨.
4. 참조변수를 통해 인스턴스 메서드 호출 (실행)
→ 로컬 변수가 메모리에 로딩됨.
5. 결과 출력
6. 프로그램 종료
📍 static 키워드
– 변수, 메서드, 클래스의 제한자로 사용.
– 변수 또는 메서드에 static 키워드를 사용할 경우,
인스턴스 생성과 상관없이 클래스가 로딩되는 시점에 메모리에 로딩됨.
(클래스가 로딩되는 Method Area에 함께 로딩됨.)
→ 따라서 참조변수 없이 클래스명만으로 멤버에 접근 가능함. 즉, 인스턴스를 생성하지 않아도 접근이 가능한 멤버.
ex ) static이 없으면 s.sister_1() 형태로 인스턴스 생성 후 메서드 호출.
static이 있으면 sister_1() 형태로 바로 메서드 호출.
– Java에서 static 키워드를 사용한다는 것은 메모리에 한번 할당되어 프로그램이 종료될 때 해제되는 것을 의미.
▶ static 변수
– 클래스명.멤버변수명 형태로 접근.
EX1 )
• 클래스 생성하기
class NormalMember{
int a = 10; // 인스턴스 변수(=멤버변수 =필드)
int b = 20; // 인스턴스 변수
}
class StaticMember{
static int a = 10; // static 멤버변수(=클래스 멤버변수 =정적 멤버변수)
int b = 20; // 인스턴스 멤버변수
}
• main 메서드에서 실행하기
1 ) NormalMember 클래스
// NomalMember 클래스의 인스턴스 생성
NormalMember n1 = new NormalMember();
NormalMember n2 = new NormalMember();
System.out.printf("n1.a : %d, n2.a : %d \n", n1.a, n2.a);
System.out.printf("n1.b : %d, n2.b : %d \n", n1.b, n2.b);
n1.a = 99; // n1 인스턴스의 인스턴스 멤버변수 a값을 99로 변경
n1.b = 999; // n1 인스턴스의 인스턴스 멤버변수 b 값을 999로 변경
System.out.printf("n1.a : %d, n2.a : %d \n", n1.a, n2.a);
System.out.printf("n1.b : %d, n2.b : %d \n", n1.b, n2.b);
– 인스턴스 멤버변수인 a와 b는 인스턴스마다 각각 생성되므로,
하나의 인스턴스에서 인스턴스 멤버변수 값을 바꾸더라도 다른 인스턴스 멤버변수에는 아무런 영향이 없음.
(생성된 메모리 공간이 다르기 때문.)
→ 따라서 n1 인스턴스의 인스턴스 멤버변수 값을 변경해도 n2의 인스턴스 멤버변수 값은 그대로임.
✓ 실행 결과
n1.a : 10, n2.a : 10
n1.b : 20, n2.b : 20
n1.a : 99, n2.a : 10
n1.b : 999, n2.b : 20
2 ) StaticMember 클래스
System.out.println("StaticMember.a : " + StaticMember.a);
– StaticMember 클래스의 인스턴스 생성 전에 static 멤버에 접근 가능.
– 인스턴스가 생성되기 전이지만 이미 메모리에 로딩되어 있으므로 클래스명만으로 static 변수에 접근할 수 있음.
✓ 실행 결과
StaticMember.a : 10
StaticMember s1 = new StaticMember();
StaticMember s2 = new StaticMember();
System.out.printf("s1.a : %d, s2.a : %d \n", s1.a, s2.a);
System.out.printf("s1.b : %d, s2.b : %d \n", s1.b, s2.b);
– 두 개의 인스턴스가 생성되며 인스턴스 멤버변수도 두 개씩 생성됨.
✓ 실행 결과
s1.a : 10, s2.a : 10
s1.b : 20, s2.b : 20
s1.a = 99; // static 멤버변수 값을 변경
s1.b = 999; // 인스턴스 멤버변수 값을 변경
System.out.printf("s1.a : %d, s2.a : %d \n", s1.a, s2.a);
System.out.printf("s1.b : %d, s2.b : %d \n", s1.b, s2.b);
– static 멤버변수 값(a)을 s1 인스턴스에서 변경하게 되면,
s2 인스턴스도 동일한 변수 값을 공유하므로 변경된 값이 두 인스턴스 모두 적용(공유)됨.
→ 즉, 하나의 인스턴스에서 값을 변경하면 모든 인스턴스가 영향을 받음.
✓ 실행 결과
s1.a : 99, s2.a : 99
s1.b : 999, s2.b : 20
s2.a = 1000;
System.out.printf("s1.a : %d, s2.a : %d \n", s1.a, s2.a);
– s2.a로 Heap영역에서 Method영역으로 가는 방법 (인스턴스 생성으로 참조변수에서 접근하는 방법)보다는
StaticMember.a로 한 번에 Method영역으로 가는 방법을 추천하므로 경고 (노란 줄) 발생.
→ 틀린건 아니지만 클래스명.변수명 (또는 메서드명) 이 형식을 추천함.
– StaticMember 클래스의 멤버 a는 static 키워드가 붙었으므로 하나의 인스턴스 값만 변경해도 모든 인스턴스 값이 바뀜.
✓ 실행 결과
s1.a : 1000, s2.a : 1000
StaticMember.a = 500;
// StaticMember.b = 500;
System.out.printf("s1.a : %d, s2.a : %d \n", s1.a, s2.a);
– static 멤버는 참조변수명 대신 클래스명만으로 접근 가능.
– static 멤버가 아닌 일반 멤버는 클래스명만으로 접근 불가, 컴파일 에러 발생.
✓ 실행 결과
s1.a : 500, s2.a : 500
– Static 멤버변수의 대표적인 예
→ java.lang.Math 클래스의 PI 변수 (이미 만들어져있음)
– 자동완성 화면에서 초록색은 메서드를 뜻하고, s는 static 키워드가 붙었다는 것을 의미함.
→ 따라서 Math.PI는 new 키워드로 인스턴스를 생성하지 않고도 바로 클래스명.변수명으로 접근 가능.
✓ 실행 결과
PI 값 : 3.141592653589793
▶ static 메서드
– 정적 메서드.
– 메서드 정의 시 리턴타입 앞에 static키워드를 붙여서 정의.
– 클래스가 메모리에 로딩될 때 static 변수와 함께 메모리에 로딩되므로 인스턴스 생성과는 무관함.
– 클래스명만으로 접근 가능
→ 클래스명.메서드명( ) 형태로 호출.
▸ static 메서드 정의 시 주의 사항
– 인스턴스 생성 시점에서 생성되는 것은 static 메서드 내에서 접근 불가.
→ static 멤버가 먼저 생성 된 후 인스턴스가 생성되므로, 인스턴스 생성 시점에서 생성되는 것은 실행 불가.
1. 인스턴스 변수 사용 불가
• 원인
– static 메서드가 로딩되는 시점은 클래스가 로딩되는 시점이며,
인스턴스 변수는 new 키워드를 통해서 인스턴스 생성 시점에 로딩되므로,
static 메서드가 로딩되는 시점에서는 존재하지 않음.
2. 레퍼런스 this 또는 super 사용 불가
• 원인
– static 메서드가 로딩되는 시점은 클래스가 로딩되는 시점이며 레퍼런스 this는 인스턴스 생성 시점에 생성되므로,
static 메서드가 로딩되는 시점에서는 존재하지 않음.
• 해결책
– this.xxx 또는 super.xxx 대신 클래스명.xxx 형식으로 접근.
3. 메서드 오버라이딩 불가
– 이유는 상속 단원에서 자세히 배움.
EX )
• 클래스 생성하기
class StaticMethod {
private int normalVar = 10;
private static int staticVar = 20;
– 인스턴스 멤버변수 normalVar 선언, 10으로 초기화.
– static (정적) 멤버변수 staticVar 선언, 20으로 초기화.
→ 둘 다 접근제한자는 private로 설정함.
• 일반 메서드 정의하기
// 일반 메서드 normalMethod() 정의
public void normalMethod() {
System.out.println("인스턴스 변수에 접근 : " + normalVar); // 가능
System.out.println("static 변수에 접근 : " + staticVar); // 가능
// static 메서드 호출
staticMethod(); // 가능
}
– 일반 메서드는 인스턴스가 생성되는 시점에 메모리에 로딩됨.
→ 따라서 일반 메서드에서 인스턴스 변수 & static 변수 모두 접근 가능.
→ 바로 밑에 정의한 static 메서드도 호출 가능.
• static 메서드 정의하기
// static 메서드 staticMethod() 정의
public static void staticMethod() {
System.out.println("정적 메서드 staticMethod()");
// System.out.println("인스턴스 변수에 접근 : " + normarVar); // 불가
System.out.println("static 변수에 접근 : " + staticVar); // 가능
– static 변수에는 접근 가능하나 인스턴스 변수에는 접근 불가.
→ static 메서드는 클래스가 메모리에 로딩되는 시점에 함께 로딩되는데,
인스턴스 변수는 아직 메모리에 로딩되기 전이므로 접근 불가능.
// normalMethod();
}
– static 메서드에서 일반 메서드 호출 불가.
→ 변수와 원인 동일함. static 메서드 생성 시점에 일반 메서드는 생성되지 않았음.
→ 미리 만들어져있지 않아서 new 키워드와 함께 사용해야 함.
• set 메서드 정의하기
// 인스턴스 변수의 set 메서드 정의
public void setNormalVar(int normalVar) {
this.normalVar = normalVar;
}
– set 메서드정의 시 this키워드를 사용하므로 set 메서드만 정의해봄.
– 파라미터로 전달받은 데이터를 클래스의 멤버변수에 저장하는 코드가 문제없이 실행됨.
// static 변수의 set 메서드 정의
public static void setStaticVar(int staticVar) {
// this.staticVar = staticVar;
StaticMethod.staticVar = staticVar;
}
}
– static 메서드 내에서는 레퍼런스 this 사용 불가.
→ 레퍼런스 this는 인스턴스 생성 시점에 생성되지만,
static 메서드가 로딩되는 시점에는 레퍼런스 this가 존재하지 않으므로 에러 발생.
– 레퍼런스 this 대신 클래스명으로 static 변수 접근 가능함.
▶ static 멤버와 인스턴스 멤버의 메모리 할당 순서
– 아래의 예제를 통해 각 멤버의 메모리 할당 순서를 알아보겠음.
EX )
public class Ex3 {
int b = check("인스턴스 변수 b");
static int a = check("static 변수 a");
// check 메서드
public static int check(String str) {
System.out.println("check() 호출 :" + str);
return 0;
}
// main 메서드
public static void main(String[] args) {
System.out.println("main() 메서드 호출");
Ex3 ex = new Ex3();
System.out.println("Ex3 인스턴스 생성 완료");
}
static int c = check("static 변수 c");
} // Class Ex3
0. Ex3 클래스가 메모리에 로딩됨.
1. static 키워드가 선언된 모든 멤버가 메모리에 로딩됨.
→ static int a / static check( ) / main( ) / static int c
2. static 멤버가 메모리에 로딩될 때, static 변수 a와 c에 check( ) 메서드 리턴값이 전달되어야 하므로
static 메서드인 check( ) 메서드 호출됨.
2-1. "static 변수 a" 출력 (static 변수 a 로딩됨)
2-2. "static 변수 c" 출력 (static 변수 c 로딩됨)
3. main() 메서드 자동으로 호출됨.
→ "main( ) 메서드 호출" 출력
4. main( ) 메서드 내에서 Ex3 인스턴스 생성됨.
5. Ex3 인스턴스 생성 시 인스턴스 변수 b가 메모리에 로딩되며,
인스턴스 변수 b가 로딩될 때, 이미 method area 메모리 영역에 등록되어 있는 check() 메서드가 호출됨.
→ "인스턴스 변수 b" 출력
6. Ex3 인스턴스 생성 후 main() 메서드의 다른 코드 실행.
→ "Ex3 인스턴스 생성 완료" 출력
✓ 실행 결과
check() 호출 :static 변수 a
check() 호출 :static 변수 c
main() 메서드 호출
check() 호출 :인스턴스 변수 b
Ex3 인스턴스 생성 완료
📍 싱글톤 디자인 패턴 (Singleton Design Pattern)
– 단 하나의 '유일한 인스턴스' 만을 생성하여 공유하도록 하는 기법.
– 외부에서 인스턴스 생성을 하지 못하도록 차단하고, 클래스 내에서 직접 인스턴스를 생성 후 외부로 리턴하는 기법.
– 누구나 단 하나의 인스턴스만을 공유하여 사용해야 함.
→ 즉, 외부에서 new 키워드를 통해 새 인스턴스 생성 불가능.
– 외부에서 함부로 접근하지 못하게 하는 역할은 private 접근제한자
외부에서 인스턴스 생성 없이 접근할 수 있게 하는 역할은 static 키워드
▸ 패턴 규칙
1. 외부에서 인스턴스 생성이 불가능하도록 생성자에 private 접근제한자를 선언하여 정의.
2. 자신의 클래스에서 직접 인스턴스를 생성하여 참조변수에 저장.
→ 외부에서 인스턴스 생성없이도 접근 가능하도록 static 키워드 사용.
→ 외부에서 함부로 접근하지 못하도록 private 접근제한자를 선언함.
3. 생성된 인스턴스를 외부로 리턴하는 Getter 메서드 정의
→ private 접근제한자로 선언된 멤버변수에 접근하여, 생성된 인스턴스를 리턴하는 용도로 정의.
→ 인스턴스 생성없이 호출해야하므로 static 키워드 사용.
EX ) |
• 클래스 생성하기
class SingletonClass{
// 임시 확인용 변수
String str;
private SingletonClass(){}
private static SingletonClass instance = new SingletonClass();
public static SingletonClass getInstance() {
return instance;
}
}
코드 분석
6 | – 외부 (다른 클래스)에서 생성자 호출이 불가능하도록 기본 생성자에 private 접근제한자 선언. |
8 | – 자신의 클래스 내에서 인스턴스를 직접 생성 후 참조변수(instance)에 저장. → 외부에서 인스턴스 생성없이 접근 해야하므로 static 키워드 사용. → 외부에서 함부로 접근하지 못하도록 private 접근제한자를 선언함. |
10, 11 | – 생성된 인스턴스를 외부로 리턴하는 get 메서드 정의. → 인스턴스를 저장하는 참조변수 instance가 private 접근제한자이므로 접근이 불가능한 대신, 인스턴스를 리턴하는 목적으로 정의. → 인스턴스 생성없이 접근해야하므로 static 키워드 사용. → return타입이 SingleonClass이므로 같은 주소값을 지닌 instance를 리턴해줌. |
• main 메서드에서 실행하기
// 인스턴스 생성 불가
// SingletonClass sc = new SingletonClass();
// SingletonClass sc2 = new SingletonClass();
// 참조변수
SingletonClass sc;
// SingletonClass sc = instance;
SingletonClass sc = SingletonClass.instance;
SingletonClass sc = SingletonClass.getInstance();
// 리턴받은 인스턴스
sc.str = "싱글톤 패턴으로 생성한 인스턴스의 str";
// sc2에 인스턴스를 리턴받아 저장
SingletonClass sc2 = SingletonClass.getInstance();
System.out.println("sc.str : " + sc.str);
System.out.println("sc2.str : " + sc2.str);
// 두 인스턴스(sc, sc2)가 같은지 비교
if(sc == sc2) {
System.out.println("sc와 sc2는 같은 인스턴스");
}else {
System.out.println("sc와 sc는 다른 인스턴스");
}
코드 분석
8, 9 | – 클래스 내에서 인스턴스 생성 시, static 키워드를 사용하면 instance에 담긴 주소값을 sc 변수에도 저장 가능. → 클래스 내에서 인스턴스를 생성하여 멤버변수 instance에 주소값을 저장했기 때문에, 해당 변수 (instance)를 통해 인스턴스의 주소값을 가져올 수 있음. → get 메서드를 통해 instance를 외부로 리턴하였음. → 이 때, 해당 변수는 static 변수이므로 클래스명만으로 접근 가능함. |
10 | – SingletonClass 내에서 싱글톤 디자인 패턴으로 생성된 인스턴스를 변수 instance에 저장했으나, private 접근제한자로 인해 접근 불가하므로 클래스에 정의해 놓은 getInstance() 메서드 호출을 통해 가져옴. → 이 때, getInstance() 메서드는 static 메서드이므로, 클래스명.메서드명() 형태로 호출 가능. → 즉, 인스턴스 생성 없이도 클래스 내의 메서드 호출 가능. |
13 | – 리턴받은 인스턴스는 참조변수 (sc)를 통해 접근 가능. |
16 | – singletonClass타입 변수 sc2에 인스턴스를 리턴받아 저장. |
18, 19 | – 리턴받은 인스턴스는 참조변수 (sc2)를 통해 접근 가능함. |
22~26 | – if문을 통해 두 인스턴스(sc, sc2)가 같은지 비교 → 같은 인스턴스. → 싱글톤 패턴으로 생성하기 위해 클래스에서 미리 인스턴스를 생성하고, 새로운 인스턴스를 생성하지 못하도록 생성자에 pivate 접근제한자를 사용했으므로, 기존에 있던 클래스의 인스턴스를 사용하는 것. 따라서, 둘은 같은 주소값을 가지며 동등비교연산자 (==)를 사용하였을 시 같다는 결과가 나옴. |
✓ 실행 결과
sc.str : 싱글톤 패턴으로 생성한 인스턴스의 str
sc2.str : 싱글톤 패턴으로 생성한 인스턴스의 str
sc와 sc2는 같은 인스턴스