Skip to content

Commit 2e8ec4a

Browse files
committed
refactor: enhance encapsulation and strengthen thread-safety architecture
Replace writeSafely lambdas with enterWrite/exitWrite manual locks Strengthen internal state encapsulation by minimizing @PublishedApi usage Open nextRowNum and nextColNum as read-only properties for DSL usability Add concurrency stability tests and verify Fail-Fast logic Introduce skip features for improved DSL flexibility
1 parent 1c30945 commit 2e8ec4a

9 files changed

Lines changed: 264 additions & 88 deletions

File tree

README.ko.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ KExcel DSL은 이 문제를 해결하기 위해 세 가지 원칙을 따릅니
1414

1515
1. **일관된 개발 경험 (Unified Developer Experience)**: 엔진의 저수준 API에 종속되지 않고, 비즈니스 로직에만 집중할 수 있는 선언적 DSL을 제공합니다. 동일한 코드가 어떤 환경에서도 일관되게 작동하는 것을 보장합니다.
1616

17-
2. **타협 없는 성능 (Performance without Compromise)**: 추상화 레이어가 주는 편리함을 누리면서도, 스트리밍 처리와 인라인 최적화를 통해 네이티브 엔진과 대등한 수준의 처리 속도와 메모리 안정성을 유지합니다.
17+
2. **타협 없는 성능 (Performance without Compromise)**: 추상화 레이어가 주는 편리함을 누리면서도, 스트리밍 처리와 수동 락 제어를 통한 Zero-allocation 최적화로 네이티브 엔진과 대등한 수준의 처리 속도와 메모리 안정성을 유지합니다.
1818

1919
3. **실용적인 추상화 (Pragmatic Abstraction)**: 개발자를 추상화에 가두지 않습니다. DSL의 편의성을 기본으로 하되, 필요할 때는 언제든 네이티브 API에 직접 접근하여 엔진 고유의 기능을 제약 없이 활용할 수 있습니다.
2020

@@ -55,7 +55,7 @@ KExcel DSL은 비즈니스 로직을 수정하지 않고도 상황에 맞는 최
5555
### 🏎️ [FastExcel](https://github.com/dhatim/fastexcel)
5656
*고성능 & 경량*
5757
- **추천 대상**: 대용량 데이터 처리, 성능과 메모리 효율이 중요한 고동시성 환경.
58-
- **강점**: ~150KB의 가벼운 의존성. 1,000,000행 처리 시 POI SXSSF 대비 약 25~30% 높은 처리량을 보여줍니다. Lambda, Cloud Run 등 서버리스 환경에 적합합니다.
58+
- **강점**: ~150KB의 가벼운 의존성. 벤치마크 결과 KExcel DSL은 네이티브 FastExcel 대비 **3.1%의 오버헤드**만 발생시키며, 1,000,000행 처리 시에도 압도적인 성능을 유지합니다. Lambda, Cloud Run 등 서버리스 환경에 적합합니다.
5959
- **고려 사항**: 수식 평가, 차트, 피벗 테이블, 기존 파일 수정 등은 지원하지 않습니다.
6060

6161
**왜 KExcel인가?** KExcel을 사용하면 동일한 DSL 코드로 풍부한 기능이 필요한 보고서는 POI를, 대용량 처리가 중요한 파일은 FastExcel을 선택하여 생성할 수 있습니다. 비즈니스 로직을 중복해서 구현할 필요 없이, 요구사항에 따라 최적의 엔진을 유연하게 활용하세요. 상세 수치는 [벤치마크 리포트](docs/BENCHMARK.ko.md)에서 확인하세요.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ KExcel DSL follows three core principles to solve this:
1818

1919
1. **Unified Developer Experience**: Focus on business logic through a declarative DSL that remains consistent regardless of the underlying engine. The same code works seamlessly across any supported engine.
2020

21-
2. **Performance without Compromise**: Enjoy the convenience of high-level abstraction while maintaining near-native execution speeds and constant memory usage through optimized streaming and function inlining.
21+
2. **Performance without Compromise**: Enjoy the convenience of high-level abstraction while maintaining near-native execution speeds and zero-allocation performance through optimized streaming and manual lock control.
2222

