왜 당신은 코드를 성능 있게 만들면서 시간을 낭비하고 있는가

발행: (2026년 3월 9일 AM 02:09 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

데이터

// Note to really senior developers: I'm aware that Double is not an
// appropriate data type for prices, but I'm using it for the sake of simplicity.
case class Product(name: String, price: Double)

val products = List(
  Product("Apple",  50),
  Product("Banana", 21),
  Product("Orange", 70)
)

함수형 버전

def applyDiscount(discount: Double)(product: Product) =
  product.price * (1 - discount)

def priceGreaterThan(limit: Double)(price: Double) =
  price > limit

def calculateTotalPrice(firstPrice: Double, secondPrice: Double) =
  firstPrice + secondPrice

val totalPrice = products
  .map(applyDiscount(0.1))          // apply 10 % discount
  .filter(priceGreaterThan(20))     // keep only prices > 20
  .reduce(calculateTotalPrice)      // sum the remaining prices

명령형 버전

var totalPrice = 0.0
for product  20 then
    totalPrice += discountedPrice

원래 논쟁

원본 게시자는 (Hic sunt dracones 주석은 무시하고) 어느 옵션이 더 보기 좋은지 물었으며, 가독성과 의도 전달 방식 때문에 함수형 버전을 약간 선호한다고 밝혔습니다.

예상대로 논의는 곧 성능으로 전환되었습니다. 일부 시니어 개발자들은 함수형 코드가 O(3 n) 시간 복잡도를 가지고, 명령형 코드는 O(n) 라고 주장했습니다.

Note: “O(3 n) is not a thing. Period.”

그들의 논리: 함수형 파이프라인은 컬렉션을 세 번 순회합니다 (map 한 번, filter 한 번, reduce 한 번), 반면 명령형 버전은 한 번만 루프를 돕니다.

왜 “O(3 n)”은 잘못된 표현인가

빅‑오 표기법은 입력 크기가 → ∞ 로 갈 때의 점근적 성장률을 설명한다.
모든 선형 함수—n/150, n, 3 n, 12 500 000 n—는 **Θ(n)**이며, 즉 같은 복잡도 클래스에 속한다. 상수 계수(3, 12 500 000 등)는 빅‑오를 논할 때 무시된다.

간단한 성능 모델

요소당 세 개의 연산이 있다고 가정하고, 각각의 실행 시간은 a, b, c 입니다.
요소를 역참조하는 데는 시간 d가 소요됩니다.

시나리오n 요소에 대한 총 시간
세 번 패스3 d n + a n + b n + c n = (3 d + ℓ) n
한 번 패스d n + a n + b n + c n = (d + ℓ) n

여기서 ℓ = a + b + c 로 정의합니다.

두 실행 시간의 비율은

[ \frac{3d + ℓ}{d + ℓ} ]

ℓ = 0 (즉, 루프 내부에 작업이 없을 때)만 세 번 패스 버전이 세 배 느려집니다.
루프 내부 작업이 역참조 비용보다 한 차례 정도 크게 (ℓ = 10 d)면, 비율은 13/11 ≈ 1.18 → 18 % 느려짐이며, 300 % 느려짐이 아닙니다.

가독성 vs. 성능

함수형 버전의 커리된 함수들은 의도를 명확히 전달함으로써 가독성을 향상시킵니다.
보다 간결한 함수형 파이프라인을 원한다면, 커리된 헬퍼들을 익명 함수(또는 메서드 레퍼런스)로 교체하고 reduce 대신 sum을 사용할 수 있습니다:

val totalPrice = products
  .map(_.price * 0.9)   // apply discount
  .filter(_ > 20)       // keep only prices > 20
  .sum                  // total

공간 복잡도

단순한 함수형 파이프라인을 사용할 때, 각 변환은 중간 컬렉션을 생성합니다:

  1. map → 크기가 n인 컬렉션
  2. filter → 크기가 ≤ n인 컬렉션
  3. sum은 필터링된 컬렉션을 소비합니다

따라서 단순한 접근 방식은 O(n) 추가 공간을 사용합니다 (중간 컬렉션).
실제로, 많은 Scala 컬렉션(예: view, Iterator)은 이러한 연산들을 결합하여 중간 컬렉션을 실체화하지 않고, O(1) 추가 공간을 달성할 수 있습니다.

요약

  • 시간 복잡도: 두 버전 모두 Θ(n)입니다. 상수 계수 차이는 보통 미미합니다.
  • 공간 사용량: 순진한 함수형 파이프라인은 중간 컬렉션을 할당하지만, 결합/스트리밍 버전은 그 오버헤드를 없앱니다.
  • 가독성: 함수형 코드는 특히 잘 이름 붙여진 헬퍼 함수와 함께 사용할 때 더 선언적이고 표현력이 풍부할 수 있습니다.

두 스타일 모두 각각의 역할이 있습니다; 프로젝트의 성능 제약과 가독성 목표에 가장 잘 맞는 방식을 선택하세요.

스칼라 컬렉션에서의 지연 평가

어쨌든, 중첩 변환을 수행하지 않는 한 입력 크기에 비례하여 공간이 선형적으로 증가하고, 파이프라인 단계 수에 따라 더 악화될 수 있는 해결책을 갖게 됩니다.

이 시점에 도달하면 기능적 코드에 메서드 호출을 포함시켜 질문을 최종적으로 해결할 수 있습니다:

val totalPrice = products.view
  .map(_.price * 0.9)
  .filter(_ > 20)
  .sum

.view 메서드가 보이시나요? 이것이 Views를 사용해 컬렉션에서 지연 평가로 전환하는 스칼라 방식입니다.

엄격한 평가—모든 변환이 즉시 적용되어 새로운 구조를 생성하는 방식—와 달리, 지연 평가는 계산이 실제로 필요할 때까지 연산을 미룹니다.

  • Mapfilter는 실제 연산을 수행하지 않고 다른 View를 반환합니다(기저 view를 감싸고 값을 계산하는 클로저를 기억합니다).
  • sum을 호출해 요소에 접근해 결과를 만들어야 할 때 비로소 실제 연산이 수행됩니다.

지연 평가 덕분에 원본 입력을 한 번만 순회하고 중간 구조를 전혀 만들지 않으므로, 이 기능적 코드는 처음에 본 단일 루프 명령형 코드와 실질적으로 동일하게 동작합니다.

트레이드‑오프

지연 평가는 비용이 없습니다:

  • 중간 view 구조를 생성합니다.
  • 중간 클로저를 생성하고 저장합니다.

입력 크기가 작을 경우, 이러한 오버헤드가 “루프”를 여러 번 수행하고 중간 컬렉션을 만드는 것보다 더 비용이 많이 들 수 있습니다.

Languages and Evaluation Strategies

  • Haskell – 기본적으로 지연(lazy, 비‑엄격) 평가.
  • Scala – 기본적으로 엄격(strict) 평가이지만, .view와 같은 메커니즘을 사용해 지연 평가로 전환할 수 있음.

시니어리티, 코드 길이, 그리고 최적화

이 글의 서두에서 말했듯이, 시니어리티는 종종 다음과 같은 것처럼 보입니다:

  • 직접 비례하는 요소는 당신이 알고 있는 언어 기능의 수입니다.
  • 역비례하는 요소는 당신이 생성하는 코드의 길이입니다.

또한 코드의 마이크로 최적화를 할 수 있는 능력과도 연결된 것처럼 보이며, O(n) 솔루션을 O(3n)으로 바꾸는 것과 같습니다(다시 농담입니다).

비즈니스 환경에서 실제로 중요한 것은?

가치 있는 개발자는 소프트웨어 제품의 총 소유 비용을 낮추는 사람이며, 즉 회사의 비용을 절감하는 사람입니다.

우리의 노력을 어디에 집중해야 할까?

업계 데이터에 따르면 (O’Reilly 저자의 인용):

  • 소프트웨어 수명 주기 비용의 **60 %**는 유지보수에서 발생합니다.
  • 유지보수 중, 비용의 **60 %**는 사용자 생성 개선(요구사항 변경)과 관련됩니다.
  • **17 %**의 유지보수 비용은 버그 수정 때문입니다.

따라서 유지보수 비용은 운영 비용(소프트웨어를 실행하는 데 필요한 인프라)보다 훨씬 큽니다. 당신이 구글처럼 성능 향상이 수백만 대의 기계에 영향을 미치는 경우가 아니라면, 다음을 우선시해야 합니다:

  1. 유지보수성
  2. 버그 없는 코드

이는 미미한 성능 향상을 위한 마이크로 최적화를 중단하고, 대신 동료 개발자와 미래의 자신이 읽고 발전시키는 데 즐거움을 느낄 수 있는 아름다운 이야기를 전달하는 코드를 작성하는 데 집중한다는 의미입니다. 함수형 프로그래밍이 여기서 종종 장점을 가지고 있지만, 이는 다음 글의 주제입니다.

“항상 코드를 작성하라, 마치 당신의 코드를 유지보수하게 될 사람이 당신이 사는 곳을 아는 폭력적인 사이코패스라고 생각하듯이.”
Martin Golding (기사에서 인용)

0 조회
Back to Blog

관련 글

더 보기 »

왜 나는 행복했을 때도 인터뷰했는가

배경: 나는 정말 좋아하는 회사에서 일하고 있었습니다. 훌륭한 매니저와 지원적인 팀이 있었고, 일을 즐겼으며, 엔지니어로서 빠르게 성장하고 있다고 느꼈습니다. I s...