Skip to content

Commit 608cb98

Browse files
authored
Merge pull request #214 from FunD-StockProject/fix/stock-master-info
Fix 종목 마스터 데이터 업데이트 오류 수정9
2 parents c0fa0d7 + 4a56ff1 commit 608cb98

File tree

2 files changed

+107
-61
lines changed

2 files changed

+107
-61
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
-- Hibernate 시퀀스 테이블 확인 및 삭제 스크립트
2+
-- 이 스크립트는 MySQL에서 hibernate_sequences 테이블이 존재하는지 확인하고 삭제합니다.
3+
4+
-- ============================================
5+
-- 1단계: 시퀀스 테이블 확인
6+
-- ============================================
7+
SELECT
8+
TABLE_NAME,
9+
TABLE_TYPE
10+
FROM
11+
INFORMATION_SCHEMA.TABLES
12+
WHERE
13+
TABLE_SCHEMA = DATABASE()
14+
AND (TABLE_NAME = 'hibernate_sequences'
15+
OR TABLE_NAME = 'stock_sequence'
16+
OR TABLE_NAME LIKE '%_sequence%');
17+
18+
-- ============================================
19+
-- 2단계: 시퀀스 테이블 삭제 (확인 후 실행)
20+
-- 주의: 이 명령을 실행하기 전에 반드시 백업을 수행하세요!
21+
-- ============================================
22+
-- hibernate_sequences 테이블 삭제
23+
DROP TABLE IF EXISTS hibernate_sequences;
24+
25+
-- stock_sequence 테이블 삭제 (만약 있다면)
26+
DROP TABLE IF EXISTS stock_sequence;
27+
28+
-- ============================================
29+
-- 3단계: stock 테이블의 AUTO_INCREMENT 확인
30+
-- ============================================
31+
-- stock 테이블 구조 확인 (AUTO_INCREMENT가 설정되어 있는지 확인)
32+
SHOW CREATE TABLE stock;
33+
34+
-- stock 테이블의 현재 AUTO_INCREMENT 값 확인
35+
SELECT
36+
AUTO_INCREMENT
37+
FROM
38+
INFORMATION_SCHEMA.TABLES
39+
WHERE
40+
TABLE_SCHEMA = DATABASE()
41+
AND TABLE_NAME = 'stock';
42+
43+
-- ============================================
44+
-- 4단계: stock 테이블의 ID 컬럼이 AUTO_INCREMENT인지 확인
45+
-- ============================================
46+
SELECT
47+
COLUMN_NAME,
48+
COLUMN_TYPE,
49+
EXTRA
50+
FROM
51+
INFORMATION_SCHEMA.COLUMNS
52+
WHERE
53+
TABLE_SCHEMA = DATABASE()
54+
AND TABLE_NAME = 'stock'
55+
AND COLUMN_NAME = 'id';
56+

