0. 개요.
블랙잭 미션 사이클1, 루드비코와 이야기하다, Enum 캐싱에 대해 알게되었다.
Enum 캐싱이 언제 빠른지. 그리고 빠를 때의 조건을 알아본 과정을 이야기해보려고 한다.
1. Enum 캐싱
Enum을 캐싱한다는 건, 말 그대로 Enum을 미리 캐싱해둔 다는 말이다.
그렇다면 '캐싱'의 의도는 어떻게 등장하게 되었을까?
1 - 1. Enum 캐싱 등장 배경
Enum이 있고, value를 Enum으로 변경하고 싶을 때, 주로 다음과 같이 코드를 작성한다.
다음은 블랙잭에서 흔히 사용되는 Enum의 예시이다.
public enum Rank {
TWO(2, "2"),
THREE(3, "3"),
FOUR(4, "4"),
FIVE(5, "5"),
SIX(6, "6"),
SEVEN(7, "7"),
EIGHT(8, "8"),
NINE(9, "9"),
TEN(10, "10"),
J(10, "J"),
Q(10, "Q"),
K(10, "K"),
ACE(11, "A");
private final int value;
private final String name;
Rank(int value, String name) {
this.value = value;
this.name = name;
}
public static Rank getRank(int targetValue) {
return Arrays.stream(Rank.values())
.filter(rank -> rank.value == targetValue)
.findFirst()
.orElseThrow();
}
}
Enum 캐싱의 의도는, 굳이 getRank()를 호출할 때마다, 리스트를 순회하여, 매칭되는 Rank를 반환할 필요가 없다는 것이다.
리스트의 시간 복잡도는 O(n)이니까!
1 - 2. Enum 캐싱 도입
그래서 다음과 같이 MAP 상수를 정의해두고, static 블록을 통해, 초기화한다.
public enum Rank {
TWO(2, "2"),
THREE(3, "3"),
FOUR(4, "4"),
FIVE(5, "5"),
SIX(6, "6"),
SEVEN(7, "7"),
EIGHT(8, "8"),
NINE(9, "9"),
TEN(10, "10"),
J(10, "J"),
Q(10, "Q"),
K(10, "K"),
ACE(11, "A");
public static final Rank[] ARRAY_CACHE = values();
private final int value;
private final String name;
Rank(int value, String name) {
this.value = value;
this.name = name;
}
public int toValue() {
return value;
}
public String getName() {
return name;
}
}
이때 values() 대신, ARRAY_CACHE를 호출해서 원하는 값을 인덱스로 가져오면 더 빠른거 아니냐! 라는 의도이다.
2. 정말 빠를까?
2 - 1. 따끔한 일침.
루드비코에게 이 이야기를 들었을 때, 오호이야하 좋은데요?라고 했다.
근데 루드비코는 이 부분에 대해서 리뷰어에게 뚜드러 맞았다고 한다.
루드비코는 PR에 Enum 캐싱을 사용해서 O(n)의 시간 복잡도를 O(1)로 줄였다고 적었다.
리뷰어는 성능 측정을 해보았냐고 물어봤고, 이론을 증명하는 방법은 실험밖에 없다고 코멘트를 달아주셨다.
2 - 2. 루드비코의 실험
루드비코는 따끔한 일침을 받고, 직접 실험을 해봤다고 한다.

