알고리즘 문제를 풀며 "더 좋은 코드란 무엇인가?"라는 질문을 스스로에게 던지게 되었습니다. 단순히 가독성만을 좇는 것이 정답일까? 아니면 성능을 위해 가독성을 포기해야 할까? 이 고민의 과정을 나머지가 1이 되는 수 찾기 문제를 통해 정리해 보았습니다.
n % i == 1을 만족하는 가장 작은 i를 찾는 과정에서 전통적인 for 루프와 Java Stream API를 비교했습니다.
class Solution {
public int solution(int n) {
for (int i = 2; i < n; i++) {
if (n % i == 1) return i;
}
return n - 1;
}
}
return IntStream.range(2, n)
.filter(i -> n % i == 1)
.findFirst()
.orElse(n - 1);
장점 (성능 최적화): 추가적인 객체 생성 없이 CPU가 메모리에 직접 접근하므로 오버헤드가 거의 없습니다. return 문을 만나면 즉시 연산을 종료하기 때문에 불필요한 반복을 확실하게 막습니다.
단점 (가독성): 로직이 복잡해질 경우 코드가 길어지고 순수하게 비즈니스 로직에만 집중하기 어려울 때가 있습니다.
장점 (가독성 및 생산성): '무엇을 하는지'에 대한 선언적 문법이므로 코드가 간결하고 읽기 쉽습니다. 데이터 흐름을 추적하기에 유리합니다.
단점 (성능 오버헤드): IntStream, Pipeline, 람다식 등 처리 과정을 위해 내부적으로 다수의 객체가 생성됩니다. 단순 루프보다 속도와 메모리 측면에서 비용이 발생합니다.
이번 경험을 통해 느낀 점은 단순히 데이터를 순회하고 조건에 맞는 값을 바로 반환해야 하는 코딩 테스트 환경에서는 for 루프를 사용하는 것이 훨씬 효율적이라는 것을 체감했습니다. 반면, 실무에서 대량의 데이터를 필터링하거나 복잡하게 변환해야 하는 상황이라면 Stream을 사용하여 코드의 가독성과 유지보수성을 챙기는 것이 더 나은 선택이 될 수 있음을 깨달았습니다.
이번 고민을 통해 상황에 맞는 도구의 선택 기준을 다음과 같이 정립했습니다.
알고리즘 및 코딩 테스트: for 루프를 우선적으로 선택합니다. 시간/메모리 제한이 엄격한 환경에서는 오버헤드가 없는 루프가 가장 강력한 정답입니다.
실무 개발: 상황에 따라 유연하게 선택합니다. 가독성이 유지보수에 큰 영향을 미치는 복잡한 로직이라면 Stream을 적극 활용하고 성능이 병목이 되는 구간이라면 루프로 리팩토링하는 전략을 취하겠습니다.
그동안은 "Stream을 쓰면 세련된 코드"라고 생각했습니다. 하지만 이번 성능 비교를 통해 기술은 상황에 따라 다른 가치를 가진다는 것을 배웠습니다.
성능이 최우선인 곳에서는 기본기인 루프의 효율성을 극대화하고 생산성이 중요한 곳에서는 Stream의 표현력을 십분 활용하는 것이 진짜 실력임을 깨달았습니다. 단순히 기술을 적용하는 것에 그치지 않고 "왜 이 도구를 선택했는가?"에 대한 근거를 가질 수 있는 개발자가 되어야겠습니다. 맹목적으로 최신 문법을 따르기보다 코드의 목적에 맞는 가장 적합한 도구를 찾아내는 안목을 기르겠습니다.