public Cars(List<String> carNames) {
ArrayList<Car> cars = new ArrayList<>();
for (String carName : carNames) {
cars.add(new Car(carName));
}
this.cars = cars;
}
Cars 객체를 생성할 때, List<String>
형태의 carNames
를 입력받은 뒤,
For-Loop
문을 통해 cars 필드를 초기화 해주었다.
하지만 For-Loop
문을 생성자 로직
에 적용하니 개인적으로 생성자 로직이 지저분하다는 생각이 들었다.
때문에 Stream
을 사용하여 짧고 가독성있는 코드로 리팩토링하여 사용하고자 하였다.
💭 미션의 요구사항에 자동차의 수는 제한이 없는데, 그럼
Integer.MAX_VALUE
대의 자동차 이름을 입력받았을 때For-Loop
와Stream
중 무엇이 더 성능 면에서 뛰어날까?
성능이 더 좋은 것을 사용해야 할텐데,,
평소 자세한 이유는 몰랐지만 주워들은 것이 있어 Stream
의 성능이 더 좋지 않을 것이라고 생각했는데,
정말 Stream
을 사용하는 것이 For-Loop
를 사용하는 것보다 성능이 안 좋다고 한다.
💡 가장 큰 이유는
For-Loop
는 만들어진지 40년 이상 지났기 때문에 최적화가 굉장히 잘 되어 있었고,
Stream
은 2015년 쯤에 Java8과 함께 생겨났기 때문에 아직 최적화 작업이 덜 되어있다는 것이 원인이라고 하는데,,
💭 그럼 무조건
For-Loop
만을 사용해야만 하는 걸까?
나는 이 고민을 해소하기 위해 Stream
과 For-Loop
의 차이에 대해 공부를 시작하게 하게 되었고,
그 과정에서 좋은 글을 발견하여 학습하며 알게된 것을 공유하고자 포스팅을 작성하게 되었다.
Primitive Type
의 경우 JVM
메모리의 Stack
에 값을 저장하고 있기 때문에
접근 속도가 빨라 For-Loop
가 본래의 성능을 발휘할 수 있기에 빠른 반복문 수행이 가능하다.
JVM
의 스택(Stack)
은 실행중인 스레드의 함수 호출 및 지역 변수 관리에 사용되며,
각 스레드는 자체 스택을 가진다.
Primitive Type
은 변수 선언 시 JVM
의 Stack
에 값을 저장하고, 변수 호출 시에 Stack
에서 값을 바로 불러오는 방식을 채택하기 때문에 접근 속도가 굉장히 빠르다.
예를 들어, Primitive Type
인 int
타입의 변수에 42라는 값을 저장하는 경우 42가 스택에 저장되며,
Stack
에 저장된 값은 변수가 선언된 스코프(블록) 내에서만 사용 가능하다.
때문에 Primitive Type
의 경우 Stream
을 사용했을 때와, For-Loop
를 사용했을 때
일반적으로 예측 가능한 결과를 내놓는다.
public class StreamAndForLoop {
static final int MAX_INT = 5000000;
static int[] testData = new int[MAX_INT];
public static void main(String[] args) {
init();
calcForLoop();
calcStream();
}
private static void init() {
for (int i = 0; i < MAX_INT; i++) {
testData[i] = i;
}
}
private static void calcStream() {
int standard = 0;
long before = System.currentTimeMillis();
Arrays.stream(testData)
.filter(t -> t > standard)
.count();
System.out.println("Stream : " + (System.currentTimeMillis() - before));
}
private static void calcForLoop() {
int standard = 0;
int count = 0;
long before = System.currentTimeMillis();
for (int data : testData) {
if(data > standard) {
count++;
}
}
System.out.println("For-Loop : " + (System.currentTimeMillis() - before));
}
}
이는 standard
라는 변수에 저장된 값과, testData
에 저장된 값들의 크기를 비교하여
더 큰 값이 존재하면 count
하는 로직을 Stream
과 For-Loop
방식으로 각각 5000000번
수행하는 코드이다.
당연하게도 Primitive Type
은 접근 속도가 빠르기 때문에 일반적인 경우인
“For-Loop
방식이 더 빠르다” 라는 말에 들어맞는 결과가 출력된다.
결론적으로,
실험한5000000번
의 경우에는 3배 가까이 차이가 나는 것을 확인할 수 있다.
그렇다면 Wrapped Type
은 어떨까?
Wrapped Type
의 경우 Primitive Type
과 달리 값을 저장할 때, Heap 메모리
에 저장한다는 특징이 있다.
Wrapped Type
변수가 선언되면 Heap 메모리
에 객체가 할당되고, Stack
에는 객체를 담고 있는 Heap 메모리 주소
가 저장된다.
결국 우리는 Stack
에 저장된 Heap 메모리 주소
를 참조하여 Heap메모리
에 접근하는 방식을 사용함으로써 저장된 데이터에 접근할 수 있게 되는 것이다.
말로만 들어도 Stack
에 값을 저장하고 바로 참조할 수 있는 Primitive Type
의 접근 방식보다 느려보이는데,
과연 위에서 사용했던 로직과 같은 로직을 Wrapped Type
으로 변환하여 실행시키면 어떤 결과가 나올까?
public class StreamAndForLoop {
static final int MAX_INT = 5000000;
static List<Integer> testData = new ArrayList<>();
public static void main(String[] args) {
init();
calcForLoop();
calcStream();
}
private static void calcStream() {
int standard = 0;
long before = System.currentTimeMillis();
testData.stream()
.filter(t -> t > standard)
.count();
System.out.println("Stream : " + (System.currentTimeMillis() - before));
}
private static void calcForLoop() {
int standard = 0;
int count = 0;
long before = System.currentTimeMillis();
for (Integer data : testData) {
if(data > standard) {
count++;
}
}
System.out.println("For-Loop : " + (System.currentTimeMillis() - before));
}
private static void init() {
for (int i = 0; i < MAX_INT; i++) {
testData.add(i);
}
}
}
이는 위와 똑같이 standard
라는 변수에 저장된 값과 testData
에 저장된 값들의 크기를 비교하여
더 큰 값이 존재하면 count
하는 로직을 수행한다.
하지만 이전처럼 Primitive Type
을 사용하지 않고 Wrapped Type
인 Integer
를 사용하도록 변경하였다.
과연 Wrapped Type
을 사용했을 때, 결과는 어떨까?
신기하지 않은가?? 아까는 3배
가까이 차이가 나던 실행 결과가 이제는 거의 동일한 수준
으로 측정되거나 역전
해버렸다.
이는 Stack
에 저장된 데이터(객체)의 주소
를 통해 Heap 메모리
에 접근하여 참조하는 비용이
Stack
에 값을 저장하고 호출 시에 바로 값에 접근하는 비용에 비해 굉장히 크기 때문에,
Stream
과 For-Loop
의 속도 격차를 완화시켜버린 것이다.
💡 이를 통해
Wrapped Type
을 사용하는For-Loop
경우,
Stream
을 사용하는 경우와 비용이 크게 차이나지 않으므로,
가독성이 더 좋은 측을 선택하여 개발해도 괜찮다는인사이트
를 얻을 수 있었다.
public Cars(List<String> carNames) {
ArrayList<Car> cars = new ArrayList<>();
for (String carName : carNames) {
cars.add(new Car(carName));
}
this.cars = cars;
}
내가 고민하던 로직의 경우 String
이라는 Wrapped Type
을 사용하고 있었기에,
관리하던 생성자 로직에 Stream
을 적용하여 아래와 같이 변경하였다.
public Cars(List<String> carNames) {
this.cars = carNames.stream()
.map(Car::new)
.toList();
}
ArrayList<Car>
객체 선언 후, 반복문을 돌며 객체에 값을 저장하지 않아도 되기 때문에 ArrayList<Car> cars
변수 선언부 삭제 후, 필드에 바로 초기화하도록 수정Stream
을 사용하여 짧고 간결한 코드 작성으로 가독성 향상For-Loop
문과 비교하여 더 좋거나 비슷한 성능
을 발휘함1주차 미션이 끝나고 사람들에게 코드리뷰를 받는 과정에서 내 코드에 대해 명쾌하게 설명할 수 없는 내 모습을 통해 아직
메타인지
가 부족함을 깨달을 수 있었다.
나름왜
에 대해 고민하고 개발한다고 생각해왔지만Deep
한 고민은 아직 부족했던 모양이다.
때문에 이번 미션에선 아주 진득하게 고민하고 개발해보고자 다짐하였다.
그렇게 다짐해서인지 코드 한 줄을 적을 때도 이런 저런 생각이 많이 들었고, 아직 내가 지식이 부족함을 다시 한 번 깨달을 수 있었다.우테코는 프리코스만 참여해도 많이 배워간다고들 하는데, 나 또한 몰입하는 과정에서 정말 많이 배우고 성장해나가고 있는 중인 것 같다. 때문에 본 코스에 참여하고 싶다는 욕구가 너무너무 샘솟는 요즘이다.
그리고 잘하고싶다는 욕구가 샘솟는 만큼 굉장히 잘하는 다른 이들의 코드 또는 지식 공유 내용을 흝어보며 내가 개발을 공부해오던 시간들을 부정당한 느낌도 들고, 더 잘하는 남들을 보며 부럽기도 하지만
조급한 마음에 체력을 오버하며 무리하게 되면 재미있던 개발이 일처럼 느껴짐을 일전에 경험해보았기에 나는서두르지 않으려한다.
이렇게 하나하나 깨달아가면서 성장하는 나를 기다려주는 것이 성장의 재미를 느끼는 방법인 것 같다.
이 글을 보는 모든 이들도 남들의 실력과 비교하기보다 오늘 하루 느낀 것들을 정리해보고 어제의 나보다 얼마나 성장했는지를 비교해보며 한 걸음씩 나아갔으면 좋겠다 :)
다들 화이팅입니다🔥
도움이 많이 되었어요 !