그 결과는 생각과 달랐다.
배열로 캐싱했을 때(어차피 O(1))가 values()보다 느리게 측정되었다는 것이다.
루드비코는 이 일화를 말하며, 이론과 현실의 차이가 존재하며, 이를 증명하기 위해서는 실험하는 자세가 필요하다는 걸 나에게 알려주었다.
3. 의심
처음 내용을 들었을 때, 와 신기하다. JVM이 내부적으로 더 빠르게 처리를 해주는구나.... 라며 놀랐다.
그런데 아무리 생각해도, O(1)의 시간 복잡도를 가지는 코드가 더 빠를거라 생각했다.
그래서 우선 values()가 어떻게 동작하는 지 확인해보려고 했다.
3 - 1. values()
우선 Java에서 Enum 클래스의 values()는 세부내용을 확인할 수 없다.
또한 Java 공식문서의 Enum 부분에도 Enum의 모든 상수를 호출하는 메서드가 values()라고 되어있을 뿐이다.
그래서 Enum.java를 컴파일해본 뒤, 이를 디스어셈블해보기로했다.
3 - 2. 컴파일 & 디스어셈블
상수가 적은 Suit.java를 컴파일 한 뒤, 디스어셈블 했다.
이글을 읽고 해보고 싶은 사람들은 다음 과정을 해보면 됩니당.
1. javac [컴파일 할 .java파일]
2. javap -v [컴파일된 .class파일]
결과는 다음과 같고, 라인 수가 약 300줄이라 접은 글 처리 해두었다.
Classfile /D:/project/java/practice/java-practice01/src/enums/Suit.class
Last modified 2026. 3. 10.; size 2556 bytes
SHA-256 checksum ce64d1a31da990dbf0717d3458f45f8316957852ba52fa9f8e52c8994f2d3c92
Compiled from "Suit.java"
public final class enums.Suit extends java.lang.Enum<enums.Suit>
minor version: 0
major version: 61
flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
this_class: #1 // enums/Suit
super_class: #26 // java/lang/Enum
interfaces: 0, fields: 8, methods: 8, attributes: 4
Constant pool:
#1 = Class #2 // enums/Suit
#2 = Utf8 enums/Suit
#3 = Fieldref #1.#4 // enums/Suit.SPADE:Lenums/Suit;
#4 = NameAndType #5:#6 // SPADE:Lenums/Suit;
#5 = Utf8 SPADE
#6 = Utf8 Lenums/Suit;
#7 = Fieldref #1.#8 // enums/Suit.DIAMOND:Lenums/Suit;
#8 = NameAndType #9:#6 // DIAMOND:Lenums/Suit;
#9 = Utf8 DIAMOND
#10 = Fieldref #1.#11 // enums/Suit.HEART:Lenums/Suit;
#11 = NameAndType #12:#6 // HEART:Lenums/Suit;
#12 = Utf8 HEART
#13 = Fieldref #1.#14 // enums/Suit.CLOVER:Lenums/Suit;
#14 = NameAndType #15:#6 // CLOVER:Lenums/Suit;
#15 = Utf8 CLOVER
#16 = Fieldref #1.#17 // enums/Suit.$VALUES:[Lenums/Suit;
#17 = NameAndType #18:#19 // $VALUES:[Lenums/Suit;
#18 = Utf8 $VALUES
#19 = Utf8 [Lenums/Suit;
#20 = Methodref #21.#22 // "[Lenums/Suit;".clone:()Ljava/lang/Object;
#21 = Class #19 // "[Lenums/Suit;"
#22 = NameAndType #23:#24 // clone:()Ljava/lang/Object;
#23 = Utf8 clone
#24 = Utf8 ()Ljava/lang/Object;
#25 = Methodref #26.#27 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#26 = Class #28 // java/lang/Enum
#27 = NameAndType #29:#30 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#28 = Utf8 java/lang/Enum
#29 = Utf8 valueOf
#30 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#31 = Methodref #26.#32 // java/lang/Enum."<init>":(Ljava/lang/String;I)V
#32 = NameAndType #33:#34 // "<init>":(Ljava/lang/String;I)V
#33 = Utf8 <init>
#34 = Utf8 (Ljava/lang/String;I)V
#35 = Fieldref #1.#36 // enums/Suit.name:Ljava/lang/String;
#36 = NameAndType #37:#38 // name:Ljava/lang/String;
#37 = Utf8 name
#38 = Utf8 Ljava/lang/String;
#39 = Methodref #1.#40 // enums/Suit.values:()[Lenums/Suit;
#40 = NameAndType #41:#42 // values:()[Lenums/Suit;
#41 = Utf8 values
#42 = Utf8 ()[Lenums/Suit;
#43 = Methodref #44.#45 // java/util/Arrays.stream:([Ljava/lang/Object;)Ljava/util/stream/Stream;
#44 = Class #46 // java/util/Arrays
#45 = NameAndType #47:#48 // stream:([Ljava/lang/Object;)Ljava/util/stream/Stream;
#46 = Utf8 java/util/Arrays
#47 = Utf8 stream
#48 = Utf8 ([Ljava/lang/Object;)Ljava/util/stream/Stream;
#49 = InvokeDynamic #0:#50 // #0:test:(Ljava/lang/String;)Ljava/util/function/Predicate;
#50 = NameAndType #51:#52 // test:(Ljava/lang/String;)Ljava/util/function/Predicate;
#51 = Utf8 test
#52 = Utf8 (Ljava/lang/String;)Ljava/util/function/Predicate;
#53 = InterfaceMethodref #54.#55 // java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
#54 = Class #56 // java/util/stream/Stream
#55 = NameAndType #57:#58 // filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
#56 = Utf8 java/util/stream/Stream
#57 = Utf8 filter
#58 = Utf8 (Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
#59 = InterfaceMethodref #54.#60 // java/util/stream/Stream.findFirst:()Ljava/util/Optional;
#60 = NameAndType #61:#62 // findFirst:()Ljava/util/Optional;
#61 = Utf8 findFirst
#62 = Utf8 ()Ljava/util/Optional;
#63 = Methodref #64.#65 // java/util/Optional.orElseThrow:()Ljava/lang/Object;
#64 = Class #66 // java/util/Optional
#65 = NameAndType #67:#24 // orElseThrow:()Ljava/lang/Object;
#66 = Utf8 java/util/Optional
#67 = Utf8 orElseThrow
#68 = Methodref #69.#70 // java/lang/String.equals:(Ljava/lang/Object;)Z
#69 = Class #71 // java/lang/String
#70 = NameAndType #72:#73 // equals:(Ljava/lang/Object;)Z
#71 = Utf8 java/lang/String
#72 = Utf8 equals
#73 = Utf8 (Ljava/lang/Object;)Z
#74 = String #5 // SPADE
#75 = String #76 // 스페이드
#76 = Utf8 스페이드
#77 = Methodref #1.#78 // enums/Suit."<init>":(Ljava/lang/String;ILjava/lang/String;)V
#78 = NameAndType #33:#79 // "<init>":(Ljava/lang/String;ILjava/lang/String;)V
#79 = Utf8 (Ljava/lang/String;ILjava/lang/String;)V
#80 = String #9 // DIAMOND
#81 = String #82 // 다이아몬드
#82 = Utf8 다이아몬드
#83 = String #12 // HEART
#84 = String #85 // 하트
#85 = Utf8 하트
#86 = String #15 // CLOVER
#87 = String #88 // 클로버
#88 = Utf8 클로버
#89 = Methodref #1.#90 // enums/Suit.$values:()[Lenums/Suit;
#90 = NameAndType #91:#42 // $values:()[Lenums/Suit;
#91 = Utf8 $values
#92 = InterfaceMethodref #93.#94 // java/util/List.of:([Ljava/lang/Object;)Ljava/util/List;
#93 = Class #95 // java/util/List
#94 = NameAndType #96:#97 // of:([Ljava/lang/Object;)Ljava/util/List;
#95 = Utf8 java/util/List
#96 = Utf8 of
#97 = Utf8 ([Ljava/lang/Object;)Ljava/util/List;
#98 = Fieldref #1.#99 // enums/Suit.LIST_CACHE:Ljava/util/List;
#99 = NameAndType #100:#101 // LIST_CACHE:Ljava/util/List;
#100 = Utf8 LIST_CACHE
#101 = Utf8 Ljava/util/List;
#102 = Fieldref #1.#103 // enums/Suit.ARRAY_CACHE:[Lenums/Suit;
#103 = NameAndType #104:#19 // ARRAY_CACHE:[Lenums/Suit;
#104 = Utf8 ARRAY_CACHE
#105 = Utf8 Signature
#106 = Utf8 Ljava/util/List<Lenums/Suit;>;
#107 = Utf8 Code
#108 = Utf8 LineNumberTable
#109 = Utf8 (Ljava/lang/String;)Lenums/Suit;
#110 = Utf8 (Ljava/lang/String;)V
#111 = Utf8 findByName
#112 = Utf8 getName
#113 = Utf8 ()Ljava/lang/String;
#114 = Utf8 lambda$findByName$0
#115 = Utf8 (Ljava/lang/String;Lenums/Suit;)Z
#116 = Utf8 <clinit>
#117 = Utf8 ()V
#118 = Utf8 Ljava/lang/Enum<Lenums/Suit;>;
#119 = Utf8 SourceFile
#120 = Utf8 Suit.java
#121 = Utf8 BootstrapMethods
#122 = MethodHandle 6:#123 // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#123 = Methodref #124.#125 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#124 = Class #126 // java/lang/invoke/LambdaMetafactory
#125 = NameAndType #127:#128 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#126 = Utf8 java/lang/invoke/LambdaMetafactory
#127 = Utf8 metafactory
#128 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#129 = MethodType #73 // (Ljava/lang/Object;)Z
#130 = MethodHandle 6:#131 // REF_invokeStatic enums/Suit.lambda$findByName$0:(Ljava/lang/String;Lenums/Suit;)Z
#131 = Methodref #1.#132 // enums/Suit.lambda$findByName$0:(Ljava/lang/String;Lenums/Suit;)Z
#132 = NameAndType #114:#115 // lambda$findByName$0:(Ljava/lang/String;Lenums/Suit;)Z
#133 = MethodType #134 // (Lenums/Suit;)Z
#134 = Utf8 (Lenums/Suit;)Z
#135 = Utf8 InnerClasses
#136 = Class #137 // java/lang/invoke/MethodHandles$Lookup
#137 = Utf8 java/lang/invoke/MethodHandles$Lookup
#138 = Class #139 // java/lang/invoke/MethodHandles
#139 = Utf8 java/lang/invoke/MethodHandles
#140 = Utf8 Lookup
{
public static final enums.Suit SPADE;
descriptor: Lenums/Suit;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final enums.Suit DIAMOND;
descriptor: Lenums/Suit;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final enums.Suit HEART;
descriptor: Lenums/Suit;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final enums.Suit CLOVER;
descriptor: Lenums/Suit;
flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final java.util.List<enums.Suit> LIST_CACHE;
descriptor: Ljava/util/List;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Signature: #106 // Ljava/util/List<Lenums/Suit;>;
public static final enums.Suit[] ARRAY_CACHE;
descriptor: [Lenums/Suit;
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
public static enums.Suit[] values();
descriptor: ()[Lenums/Suit;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #16 // Field $VALUES:[Lenums/Suit;
3: invokevirtual #20 // Method "[Lenums/Suit;".clone:()Ljava/lang/Object;
6: checkcast #21 // class "[Lenums/Suit;"
9: areturn
LineNumberTable:
line 6: 0
public static enums.Suit valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)Lenums/Suit;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class enums/Suit
2: aload_0
3: invokestatic #25 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #1 // class enums/Suit
9: areturn
LineNumberTable:
line 6: 0
public static enums.Suit findByName(java.lang.String);
descriptor: (Ljava/lang/String;)Lenums/Suit;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: invokestatic #39 // Method values:()[Lenums/Suit;
3: invokestatic #43 // Method java/util/Arrays.stream:([Ljava/lang/Object;)Ljava/util/stream/Stream;
6: aload_0
7: invokedynamic #49, 0 // InvokeDynamic #0:test:(Ljava/lang/String;)Ljava/util/function/Predicate;
12: invokeinterface #53, 2 // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
17: invokeinterface #59, 1 // InterfaceMethod java/util/stream/Stream.findFirst:()Ljava/util/Optional;
22: invokevirtual #63 // Method java/util/Optional.orElseThrow:()Ljava/lang/Object;
25: checkcast #1 // class enums/Suit
28: areturn
LineNumberTable:
line 23: 0
line 24: 12
line 25: 17
line 26: 22
line 23: 28
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #35 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 30: 0
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #1 // class enums/Suit
3: dup
4: ldc #74 // String SPADE
6: iconst_0
7: ldc #75 // String 스페이드
9: invokespecial #77 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
12: putstatic #3 // Field SPADE:Lenums/Suit;
15: new #1 // class enums/Suit
18: dup
19: ldc #80 // String DIAMOND
21: iconst_1
22: ldc #81 // String 다이아몬드
24: invokespecial #77 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
27: putstatic #7 // Field DIAMOND:Lenums/Suit;
30: new #1 // class enums/Suit
33: dup
34: ldc #83 // String HEART
36: iconst_2
37: ldc #84 // String 하트
39: invokespecial #77 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
42: putstatic #10 // Field HEART:Lenums/Suit;
45: new #1 // class enums/Suit
48: dup
49: ldc #86 // String CLOVER
51: iconst_3
52: ldc #87 // String 클로버
54: invokespecial #77 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
57: putstatic #13 // Field CLOVER:Lenums/Suit;
60: invokestatic #89 // Method $values:()[Lenums/Suit;
63: putstatic #16 // Field $VALUES:[Lenums/Suit;
66: invokestatic #39 // Method values:()[Lenums/Suit;
69: invokestatic #92 // InterfaceMethod java/util/List.of:([Ljava/lang/Object;)Ljava/util/List;
72: putstatic #98 // Field LIST_CACHE:Ljava/util/List;
75: invokestatic #39 // Method values:()[Lenums/Suit;
78: putstatic #102 // Field ARRAY_CACHE:[Lenums/Suit;
81: return
LineNumberTable:
line 7: 0
line 8: 15
line 9: 30
line 10: 45
line 6: 60
line 13: 66
line 14: 75
}
Signature: #118 // Ljava/lang/Enum<Lenums/Suit;>;
SourceFile: "Suit.java"
BootstrapMethods:
0: #122 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#129 (Ljava/lang/Object;)Z
#130 REF_invokeStatic enums/Suit.lambda$findByName$0:(Ljava/lang/String;Lenums/Suit;)Z
#133 (Lenums/Suit;)Z
InnerClasses:
public static final #140= #136 of #138; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
간단하게 보면, 다음과 같이 values() 메서드가 생성된 걸 확인할 수 있다.
public static enums.Suit[] values();
descriptor: ()[Lenums/Suit;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #16 // Field $VALUES:[Lenums/Suit;
3: invokevirtual #20 // Method "[Lenums/Suit;".clone:()Ljava/lang/Object;
6: checkcast #21 // class "[Lenums/Suit;"
9: areturn
LineNumberTable:
line 6: 0
여기서 부터는 Gemini의 도움을 받았다. 위 주석은 javap 명령어가 자동으로 적어준다.
values()가 호출되면, 상수풀에서 16번에 저장되어있는 $VALUES라는 스태틱 배열을 가져온다.
그리고 해당 배열을 clone()한다.
여기서 새로운 배열이 생성된다.
참고로 $VALUES라는 상수 저장은 static{} 이 실행될 때, 되는 걸 확인할 수 있다.
static을 invoke한 뒤, 16번에 $VALUES로 저장하는 과정이 컴파일 단계에서 static{ }에 수행되도록 정해진다.
60: invokestatic #89 // Method $values:()[Lenums/Suit;
63: putstatic #16 // Field $VALUES:[Lenums/Suit;
디스어셈블 전까지는 루드비코의 결과가 조금 의심스러웠는데, 이 결과를 보고 많이 의심스러워졌었다.
아무리 생각해도 clone()을 계속 호출하는 values()보다 캐싱이 더 빠르지 않을까?? 라는 생각이었기 때문이다.
그래서 나도 직접 실험을 해보려고했다.
4. 나의 실험 결과
우선 루드비코의 실험 환경을 물어봤는 데, OneCompiler라는 사이트에서 실행했었다.
EnumValuesCachingBenchmark - Java - OneCompiler
Java online compiler Write, Run & Share Java code online using OneCompiler's Java online compiler for free. It's one of the robust, feature-rich online compilers for Java language, running on Java 25. Getting started with the OneCompiler's Java editor is e
onecompiler.com
그래서 아! 사이트에서 실행되는 환경이라 잘못될 수 있으려나? 생각했다.
그래서 저 OneCompiler 사이트에 있는 코드를 내 컴퓨터에 복사해서 돌려봤다.

내 컴퓨터에서는 배열로 캐싱한게 더 빨랐다.
영크크....
혹시 몰라 JMH라는 성능 벤치마킹 툴을 사용해봤는데

다음과 같이, 배열 캐싱의 점수가 압도적으로 좋았다.
그래서 나는 노트북과 환경에 따라 성능이 다를 수 있다는 결론을 내게 되었고,
이를 루드비코에게 전해주었다.
5. 조금 더 생각해보기.
노트북과 환경에 따라 성능이 다를 수 있는 거 같다고 루드비코에게 전했다.
루드비코는 추가로 자신의 인텔리제이에서 테스트를 해보니, values()가 더 빨랐었다.
음 역시 노트북에 따라 달라지는 구나! 라고 생각을 했다.
5 - 1. 설명되지 않는 의문 추가
그러나 한가지 의문이 생기기 시작했다.
바로 JMH의 결과는 나와 루드비코 모두 Caching Array가 더 빠르게 나온 것이다??
나는 그렇다고 쳐도, 루드비코는 직접 실험해봤을 때는 values()가 빠르고, JMH를 돌렸을 때는 Caching Array가 더 빠르게 나왔다. 같은 노트북에서 결과가 다르게 나온 것이다.
5 - 2. 새로운 가설.
확실하게 알 수 있는 건, 속도 차이의 원인이 '환경'은 아니라는 것이다.
그렇다면, 어떤 것이 다른 결과를 만들었을까?
생각을 해보니 실행 횟수가 다르다.
여기서 생각한 나의 가설은 JVM이 실행할 때, 모든 메서드를 최적화하는 방법을 찾기 힘들다는 것이다.
그래서 자주 사용되는 메서드나 사용될 것 같은 메서드를 최적화하는 방법을 사용하지 않을까? 라는 것이다.
Spring Boot에서 첫번째 요청은 응답 속도가 느린데, 그 뒤는 최적화 되어 속도가 빨라졌던 경험으로 파생된 가설이다.
6. JVM
6 - 1. 가벼운 이론 (틀린 내용이 있다면 언급해주세요...)
조금 찾아보니 JVM은 Warm Up이라고 하여, 최초에는 느리고, 점차 최적화 되어 속도가 빨라진다고 한다.
특히, 자바는 어플리케이션을 실행할 때, 모든 파일을 컴파일하지 않는다고 한다.
자바의 Class Loader는 JVM의 Runtime Data Area에 .class파일을 로드, 링크, 초기화하는 작업을 수행하게한다.
Load는 말 그대로 클래스파일을 메모리에 올리는 작업이다.
Link는 해당 클래스파일이 실행 가능한 지 검증하는 작업이다. (물론 세세하게 3단계로 나누어져 있지만, 논외)
Initialization은 클래스 변수들을 적절히 초기화하는 작업이다.
이때, 유의할 점은 메모리에 Loading할 때, 모든 .class파일을 올리지 않고, 필요에 의해서 동적으로 올린다는 것이다.
따라서, Warm Up 전에는 Class loader가 수행하는 로드, 링크, 초기화 작업이 필요하기 때문에 오래 걸린다.
자주 실행되는 코드를 기계어로 컴파일하는 JIT 컴파일러는 메모리에 로드되어야 비로소 작업을 수행할 수 있다.
결국, 한 번만 실행한 코드는 Warm Up이 되지 않았고 이렇게 측정한 시간은 제대로된 성능 측정이 아니라는 것이다.
그래서 여러번 실행해보는 JMH의 성능 측정 결과가 더 정확한 것이다.
(위 글 중 틀린 내용이나 추가할 내용이 있나면 댓글 남겨주세요. JVM까지 자세히 공부하지 못했기에 틀린 내용이 무조건 있을 거 같습니다.)
6 - 2. 여러번 실행해보니!
그래서 다음과 같이, 벤치마킹을 돌리는 메서드를 하나로 통일했고 이를 여러번 반복하기로 했다.
public static void main(String[] args) {
for(int i = 0 ; i < 30 ; i++){ // 30번 정도 실행
doBenchmarking(i);
}
}
private static void doBenchmarking(int callNumber){
benchmarkListCaching();
benchmarkArrayCaching();
benchmarkCardsNoCaching();
System.out.println(callNumber + "번째 벤치마킹 종료");
}
위 코드에서는 30번 정도 반복했다.
6 - 3. 결과
일부러 OneCompiler에서 실행했다.
캐싱(List.of()) - 소요 시간: 874ms
캐싱(기본 배열[]) - 소요 시간: 529ms
values() 호출 - 소요 시간: 344ms
0번째 벤치마킹 종료
캐싱(List.of()) - 소요 시간: 229ms
캐싱(기본 배열[]) - 소요 시간: 19ms
values() 호출 - 소요 시간: 42ms
1번째 벤치마킹 종료
캐싱(List.of()) - 소요 시간: 840ms
캐싱(기본 배열[]) - 소요 시간: 0ms
values() 호출 - 소요 시간: 351ms
2번째 벤치마킹 종료
캐싱(List.of()) - 소요 시간: 154ms
캐싱(기본 배열[]) - 소요 시간: 0ms
values() 호출 - 소요 시간: 18ms
결과는 위와 같이, 내 가설이 맞다고 말해준다.
첫 호출은 캐싱이 느리지만, 실행을 반복할 수 록, 캐싱의 소요 시간이 매우 빨라지는 걸 확인할 수 있으며, Warm Up이 금방 되어, 3번째 호출만에 소요 시간이 0ms가 되었다.
루드비코에게 이 사실을 말해주었고, 루드비코의 노트북과 나의 노트북에서도 테스트해본 결과 동일하게 나왔다.
7. 소감
처음에는 Enum 캐싱과 values()에 대한 호기심으로 시작했다.
하다보니 얕게나마, JVM을 알아보게 되었다.
단순히 아 환경마다 다르구나~ 라고 생각하고 말았을 수 있는데, 우테코에서 생활하는 김에 계속 궁금증을 해결해보려고 했고, 그 궁금증이 해결되어서 뿌듯하다.
JVM은 너무 얕게 본거 같은데, 공부를 시작하기엔 너무 방대한 거 같아서, 공부하기 무섭다...
'우아한 테크 코스 8기' 카테고리의 다른 글
| Scanner vs BufferedReader (+ 간단한 GC) (0) | 2026.03.23 |
|---|---|
| 불변 클래스 record (0) | 2026.03.20 |
| 우아한 테크 코스 8기 Level 1 - 1주차 (0) | 2026.03.03 |
| [우아한 테크 코스 8기] 최종 합격 (0) | 2026.01.23 |
| [우아한 테크 코스 8기] 1차 합격 및 최종 코딩테스트 후기 (0) | 2025.12.30 |