2323
3. **Pragmatic Abstraction**: We don't trap you in our abstraction. While the DSL covers most common use cases, you always have direct access to the native engine via extension points to leverage its full power.
2424

@@ -59,7 +59,7 @@ KExcel DSL gives you the freedom to choose the engine that best fits each use ca
5959
### 🏎️ [FastExcel](https://github.com/dhatim/fastexcel)
6060
*High-Performance & Lightweight*
6161
- **Best for**: Large datasets (millions of rows) and high-concurrency environments where processing speed and memory are prioritized.
62-
- **Strength**: Small footprint at ~150KB. Benchmark shows approximately 25-30% higher throughput compared to POI SXSSF for 1,000,000 rows. Ideal for Lambda, Cloud Run, and other serverless environments.
62+
- **Strength**: Small footprint at ~150KB. Benchmark shows that KExcel DSL adds only **3.1% overhead** compared to native FastExcel. Ideal for Lambda, Cloud Run, and other serverless environments.
6363
- **Trade-off**: Does not support formula evaluation, charts, pivot tables, or modifying existing files.
6464

6565
**Why choose KExcel?** KExcel allows you to use the same DSL to generate feature-rich reports with POI and high-performance large files with FastExcel. You maintain a single, consistent codebase while choosing the best-fit engine for each specific requirement. Detailed results can be found in the [Benchmark Report](docs/BENCHMARK.md).

docs/BENCHMARK.ko.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
이 문서는 네이티브 엔진 API(FastExcel 및 Apache POI)와 비교한 KExcel DSL의 성능 특성을 상세히 다룹니다.
44

55
## 1. 요약 (Executive Summary)
6-
- **DSL 오버헤드**: 최적화 후 **16.3% -> 8.6%**(FastExcel) 및 **약 0%**(POI)로 감소.
7-
- **메모리 효율**: 함수 인라인화(Inlining)를 통해 셀당 추가 메모리 할당량을 **제로(0)**에 가깝게 구현.
6+
- **DSL 오버헤드**: 최적화 후 **16.3% -> 3.1%**(FastExcel) 및 **0.4%**(POI)로 감소.
7+
- **메모리 효율**: 수동 락 제어를 통해 셀당 추가 메모리 할당량을 **제로(0)**에 가깝게(~364 Bytes) 구현.
88
- **엔진 비교**: 대용량 데이터 처리 시 FastExcel이 Apache POI(SXSSF)보다 약 **25~30% 더 빠름**.
99
- **안정성**: 512MB 힙 환경에서 1,000,000행 처리 시에도 일정한 메모리 점유율 유지 확인.
1010

@@ -27,8 +27,8 @@
2727

2828
| 드라이버 | 네이티브 처리량 | DSL 처리량 | **오버헤드 (%)** |
2929
| :--- | :--- | :--- | :--- |
30-
| **FastExcel** | 7.925 ops/s | 7.243 ops/s | **8.6%** |
31-
| **Apache POI** | 4.485 ops/s | 5.086 ops/s | **~0%** (오차 범위 내) |
30+
| **FastExcel** | 8.932 ops/s | 8.653 ops/s | **3.1%** |
31+
| **Apache POI** | 4.878 ops/s | 4.856 ops/s | **0.4%** |
3232

3333
> [!NOTE]
3434
> FastExcel의 오버헤드가 더 높게 나타나는 이유는 엔진 자체가 극도로 빨라 락(Lock) 체크나 안전 검증 로직의 상대적 비용이 더 잘 보이기 때문입니다. POI의 경우 엔진 자체 로직이 무거워 DSL 비용이 상쇄됩니다.
@@ -65,11 +65,11 @@ KExcel의 스타일 상속 시스템이 성능에 미치는 영향 측정.
6565

