😣서론
최근 싱글톤을 직접 구현하여 사용하게 되었고,
스레드 세이프를 하기 위해서 static inner class를 사용하게 되었다.
public class LottoTicketBooth {
private LottoTicketBooth() {
}
private static class LottoTicketBoothHolder {
private static final LottoTicketBooth LOTTO_TICKET_BOOTH = new LottoTicketBooth();
}
public static LottoTicketBooth getInstance() {
return LottoTicketBoothHolder.LOTTO_TICKET_BOOTH;
}
}
위와 같이 사용하게 된다면
LottoTicketBoothHolder 클래스의 'LOTTO_TICKET_BOOTH' 값 은
지연로딩으로 인해 호출할 때 초기화가 된다고 한다.
여기서 의문이 생겼다.
분명 static으로 선언된 친구들은 jvm이 올라갈 때 전부 초기화가 이루어진다고 알고 있었는데,
어째서 static inner class는 호출될 때 초기화가 되는거지...?
😗본론
여러 블로그들이 static inner class를 이용한 싱글톤을 설명을 하였다.
하지만, 전부 '내부 클래스는 호출될 때 로드돼요' 로 끝이 났다.
어디서도 static inner class가 어째서 다른 static 값과 같이 맨 처음 로드가 되지 않고
호출될 때 로드되는 지에 대해서는 찾아볼 수가 없었다..
일단,
눈으로 보기 전까지 못 믿는다.
public class LottoTicketBooth {
private LottoTicketBooth() {
}
private static final String LOTTO_TICKET_BOOTH_HI = "LottoTicketBooth HI !!!!";
static {
System.out.println(LOTTO_TICKET_BOOTH_HI);
}
private static class LottoTicketBoothHolder {
static {
System.out.println("LottoTicketBoothHolder HI !!!");
}
private static final LottoTicketBooth LOTTO_TICKET_BOOTH = new LottoTicketBooth();
}
public static LottoTicketBooth getInstance() {
return LottoTicketBoothHolder.LOTTO_TICKET_BOOTH;
}
public static void main(String[] args) {
System.out.println("HI??");
}
}
이젠, 믿는다.😏
그런데,
'HI??' 보다 static에 있는 'LottoTicketBooth HI !!!!'이 먼저 호출이 되었다.
이는 클래스가 올라가면서 static을 먼저 초기화 하였기 때문!?
그런데 static inner class의 static은 호출되지 않았다...
왜..?
.
.
.
아무리 찾아도 내 궁금증을 해결하지 못해서 SOS 요청..
현재 진행 중인 NextStep 13기 리뷰어님에게 질문을 남겼더니,
친절하게 설명 + 참고링크까지 보내주셨다..😲😲 (감사합니다)
.
.
.
하지만,
몇 십분 동안
해당 글과, 다른 글들을 더 찾아보면서도 잘 이해하지 못했었다.
그러던 와중에 ! 하나 눈에 딱 들어오는 댓글이 있었다.
로드를 클래스 초기화(리졸브)와 혼동하지 마십시오.
ClassLoader#loadClass(String)가 클래스를 초기화하지 않습니다.
어..?
로드랑 초기화랑 다른거야..?
맞아.. 다르지..? 근데 같이 동작하는 게 아니었나..?
해당 글에 힘을 실어주는 답변이 맨 아래에 있었다. (이걸 못 보다니.!)
(해당 답변을 발췌하여 번역하였습니다.)
public class Outer
{
private static final String TEST01 = "I'm TEST01";
static
{
System.out.println("1 - Initializing class Outer, where TEST01 = " + TEST01);
}
public static void main(String[] args)
{
System.out.println("2 - TEST01 --> " + TEST01 );
System.out.println("3 - Inner.class --> " + Inner.class);
System.out.println("5 - Inner.info() --> " + Inner.info() );
}
private static class Inner
{
static
{
System.out.println("4 - Initializing class Inner");
}
public static String info()
{
return "I'm a method in Inner";
}
}
}
특히 다음 줄에서 일련 번호에 집중해주세요.
System.out.println("5 - Inner.info() --> " + Inner.info() );
프로그램을 실행하면 콘솔에서 다음 결과를 볼 수 있습니다.
1 - Initializing class Outer, where TEST01 = I'm TEST01
2 - TEST01 --> I'm TEST01
3 - Inner.class --> class com.javasd.designpatterns.tests.Outer$Inner
4 - Initializing class Inner
5 - Inner.info() --> I'm a method in Inner
각 단계에 대한 자세한 내용:
1 - 프로그램을 실행하면 'Outer' 클래스가 초기화됩니다.
static 변수 'TEST01'은 static 블록 전에 초기화됩니다.
'Inner' 클래스는 로드, 초기화되지 않았습니다.
2 - 'Main' 메서드가 호출되어 'TEST01'의 값을 표시합니다.
3 - System.out은 'Inner' 클래스를 나타냅니다.
'Inner' 클래스는 초기화되지 않았지만 로드되었습니다(메모리 모델에 참조가 있는 이유임).
4 - 가장 흥미로운 부분은 다음과 같습니다.
왜냐하면 System.out은 'Inner'(Inner.info)의 'info' 메서드에 액세스해야 하며,
'info' 메서드의 결과를 반환하기 전에 'Inner' 클래스를 초기화해야 합니다.
그래서 'Inner' 클래스의 static 이 4단계입니다.
5 - 마지막으로 System.out에는 표시해야 할 모든 데이터가 있고, 마지막 줄이 콘솔에 표시됩니다.
이 글에서 중심적으로 봐야할 키워드는 두 개라고 생각 된다.
로드, 초기화
Main 메소드를 포함하고 있는 Outer 클래스는
Main 메소드를 실행하기 전에 초기화가 진행이 된다.
그렇기 때문에 static 변수와 static 블록이 초기화가 진행되어, 호출되게 된다. (1번 print)
다음으로 봐야할 부분은 (3번 print)
3번에서는 Inner.class를 호출하였고, 해당 클래스가 메모리에 올라가게 되어
해당 값(class com.javasd.designpatterns.tests.Outer$Inner)이 표시가 되었다.
이는 클래스는 로드가 되어 있는 상태라는 의미입니다.
하지만, static 블록이 호출되지 않았기에 해당 Inner 클래스는 아직 초기화 전이란 것을 알 수 있다.
마지막으로 볼 부분은 본문에서도 말했듯 가장 흥미로운 (4번 print)
4번에서는 Inner 클래스의 info 메소드를 호출하였다.
info 메소드를 호출하기 위해서는 Inner 클래스가 초기화가 되어 있어야 한다.
그렇기에 info를 호출하기 전에 Inner 클래스의 초기화가 진행이 되었고,
초기화가 진행됨에 따라 static 블록에 있는 4번 print 가 먼저 호출이 된 것.
이는 아래와 같은 흐름으로 진행된 것으로 보입니다.
0. Outer.class 로드 됨.
1. Outer.class의 Main메소드를 호출하기 위해 Outer 클래스 초기화 진행
2. static 선언은 클래스 초기화와 동시에 이루어지기 때문에 Main 메소드 전에 콘솔 1이 호출 됨.
3. Main 메소드 호출 2번 콘솔 (static 변수도 초기화됨)
4. Inner.class 호출로 Inner.class 로드 (초기화는 진행하지 않음)
5. Main 메소드를 통해 Inner.info() 접근 시도
6-0. Inner.class는 static이기에 새로 new 를 통해 신규 인스턴스를 생성하지 않음.
6-1. 그럼 Inner.class가 이미 초기화가 되었는가? - x
6-2. Inner.class 초기화 진행
7. static 선언은 클래스 초기화와 동시에 이루어지기 때문에 info 메소드 호출 전에 호출 됨.
8. Inner.info() 호출
거의 이틀 간 고민을 했었는데,
해당 글을 통해 이해하고 나니 꽤 상식적? 쉬운? 개념인 것 같다.
이러한 접근을 방해하는 나의 실수이자 생각이 잡혀 있었다.
😮나의 실수1
로드 === 초기화
jvm설명 글을 읽었을 때에
Loading -> Linking -> Initialization 순으로 된다고 하였다.
나는 이게 동시에 이루어지는 건 줄 알았지만
막상 직접 진행해보니 Loading !== Initalization 인 걸 깨닫게 되었다.
😮나의 실수2
static이 올라가는 시점
나는 jvm에게 모두 그냥 다! 해줘! 를 통해
자바가 올라갈 때 모든 static으로 정의된 것들이 한 번에 올라가는 줄 알았다.
아무리 static 으로 정의 된 값이라 하더라도
모두 jvm이 시작하자마자 올라가는 건 너무 비효율적인 방법이다.
그게 언제 사용될 줄 알고,,, 사용 안 될지도 모르는 모든 클래스들의 static 값을 올린다니..?🤦♂️
클래스가 시작할 때 초기화가 되면서 올라가는 게 맞다.
왜냐하면 그 클래스에서, 클래스를 통해 사용하기 위해 정의 된 static이기 때문에.
하지만, 대부분 사람들은 로딩 or 로드가 이루어진다고 말하며,
해당 inner class를 지연로딩이라고 설명을 한다.
내가 생각하기에는 로딩과 초기화는 거의 동시에 이루어지기 때문에
로딩부터 초기화를 한 번에 통틀어서 로딩이라는 명칭으로 사용되는 것 같다.
하지만, 이는 초보자에게는 헷갈리게 할 수 있는 용어라고 생각된다.
(내가 그랬던 것 처럼)
스택오버플로우에서 나온 댓글처럼 둘을 따로 보고 접근 하는 것이
더욱 헷갈리지 않고 명확하게 이해할 수 있는 접근방식이라 생각이 된다.
😮나의 실수3
static 키워드의 속았다.
사실 모든 클래스는 '원래' static 메모리에 올라가는 static 들이다.
클래스에 static을 붙이게 되면, 인스턴스의 생성 방식이 달라지기만 할 뿐..
static이 붙지 않았을 경우.
new 생성자를 통해 인스턴스 생성 후 만들어진 인스턴스를 통해 접근이 가능해진다.
static이 붙은 경우.
내부 클래스의 인스턴스를 바로 접근이 가능해진다.
또한, 스태틱 메소드에서는 스태틱 메소드만을 호출할 수 있다.
그렇기 때문에 정적 팩토리 메소드를 통해 진행되는 싱글톤 패턴을 inner class로 사용하려면
해당 클래스 또한 static이어야만 호출할 수 있게 된다.
😎결론
클래스는 호출될 때 초기화를 진행한다.
우리는 대부분 new 생성자를 통해서 클래스를 초기화 하며
이때 static으로 선언된 애들이 올라가게 된다.
하지만, static은 유일한 하나이기 때문에 한 번 올라가면,
새로 해당 클래스를 초기화를 진행하더라도 먼저 올라간 걸 호출해서 사용된다.
😐여담
확실히 전세계에서 사용하는 스택오버플로우에 더욱 많은 글들이 존재하고,
내가 고민했던 것들은 선배 개발자들이 먼저 고민해서 토론을 나눈 것들이 많다.
영어를 잘해서 스택오버플로우를. 공식문서를
편하게 읽고, 쓰고, 공감하고.. 하고 싶다..
+ 여담이지만..
이번 진행을 하면서 static으로 정의된 변수 or 클래스들은
모두 선언 순서가 중요하다는 걸 알았다.
어찌보면 당연한 거였지만, 자바를 사용하면서 상수나 변수를 항상 맨 위에 올려놓고
사용하였기에 순간 순서가 바뀌어 빨간줄이 보여서 당황하였다.
2022.10.12 수정
클래스를 처음 사용할 때 Class Loader에서
Loading과 Linking이 진행되고, 해당 클래스가 초기화(new)될 때 Initialization이 진행된다.
보통 외부 클래스의 경우 처음 클래스를 사용할 때 new User() 와 같이 사용하여 로딩과 초기화가 같이 이루어진다.
내부 클래스도 보통 첫 초기화를 진행할 때 로딩과 초기화가 이루어지지만,
Inner.class와 같이 별도로 로딩만을 진행시킬 수도 있어서 로딩과 초기화를 다른 시간에 진행할 수 있는 것 같다.
2022.12.19 수정
InnerClass 로딩 순서를 OuterClass 로딩과 동일하게 이해했었는데 아니었다.
InnerClass의 로딩은 Inner.class 를 호출할 때 로딩이 되었다.
java -verbose:class [클래스명] 을 사용하면 로딩되는 각 클래스들의 정보를 확인할 수 있다.
중간에 다른 클래스들이 로딩되는건 많이 잘랐습니다.
해당 이미지로 실행순서를 살펴보겠습니다.
- javac Outer.java 명령어로 컴파일을 진행합니다.
- java -verbose:class Outer 명령어로 Outer 클래스를 실행하는데, 로딩 되어지는 각 클래스 정보를 봅니다.
- 모든 클래스의 상위 클래스는 Object입니다. 당연히 Object 클래스 먼저 로딩됩니다.
- Outer surce: file:/Users/hyo/workspaces/ 로 Outer 클래스를 로드합니다.
- 1, 2 print를 출력합니다.
- 3번 print가 찍히기 전 Outer$Inner 클래스를 로드합니다.
- 3, 4, 5 print를 출력합니다.
Class 초기화 타이밍
- 인스턴스 생성
- 정적 메서드 호출
- 정적 변수 할당(사용) (상수 변수가 아닌 변수 - not final)
중요한 건 Class 로딩 및 초기화는 단 한번 수행한다.
jvm이 여러 스레드에서 시도해도 스레드 세이프하게 만들어준다.
어? new로 클래스를 초기화하는 게 아니었나?
여기서 클래스 초기화와 인스턴스 초기화가 또 다른 것 같다.
간단한 테스트를 통해봐보자.
public class Outer
{
private static final String TEST01 = "I'm TEST01";
static
{
System.out.println("1 - Initializing class Outer, where TEST01 = " + TEST01);
}
public static void main(String[] args)
{
System.out.println("2 - TEST01 --> " + TEST01 );
System.out.println("3 - Inner.class --> " + Inner.class);
System.out.println("5 - Inner.info() --> " + Inner.info() );
Inner inner1 = new Inner(5);
Inner inner2 = new Inner(4);
System.out.println("6 - inner1 a --> " + inner1.getA() + " " + inner1);
System.out.println("7 - inner2 a --> " + inner2.getA() + " " + inner2);
}
private static class Inner
{
private int a;
Inner(int a) {
this.a = a;
}
static
{
System.out.println("4 - Initializing class Inner");
}
public static String info()
{
return "I'm a method in Inner";
}
public int getA() {
return a;
}
}
}
Inner 인스턴스를 두개를 추가했다.
로딩녀석은 보이지 않고, 모르겠는 녀석들이 보인다.
LambdaForm과 BoundMethodHandle을 한 번 알아봐야겠다.
너무 늦었다.. 천천히 다시 알아봐야지..
몇몇 분들이 질문을 주시고, 수정사항을 알려주시면서 글 내용이 점차 더 업그레이드 되는 것 같습니다. 감사합니다.
글을 좀 더 수정해서 읽기 쉽게 업데이트 하겠습니다.
글을 읽으시고, 잘못된 개념이나 내용이 있다면 지적 부탁드립니다 !(__)
참고
https://stackoverflow.com/questions/24538509/does-the-java-classloader-load-inner-classes#comment37999635_24538703
https://nesoy.github.io/articles/2020-11/ClassLoader
https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html
https://velog.io/@skyepodium/%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-%EC%96%B8%EC%A0%9C-%EB%A1%9C%EB%94%A9%EB%90%98%EA%B3%A0-%EC%B4%88%EA%B8%B0%ED%99%94%EB%90%98%EB%8A%94%EA%B0%80/#1-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%94%A9-%EC%8B%9C%EC%A0%90-1
http://sjava.net/2008/02/java-%EB%AA%85%EB%A0%B9%EC%96%B4%EC%9D%98-%EC%98%B5%EC%85%98-%EC%A0%95%EB%A6%AC/
https://freeprog.tistory.com/198
https://www.infoworld.com/article/2979739/java-101-classes-and-objects-in-java.html
'JVM > Java' 카테고리의 다른 글
[Java] GC (Garbage Collection) ?? (0) | 2022.03.13 |
---|---|
[Java] 22년 부터는 Integer 타입을 사용할 때 조심하자! (0) | 2022.01.06 |
[Java] junit5 파라미터로 List 전달하는 방법 (0) | 2021.11.19 |
[Java]Stack 대신 Deque 사용하기 (0) | 2021.09.20 |
[Java] HashMap. stream으로 sum하기 (0) | 2021.09.14 |