iOS/NumberterKit

[NumberterKit] 성능 테스트 결과

Dev_Ted 2025. 5. 16. 10:27

검증

NumberterKit이 그럼 기존의 Race Condition 문제를 해결했는지와 실제로 성능은 어느정도 개선되었는지에 대해 분석하였습니다.

 

 

테스트 케이스를 통한 검증

앞서 수동 테스트로 동작을 확인했지만,
신뢰할 수 있는 결과를 보장하기 위해 XCTestCase를 통한 자동화 테스트도 함께 진행하였습니다.

다음과 같이 Race Condition 재현 테스트테스트 케이스로 등록하여,
매번 일관된 결과가 출력되는지 검증했습니다.

import XCTest

final class FormatterProviderTests: XCTestCase {

    func testDecimalFormatterRaceSafe() {
        let number = NSNumber(value: 1234.5678)
        let expectedResults = [
            0: "1,235",
            1: "1,234.57"
        ]

        let iterations = 20

        DispatchQueue.concurrentPerform(iterations: iterations) { i in
            let digits = (i % 2 == 0) ? 0 : 2
            let formatted = FormatterProvider.decimal(fractionDigits: digits)
                .string(from: number)

            XCTAssertEqual(formatted, expectedResults[i % 2])
        }
    }
}

 

테스트 결과, 모든 스레드에서 요청한 자리수에 맞는 일관된 출력이 검증되었습니다.

 

 

 

성능 테스트

다음으론 SpinLock 기반 공유 Formatter와  매번 새 인스턴스를 생성하는 방식성능 차이를 테스트해 보았습니다.

 

테스트 환경

  1. 싱글 스레드 10,000회 호출
  2. 멀티 스레드 (10개의 스레드 × 1,000회 호출)

두 가지 환경에서

  • FormatterProvider.decimal(fractionDigits:) (SpinLock 기반 공유 인스턴스)
  • FormatterProvider.newDecimal(fractionDigits:) (매번 새 인스턴스 생성)

을 비교하였습니다.

 

 

다음은 테스트 환경에 대한 정보입니다.

테스트 기기 MacBook Pro (Apple Silicon M1)
테스트 도구 Xcode XCTest.measure
테스트 횟수 기본 10회 반복 측정
성능 지표 평균 실행 시간 (Wall Clock Time)
비교 대상 공유 인스턴스 방식 vs 매번 새 인스턴스 생성 방식

 

measure란 해당 메서드를 10번 실행한 후 평균 실행 시간을 알려주는 메서드입니다.

 

 

싱글 스레드 테스트 결과

 

먼저 싱글 스레드에서 10,000번을 호출했을 때의 성능 결과입니다.

    // MARK: 싱글 스레드
    /// NumberFormatter 생성
    func testFormatterCreationPerformance() {
        measure {
            for _ in 0..<10_000 {
                let formatter = NumberFormatter()
                _ = formatter.string(from: 1234.5678)
            }
        }
    }
    /// 공유 인스턴스
    func testSharedFormatterWithSpinLockPerformance() {
        let number = NSNumber(value: 1234.5678)

        measure {
            for _ in 0..<10_000 {
                _ = FormatterProvider.decimal(fractionDigits: 2).string(from: number)
            }
        }
    }

 

SpinLock 기반 공유 Formatter 0.0796 (Sec)
매번 새 인스턴스 생성 0.327 (sec)

 

제가 구현한 SpinLock 기반 공유 방식이 약 4배 이상 빠른 성능을 보여주었습니다.

 

 

 

 

멀티 스레드 테스트 결과

 

다음은 10개의 멀티 스레드에서 각 1,000회씩 호출했을 때의 성능 결과입니다.

    // MARK: 멀티 스레드
    /// NumberFormatter 생성
    func testNewFormatterCreationPerformanceMultiThread() {
        let number = NSNumber(value: 1234.5678)
        let iterations = 10_000

        measure {
            DispatchQueue.concurrentPerform(iterations: 10) { _ in
                for _ in 0..<(iterations / 10) {
                    let formatter = NumberFormatter()
                    formatter.numberStyle = .decimal
                    formatter.minimumFractionDigits = 2
                    formatter.maximumFractionDigits = 2
                    formatter.groupingSeparator = ","
                    formatter.locale = Locale(identifier: "ko_KR")

                    _ = formatter.string(from: number)
                }
            }
        }
    }
    /// 공유 인스턴스
    func testSharedFormatterPerformanceMultiThread() {
        let number = NSNumber(value: 1234.5678)
        let iterations = 10_000

        measure {
            DispatchQueue.concurrentPerform(iterations: 10) { _ in
                for _ in 0..<(iterations / 10) {
                    _ = FormatterProvider.decimal(fractionDigits: 2).string(from: number)
                }
            }
        }
    }

 

SpinLock 기반 공유 Formatter 0.122 (Sec)
매번 새 인스턴스 생성 0.265 (Sec)

 

제가 구현한 SpinLock 기반 공유 방식이 약 2배 이상 빠른 성능을 보여주었습니다.

 

 

 

 

정리하며

이번 테스트를 통해 SpinLock 기반 공유 Formatter 방식이 성능적으로도 충분히 유리하다는 것을 확인할 수 있었습니다.

  • 싱글 스레드: 객체 생성 비용 절감으로 최대 4배 성능 향상
  • 멀티 스레드: 락의 오버헤드에도 불구하고 최대 2배 성능 향상

 

이 결과는 락 사용이 무조건 느리다는 일반적인 편견과 달리,
적절한 상황에서는 락이 성능과 안정성 모두를 잡을 수 있다는 가능성을 보여주었습니다.

앞으로도 성능과 구조의 균형을 고민하며 최적의 설계 방안을 찾기 위해 계속 실험해 나가겠습니다.

728x90