src/main/java/com/fund/stockProject/stock/service/StockImportService.java

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public class StockImportService {
4040
* 실험 등에서 사용된 종목은 보존합니다.
4141
* @param jsonFilePath JSON 파일 경로
4242
*/
43-
@Transactional
4443
public void importStocksFromJson(String jsonFilePath) {
4544
try {
4645
File file = new File(jsonFilePath);
@@ -144,69 +143,38 @@ public void importStocksFromJson(String jsonFilePath) {
144143
}
145144

146145
// 모든 종목을 symbol 기준으로 UPSERT 처리
147-
// 배치 처리로 시퀀스 접근 최소화 (성능 향상 및 락 경합 감소)
146+
// 작은 트랜잭션으로 분리하여 락 경합 최소화
148147
if (!stocksToSaveMap.isEmpty()) {
149148
int processedCount = 0;
150149
int updatedCount = 0;
151150
int insertedCount = 0;
152151
int errorCount = 0;
153152

154-
// 배치 크기 설정 (시퀀스 접근 빈도 조절)
153+
// 배치 크기 설정 (작은 트랜잭션으로 묶어서 처리)
155154
final int BATCH_SIZE = 50;
156155
List<Stock> batch = new ArrayList<>();
157156

158157
for (Stock stock : stocksToSaveMap.values()) {
159-
try {
160-
// 항상 DB에서 symbol로 확인 (가장 확실한 방법)
161-
Optional<Stock> existingStockOpt = stockRepository.findBySymbol(stock.getSymbol());
162-
163-
if (existingStockOpt.isPresent()) {
164-
// 기존 종목 업데이트 - 즉시 처리
165-
Stock existing = existingStockOpt.get();
166-
existing.updateSymbolNameIfNull(stock.getSymbolName());
167-
existing.setValid(true);
168-
if (stock.getDomesticSector() != null) {
169-
existing.setDomesticSector(stock.getDomesticSector());
170-
}
171-
if (stock.getOverseasSector() != null) {
172-
existing.setOverseasSector(stock.getOverseasSector());
173-
}
174-
stockRepository.save(existing);
175-
updatedCount++;
176-
processedCount++;
177-
} else {
178-
// 새 종목은 배치에 추가
179-
batch.add(stock);
180-
181-
// 배치가 가득 차면 일괄 처리
182-
if (batch.size() >= BATCH_SIZE) {
183-
BatchResult batchResult = processBatch(batch);
184-
insertedCount += batchResult.inserted;
185-
updatedCount += batchResult.updated;
186-
errorCount += batchResult.errors;
187-
processedCount += batch.size();
188-
batch.clear();
189-
190-
// 배치 처리 후 영속성 컨텍스트 정리
191-
entityManager.flush();
192-
entityManager.clear();
193-
}
194-
}
195-
} catch (Exception e) {
196-
errorCount++;
197-
log.error("Unexpected error processing stock: {}", stock.getSymbol(), e);
158+
batch.add(stock);
159+
160+
// 배치가 가득 차면 작은 트랜잭션으로 일괄 처리
161+
if (batch.size() >= BATCH_SIZE) {
162+
UpsertResult batchResult = processBatchInTransaction(batch);
163+
insertedCount += batchResult.inserted;
164+
updatedCount += batchResult.updated;
165+
errorCount += batchResult.errors;
166+
processedCount += batch.size();
167+
batch.clear();
198168
}
199169
}
200170

201171
// 남은 배치 처리
202172
if (!batch.isEmpty()) {
203-
BatchResult batchResult = processBatch(batch);
173+
UpsertResult batchResult = processBatchInTransaction(batch);
204174
insertedCount += batchResult.inserted;
205175
updatedCount += batchResult.updated;
206176
errorCount += batchResult.errors;
207177
processedCount += batch.size();
208-
entityManager.flush();
209-
entityManager.clear();
210178
}
211179

212180
log.info("Processed {} stocks - Inserted: {}, Updated: {}, Errors: {}",
@@ -410,57 +378,79 @@ private String getRootCauseMessage(Exception e) {
410378
}
411379

412380
/**
413-
* 배치 처리 결과를 담는 내부 클래스
381+
* UPSERT 결과를 담는 내부 클래스
414382
*/
415-
private static class BatchResult {
383+
private static class UpsertResult {
416384
int inserted = 0;
417385
int updated = 0;
418386
int errors = 0;
419387
}
420388

421389
/**
422-
* 배치로 새 종목을 저장합니다.
423-
* 중복 키 오류가 발생하면 개별 처리합니다.
424-
* @return 배치 처리 결과 (inserted, updated, errors)
390+
* 작은 트랜잭션으로 배치를 처리합니다.
391+
* 각 배치는 독립적인 트랜잭션으로 처리되어 락 경합을 최소화합니다.
425392
*/
426-
private BatchResult processBatch(List<Stock> batch) {
427-
BatchResult result = new BatchResult();
393+
@Transactional
394+
private UpsertResult processBatchInTransaction(List<Stock> batch) {
395+
UpsertResult result = new UpsertResult();
428396

429397
for (Stock stock : batch) {
430398
try {
431-
stockRepository.save(stock);
432-
result.inserted++;
399+
// symbol로 기존 종목 확인
400+
Optional<Stock> existingOpt = stockRepository.findBySymbol(stock.getSymbol());
401+
402+
if (existingOpt.isPresent()) {
403+
// 기존 종목 업데이트
404+
Stock existing = existingOpt.get();
405+
existing.updateSymbolNameIfNull(stock.getSymbolName());
406+
existing.setValid(true);
407+
if (stock.getDomesticSector() != null) {
408+
existing.setDomesticSector(stock.getDomesticSector());
409+
}
410+
if (stock.getOverseasSector() != null) {
411+
existing.setOverseasSector(stock.getOverseasSector());
412+
}
413+
stockRepository.save(existing);
414+
result.updated++;
415+
} else {
416+
// 새 종목 삽입 (IDENTITY 전략 사용)
417+
stockRepository.save(stock);
418+
result.inserted++;
419+
}
433420
} catch (DataIntegrityViolationException e) {
434421
// 중복 키 오류 발생 시 기존 종목으로 처리
435422
entityManager.clear();
436423
if (handleDuplicateKeyError(stock, e)) {
437424
result.updated++;
438-
log.debug("Updated stock after duplicate key error in batch: {}", stock.getSymbol());
439425
} else {
440426
result.errors++;
441-
log.warn("Failed to handle duplicate key error for stock in batch: {}", stock.getSymbol());
427+
log.warn("Failed to handle duplicate key error for stock: {}", stock.getSymbol());
442428
}
443429
} catch (Exception e) {
444-
// 예외 메시지에 "Duplicate entry" 또는 "Lock wait timeout"이 포함되어 있는지 확인
445430
String errorMessage = getRootCauseMessage(e);
446-
if (errorMessage != null && (errorMessage.contains("Duplicate entry") || errorMessage.contains("Lock wait timeout"))) {
431+
if (errorMessage != null && (errorMessage.contains("Duplicate entry") ||
432+
errorMessage.contains("Lock wait timeout") ||
433+
errorMessage.contains("could not read a hi value"))) {
447434
entityManager.clear();
448435
if (handleDuplicateKeyError(stock, e)) {
449436
result.updated++;
450-
log.debug("Updated stock after error in batch: {}", stock.getSymbol());
451437
} else {
452438
result.errors++;
453-
log.warn("Failed to handle error for stock in batch: {} - {}", stock.getSymbol(), errorMessage);
439+
log.warn("Failed to handle error for stock: {} - {}", stock.getSymbol(), errorMessage);
454440
}
455441
} else {
456442
result.errors++;
457-
log.warn("Failed to save stock in batch: {} - {}", stock.getSymbol(), errorMessage);
443+
log.warn("Failed to save stock: {} - {}", stock.getSymbol(), errorMessage);
458444
}
459445
}
460446
}
461447

448+
// 트랜잭션 내에서 flush하여 ID 생성 확정
449+
entityManager.flush();
450+
462451
return result;
463452
}
453+
464454

465455
}
466456

0 commit comments

Comments
 (0)