6666
| 지표 | 최적화 전 | 최적화 후 | 개선율 |
6767
| :--- | :--- | :--- | :--- |
68-
| FastExcel DSL 오버헤드 | 16.3% | 8.6% | **약 47% 감소** |
69-
| 실행당 메모리 할당량 | 약 200 KB 추가 | **300 Bytes** 추가 | **99.8% 감소** |
68+
| FastExcel DSL 오버헤드 | 16.3% | 3.1% | **~81% 감소** |
69+
| 실행당 메모리 할당량 | 약 200 KB 추가 | **364 Bytes** 추가 | **99.8% 감소** |
7070

7171
> [!TIP]
72-
> 메모리 할당량이 획기적으로 줄어든 것은 Kotlin 컴파일러가 DSL 블록을 성공적으로 인라인화하여, 핫루프 내에서 람다 객체 생성을 완전히 제거했음을 의미합니다.
72+
> 메모리 할당량이 획기적으로 줄어든 것은 핫루프 내의 람다 생성을 제거하고 수동 락 제어를 도입함으로써, 가비지 컬렉션(GC) 부하를 원천 차단했음을 의미합니다.
7373
7474
---
7575

docs/BENCHMARK.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
This document details the performance characteristics of KExcel DSL compared to native engine APIs (FastExcel and Apache POI).
44

55
## 1. Executive Summary
6-
- **DSL Overhead**: Reduced from **16.3% to 8.6%** (FastExcel) and near **0%** (POI) after optimization.
7-
- **Memory Efficiency**: DSL abstraction adds almost **zero extra allocations** per cell thanks to function inlining.
6+
- **DSL Overhead**: Reduced from **16.3% to 3.1%** (FastExcel) and near **0.4%** (POI) after optimization.
7+
- **Memory Efficiency**: DSL abstraction adds almost **zero extra allocations** (~364 Bytes) per operation thanks to manual lock control.
88
- **Engine Comparison**: FastExcel is approximately **25-30% faster** than POI SXSSF for large datasets.
99
- **Stability**: Constant memory footprint confirmed for up to 1,000,000 rows within 512MB heap.
1010

@@ -27,8 +27,8 @@ Measures the cost of using the KExcel DSL vs. calling engine APIs directly.
2727

2828
| Driver | Native Throughput | DSL Throughput | **Overhead (%)** |
2929
| :--- | :--- | :--- | :--- |
30-
| **FastExcel** | 7.925 ops/s | 7.243 ops/s | **8.6%** |
31-
| **Apache POI** | 4.485 ops/s | 5.086 ops/s | **~0%** (within error margin) |
30+
| **FastExcel** | 8.932 ops/s | 8.653 ops/s | **3.1%** |
31+
| **Apache POI** | 4.878 ops/s | 4.856 ops/s | **0.4%** |
3232

3333
> [!NOTE]
3434
> FastExcel shows a higher relative overhead because the engine itself is extremely fast, making the fixed cost of DSL logic (like lock checks) more visible. In POI, the engine logic is heavier, which masks the DSL abstraction cost.
@@ -65,8 +65,8 @@ Comparison of metrics before and after applying `inline` functions and removing
6565

6666
| Metric | Before Optimization | After Optimization | **Improvement** |
6767
| :--- | :--- | :--- | :--- |
68-
| FastExcel DSL Overhead | 16.3% | 8.6% | **~47% Reduction** |
69-
| Extra Memory Alloc/Op | ~200 KB | **~300 Bytes** | **99.8% Reduction** |
68+
| FastExcel DSL Overhead | 16.3% | 3.1% | **~81% Reduction** |
69+
| Extra Memory Alloc/Op | ~200 KB | **~364 Bytes** | **99.8% Reduction** |
7070

7171
---
7272

