TBEG을 실제 프로젝트에 도입하면 다양한 상황을 마주하게 됩니다. 이 글에서는 실전에서 자주 겪는 문제와 해결법, 그리고 더 나은 결과를 위한 팁을 정리합니다.
${repeat(...)} 마커는 워크북 내 어디든 배치할 수 있습니다. 데이터 영역 위의 행에 배치하면 가독성이 좋아집니다.
| A | B | C | |
|---|---|---|---|
| 1 | ${repeat(employees, A2:C2, emp)} | ||
| 2 | ${emp.name} | ${emp.position} | ${emp.salary} |
수식의 범위는 자동으로 확장되므로 위치는 자유입니다. 다만 데이터 → 집계 순서가 자연스러워 Excel 편집 시 가독성이 좋습니다.
| A | B | C | |
|---|---|---|---|
| 1 | ${repeat(items, A2:C2, item)} | ||
| 2 | ${item.name} | ${item.value} | ${item.qty} |
| 3 | 합계 | =SUM(B2:B2) | =AVERAGE(C2:C2) |
SUM, AVERAGE, COUNT, MAX, MIN 등 범위를 참조하는 모든 수식이 자동 조정됩니다.
1행 1데이터가 가장 직관적입니다. 복잡한 레이아웃이 필요하면 다중 행 반복(A2:B3)을 사용하세요.
${hideable(emp.salary, C1:C3, dim)}
bundle 범위로 타이틀, 합계까지 함께 관리DIM 모드: 레이아웃 유지 + 비활성화 처리DELETE 모드(기본): 열을 물리적으로 삭제같은 시트에 여러 repeat 영역을 배치할 때, 열 범위가 겹치면 오류가 발생합니다. 열을 분리하거나 행을 분리하여 배치하세요.
상세 가이드: 모범 사례 전문
데이터 크기에 따라 단계적으로 적용하세요.
컬렉션의 전체 건수를 함께 제공하면 데이터 이중 순회를 방지합니다.
val count = employeeRepository.count().toInt()
val provider = simpleDataProvider {
items("employees", count) {
employeeRepository.findAll().iterator()
}
}
모든 데이터를 미리 메모리에 로드하지 말고, Lambda로 필요한 시점에 로드합니다.
// 비권장: 모든 데이터를 미리 로드
val allEmployees = employeeRepository.findAll()
items("employees", allEmployees)
// 권장: 지연 로딩
items("employees", count) {
employeeRepository.findAll().iterator()
}
10만 행 이상의 대용량 데이터는 JPA Stream 또는 MyBatis Cursor를 사용합니다.
@Transactional(readOnly = true)
fun generateLargeReport(): Path {
val count = employeeRepository.count().toInt()
val provider = simpleDataProvider {
items("employees", count) {
employeeRepository.streamAll().iterator()
}
}
return excelGenerator.generateToFile(
template = template,
dataProvider = provider,
outputDir = outputDir,
baseFileName = "large_report"
)
}
| 데이터 크기 | 예상 생성 시간 | 권장 방식 |
|---|---|---|
| ~1,000행 | ~20ms | Map 방식으로 충분 |
| ~10,000행 | ~110ms | simpleDataProvider + count |
| ~50,000행 | ~500ms | + DB Stream |
| ~100,000행 | ~1초 | + DB Stream |
| ~500,000행 | ~5초 | 커스텀 DataProvider + DB Stream + generateToFile() |
| ~1,000,000행 | ~9초 | 커스텀 DataProvider + DB Stream + generateToFile() |
3개 컬럼 repeat + SUM 수식 기준(DataProvider + generateToFile). 컬럼 수, 수식 복잡도, 서버 사양에 따라 달라질 수 있습니다.
${title}, ${emp.name} 등이 치환되지 않고 출력되는 경우:
| 원인 | 해결 |
|---|---|
| 데이터에 해당 키가 없음 | data 맵 또는 DataProvider에 변수를 추가 |
| 변수명 오타 | 템플릿의 마커명과 데이터의 키를 정확히 일치 |
| repeat 변수가 범위 밖에서 사용됨 | ${emp.name} 등은 repeat 범위 안에서만 유효 |
팁:
TbegConfig(missingDataBehavior = MissingDataBehavior.THROW)로 설정하면 누락된 데이터에 대해 예외가 발생하여 원인 파악이 쉬워집니다.
TemplateProcessingException템플릿 파싱 중 문법 오류가 발견되면 발생합니다.
| ErrorType | 원인 | 해결 |
|---|---|---|
INVALID_MARKER_SYNTAX | 마커 문법 오류 | 괄호, 파라미터 형식 확인 |
MISSING_REQUIRED_PARAMETER | 필수 파라미터 누락 | repeat의 collection, range 등 확인 |
INVALID_RANGE_FORMAT | 잘못된 셀 범위 | A2:C2 같은 올바른 범위 사용 |
RANGE_CONFLICT | 범위 충돌 (중첩, 경계 걸침) | 겹치는 범위를 분리하거나 완전 포함으로 조정 |
OutOfMemoryError대용량 데이터 처리 시 JVM 메모리가 부족하면 발생합니다.
-Xmx2g피벗 테이블이 포함된 템플릿은 재생성 과정에서 결과 파일 전체를 메모리에 로드하므로 약 30만 행이 현실적 상한입니다.
merge는 연속된 같은 값만 병합합니다. 같은 값이 떨어져 있으면 별도의 병합 그룹이 됩니다.
// 정렬 전: [영업1팀, 영업2팀, 영업1팀] -> 영업1팀이 2개 그룹으로 분리됨
// 정렬 후: [영업1팀, 영업1팀, 영업2팀] -> 영업1팀이 하나로 병합됨
val employees = employeeRepository.findAll().sortedBy { it.department }
확인할 포인트 3가지:
hideFields("employees", "salary")의 필드명이 템플릿 마커(${emp.salary})와 일치해야 합니다.hideable 마커에 bundle 파라미터가 지정되지 않은 경우입니다.
${hideable(emp.salary, C1:C4)}
C1:C4 범위에 필드 타이틀(C1), 데이터(C2~C3), 합계(C4)를 모두 포함하면 함께 처리됩니다.
| 데이터 규모 | 권장 -Xmx | 비고 |
|---|---|---|
| 1만 행 이하 | 512MB | 대부분의 일반 보고서 |
| 10만 행 | 1~2GB | |
| 50만 행 | 4GB | |
| 100만 행 | 8GB |
동시에 여러 보고서를 생성하는 경우, 위 값에 동시 요청 수를 곱하여 산정하세요.
ExcelGenerator는 스레드 안전하므로 싱글톤 빈으로 사용하세요.
@Configuration
class TbegConfig {
@Bean
fun excelGenerator() = ExcelGenerator()
}
# application-dev.yml
tbeg:
missing-data-behavior: throw
# application-prod.yml
tbeg:
missing-data-behavior: warn
개발 환경에서는 THROW로 누락 데이터를 즉시 감지하고, 운영 환경에서는 WARN으로 경고만 출력합니다.
JPA Stream은 트랜잭션이 종료되면 닫힙니다. Excel 생성이 완료될 때까지 트랜잭션이 유지되어야 합니다.
@Transactional(readOnly = true) // 필수!
fun generateReport(): ByteArray {
val provider = simpleDataProvider {
items("employees", count) {
employeeRepository.streamAll().iterator()
}
}
return excelGenerator.generate(template, provider)
}
@Transactional이 없으면LazyInitializationException이 발생할 수 있습니다.
이 글에서 다룬 내용을 요약하면:
MissingDataBehavior.THROW로 개발 중 즉시 감지상세한 내용은 GitHub 문서를 참조하세요:
GitHub: jogakdal/data-processors-with-excel
Maven Central:io.github.jogakdal:tbeg:1.2.3