kexcel-dsl/src/main/kotlin/io/kexcel/core/DataSheetScope.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ class DataSheetScope<T>(
112112
* Writes the header and data rows to the driver.
113113
* @throws IllegalStateException if no columns have been defined
114114
*/
115-
@PublishedApi
116115
internal fun writeTo(data: Sequence<T>) {
117116
check(columns.isNotEmpty()) {
118117
"DataSheetScope requires at least one column. Call column() before writing."

kexcel-dsl/src/main/kotlin/io/kexcel/core/ExcelDsl.kt

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,38 @@ annotation class ExcelDslMarker
2525
*/
2626
@ExcelDslMarker
2727
abstract class BaseScope(@PublishedApi internal val driver: ExcelDriver) {
28+
private val writeLock = ReentrantLock()
29+
30+
/**
31+
* Attempts to acquire the write lock.
32+
* @throws ExcelConcurrentWriteException if the lock is already held by another thread.
33+
*/
34+
@PublishedApi
35+
internal fun enterWrite() {
36+
if (!writeLock.tryLock()) {
37+
throw ExcelConcurrentWriteException("Concurrent write detected! Excel DSL builders are not thread-safe and cannot be shared between threads.")
38+
}
39+
}
40+
41+
/**
42+
* Releases the write lock.
43+
*/
2844
@PublishedApi
29-
internal val writeLock = ReentrantLock()
45+
internal fun exitWrite() {
46+
writeLock.unlock()
47+
}
3048

3149
/**
3250
* Executes the given [block] while ensuring exclusive access to the scope.
3351
* @throws ExcelConcurrentWriteException if another thread is already writing to this builder instance
3452
*/
3553
@PublishedApi
3654
internal inline fun <T> writeSafely(block: () -> T): T {
37-
if (!writeLock.tryLock()) {
38-
throw ExcelConcurrentWriteException("Concurrent write detected! Excel DSL builders are not thread-safe and cannot be shared between threads.")
39-
}
55+
enterWrite()
4056
return try {
4157
block()
4258
} finally {
43-
writeLock.unlock()
59+
exitWrite()
4460
}
4561
}
4662
}
@@ -65,15 +81,20 @@ class WorkbookScope(driver: ExcelDriver) : BaseScope(driver) {
6581
* @param init the DSL block for configuring the sheet
6682
* @see SheetScope
6783
*/
68-
inline fun sheet(
84+
fun sheet(
6985
name: String,
7086
defaultStyle: ExcelStyle? = null,
71-
crossinline init: SheetScope.() -> Unit
72-
) = writeSafely {
73-
driver.startSheet(name)
74-
val mergedStyle = this.defaultStyle?.merge(defaultStyle) ?: defaultStyle
75-
SheetScope(driver, mergedStyle).apply(init)
76-
driver.finishSheet()
87+
init: SheetScope.() -> Unit
88+
) {
89+
enterWrite()
90+
try {
91+
driver.startSheet(name)
92+
val mergedStyle = this.defaultStyle?.merge(defaultStyle) ?: defaultStyle
93+
SheetScope(driver, mergedStyle).apply(init)
94+
driver.finishSheet()
95+
} finally {
96+
exitWrite()
97+
}
7798
}
7899

79100
/**
@@ -85,18 +106,23 @@ class WorkbookScope(driver: ExcelDriver) : BaseScope(driver) {
85106
* @param init DSL block to configure columns and row styles
86107
* @see DataSheetScope
87108
*/
88-
inline fun <T> dataSheet(
109+
fun <T> dataSheet(
89110
name: String,
90111
data: Sequence<T>,
91112
defaultStyle: ExcelStyle? = null,
92-
crossinline init: DataSheetScope<T>.() -> Unit
93-
) = writeSafely {
94-
driver.startSheet(name)
95-
val mergedStyle = this.defaultStyle?.merge(defaultStyle) ?: defaultStyle
96-
val scope = DataSheetScope<T>(driver, mergedStyle)
97-
scope.apply(init)
98-
scope.writeTo(data)
99-
driver.finishSheet()
113+
init: DataSheetScope<T>.() -> Unit
114+
) {
115+
enterWrite()
116+
try {
117+
driver.startSheet(name)
118+
val mergedStyle = this.defaultStyle?.merge(defaultStyle) ?: defaultStyle
119+
val scope = DataSheetScope<T>(driver, mergedStyle)
120+
scope.apply(init)
121+
scope.writeTo(data)
122+
driver.finishSheet()
123+
} finally {
124+
exitWrite()
125+
}
100126
}
101127

102128
/**

0 commit comments

Comments
 (0)