Skip to content

Commit a90059b

Browse files
authored
Merge pull request #211 from FunD-StockProject/fix/stock-master-info
Fix 종목 마스터 데이터 업데이트 오류 수정7
2 parents 73444be + f5ae040 commit a90059b

File tree

2 files changed

+79
-105
lines changed

2 files changed

+79
-105
lines changed

src/main/java/com/fund/stockProject/stock/entity/Stock.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import jakarta.persistence.Id;
2222
import jakarta.persistence.OneToMany;
2323
import jakarta.persistence.OrderBy;
24-
import jakarta.persistence.SequenceGenerator;
2524
import lombok.AccessLevel;
2625
import lombok.Getter;
2726
import lombok.NoArgsConstructor;
@@ -31,8 +30,7 @@
3130
@NoArgsConstructor(access = AccessLevel.PROTECTED)
3231
public class Stock extends Core {
3332
@Id
34-
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "stock_seq")
35-
@SequenceGenerator(name = "stock_seq", sequenceName = "stock_sequence", allocationSize = 1)
33+
@GeneratedValue(strategy = GenerationType.IDENTITY)
3634
private Integer id;
3735

3836
@OneToMany(mappedBy = "stock", cascade = CascadeType.ALL, orphanRemoval = true)

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

Lines changed: 78 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import com.fund.stockProject.stock.repository.StockRepository;
1212
import jakarta.persistence.EntityManager;
1313
import jakarta.persistence.PersistenceContext;
14-
import jakarta.persistence.Query;
1514
import lombok.RequiredArgsConstructor;
1615
import lombok.extern.slf4j.Slf4j;
1716
import org.springframework.dao.DataIntegrityViolationException;
@@ -50,8 +49,8 @@ public void importStocksFromJson(String jsonFilePath) {
5049
return;
5150
}
5251

53-
// 시퀀스 동기화: 실제 DB의 최대 ID로 시퀀스 설정
54-
synchronizeSequence();
52+
// IDENTITY 전략을 사용하므로 시퀀스 동기화 불필요
53+
// MySQL AUTO_INCREMENT가 자동으로 ID를 생성하므로 락 경합 없음
5554

5655
List<Map<String, Object>> stocksData = objectMapper.readValue(
5756
file,
@@ -144,24 +143,25 @@ public void importStocksFromJson(String jsonFilePath) {
144143
}
145144
}
146145

147-
// 모든 종목을 symbol 기준으로 UPSERT 처리 (단순하고 안전한 방법)
146+
// 모든 종목을 symbol 기준으로 UPSERT 처리
147+
// 배치 처리로 시퀀스 접근 최소화 (성능 향상 및 락 경합 감소)
148148
if (!stocksToSaveMap.isEmpty()) {
149149
int processedCount = 0;
150150
int updatedCount = 0;
151151
int insertedCount = 0;
152152
int errorCount = 0;
153153

154+
// 배치 크기 설정 (시퀀스 접근 빈도 조절)
155+
final int BATCH_SIZE = 50;
156+
List<Stock> batch = new ArrayList<>();
157+
154158
for (Stock stock : stocksToSaveMap.values()) {
155159
try {
156-
// 이전 작업을 DB에 반영
157-
entityManager.flush();
158-
entityManager.clear();
159-
160160
// 항상 DB에서 symbol로 확인 (가장 확실한 방법)
161161
Optional<Stock> existingStockOpt = stockRepository.findBySymbol(stock.getSymbol());
162162

163163
if (existingStockOpt.isPresent()) {
164-
// 기존 종목 업데이트
164+
// 기존 종목 업데이트 - 즉시 처리
165165
Stock existing = existingStockOpt.get();
166166
existing.updateSymbolNameIfNull(stock.getSymbolName());
167167
existing.setValid(true);
@@ -172,54 +172,43 @@ public void importStocksFromJson(String jsonFilePath) {
172172
existing.setOverseasSector(stock.getOverseasSector());
173173
}
174174
stockRepository.save(existing);
175-
entityManager.flush();
176175
updatedCount++;
177-
log.debug("Updated existing stock: {}", stock.getSymbol());
176+
processedCount++;
178177
} else {
179-
// 새 종목 저장
180-
// ID가 null인 새 엔티티이므로 persist()가 호출됨
181-
// 시퀀스에서 ID를 생성하지만, 혹시 모를 중복을 대비해 예외 처리
182-
try {
183-
stockRepository.save(stock);
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+
// 배치 처리 후 영속성 컨텍스트 정리
184191
entityManager.flush();
185-
entityManager.detach(stock); // 영속성 컨텍스트에서 분리
186-
insertedCount++;
187-
log.debug("Inserted new stock: {}", stock.getSymbol());
188-
} catch (DataIntegrityViolationException e) {
189-
// 중복 키 오류 발생 시 기존 종목으로 처리
190192
entityManager.clear();
191-
if (handleDuplicateKeyError(stock, e)) {
192-
updatedCount++;
193-
log.debug("Updated stock after duplicate key error: {}", stock.getSymbol());
194-
} else {
195-
errorCount++;
196-
log.warn("Failed to handle duplicate key error for stock: {}", stock.getSymbol());
197-
}
198-
} catch (Exception e) {
199-
// 예외 메시지에 "Duplicate entry"가 포함되어 있는지 확인
200-
String errorMessage = getRootCauseMessage(e);
201-
if (errorMessage != null && errorMessage.contains("Duplicate entry")) {
202-
entityManager.clear();
203-
if (handleDuplicateKeyError(stock, e)) {
204-
updatedCount++;
205-
log.debug("Updated stock after duplicate key error: {}", stock.getSymbol());
206-
} else {
207-
errorCount++;
208-
log.warn("Failed to handle duplicate key error for stock: {}", stock.getSymbol());
209-
}
210-
} else {
211-
errorCount++;
212-
log.warn("Failed to save stock: {} - {}", stock.getSymbol(), errorMessage);
213-
}
214193
}
215194
}
216-
processedCount++;
217195
} catch (Exception e) {
218196
errorCount++;
219197
log.error("Unexpected error processing stock: {}", stock.getSymbol(), e);
220198
}
221199
}
222200

201+
// 남은 배치 처리
202+
if (!batch.isEmpty()) {
203+
BatchResult batchResult = processBatch(batch);
204+
insertedCount += batchResult.inserted;
205+
updatedCount += batchResult.updated;
206+
errorCount += batchResult.errors;
207+
processedCount += batch.size();
208+
entityManager.flush();
209+
entityManager.clear();
210+
}
211+
223212
log.info("Processed {} stocks - Inserted: {}, Updated: {}, Errors: {}",
224213
processedCount, insertedCount, updatedCount, errorCount);
225214
}
@@ -421,70 +410,57 @@ private String getRootCauseMessage(Exception e) {
421410
}
422411

423412
/**
424-
* 시퀀스를 실제 DB의 최대 ID로 동기화합니다.
425-
* 이는 중복 키 오류를 방지하기 위해 필요합니다.
426-
* Hibernate가 MySQL에서 시퀀스를 사용할 때 생성하는 테이블을 업데이트합니다.
413+
* 배치 처리 결과를 담는 내부 클래스
427414
*/
428-
private void synchronizeSequence() {
429-
try {
430-
Integer maxId = stockRepository.findMaxId();
431-
if (maxId == null) {
432-
maxId = 0;
433-
}
434-
435-
int nextVal = maxId + 1;
436-
437-
// Hibernate가 생성하는 시퀀스 테이블 이름은 버전에 따라 다를 수 있습니다.
438-
// 일반적으로 hibernate_sequences 또는 sequence_name이 'stock_sequence'인 테이블을 사용합니다.
439-
440-
// 방법 1: hibernate_sequences 테이블 업데이트 시도 (Hibernate 5+)
415+
private static class BatchResult {
416+
int inserted = 0;
417+
int updated = 0;
418+
int errors = 0;
419+
}
420+
421+
/**
422+
* 배치로 새 종목을 저장합니다.
423+
* 중복 키 오류가 발생하면 개별 처리합니다.
424+
* @return 배치 처리 결과 (inserted, updated, errors)
425+
*/
426+
private BatchResult processBatch(List<Stock> batch) {
427+
BatchResult result = new BatchResult();
428+
429+
for (Stock stock : batch) {
441430
try {
442-
Query updateQuery = entityManager.createNativeQuery(
443-
"UPDATE hibernate_sequences SET next_val = :nextVal WHERE sequence_name = 'stock_sequence'"
444-
);
445-
updateQuery.setParameter("nextVal", nextVal);
446-
int updated = updateQuery.executeUpdate();
447-
448-
if (updated == 0) {
449-
// 레코드가 없으면 생성
450-
Query insertQuery = entityManager.createNativeQuery(
451-
"INSERT INTO hibernate_sequences (sequence_name, next_val) VALUES ('stock_sequence', :nextVal) " +
452-
"ON DUPLICATE KEY UPDATE next_val = :nextVal"
453-
);
454-
insertQuery.setParameter("nextVal", nextVal);
455-
insertQuery.executeUpdate();
431+
stockRepository.save(stock);
432+
result.inserted++;
433+
} catch (DataIntegrityViolationException e) {
434+
// 중복 키 오류 발생 시 기존 종목으로 처리
435+
entityManager.clear();
436+
if (handleDuplicateKeyError(stock, e)) {
437+
result.updated++;
438+
log.debug("Updated stock after duplicate key error in batch: {}", stock.getSymbol());
439+
} else {
440+
result.errors++;
441+
log.warn("Failed to handle duplicate key error for stock in batch: {}", stock.getSymbol());
456442
}
457-
log.info("Synchronized sequence to max ID: {} (next_val: {})", maxId, nextVal);
458-
} catch (Exception e1) {
459-
// hibernate_sequences 테이블이 없으면 다른 방법 시도
460-
log.debug("hibernate_sequences table not found, trying alternative method: {}", e1.getMessage());
461-
462-
// 방법 2: stock_sequence 테이블 직접 업데이트 시도
463-
try {
464-
Query updateQuery2 = entityManager.createNativeQuery(
465-
"UPDATE stock_sequence SET next_val = :nextVal"
466-
);
467-
updateQuery2.setParameter("nextVal", nextVal);
468-
int updated2 = updateQuery2.executeUpdate();
469-
470-
if (updated2 == 0) {
471-
Query insertQuery2 = entityManager.createNativeQuery(
472-
"INSERT INTO stock_sequence (next_val) VALUES (:nextVal) " +
473-
"ON DUPLICATE KEY UPDATE next_val = :nextVal"
474-
);
475-
insertQuery2.setParameter("nextVal", nextVal);
476-
insertQuery2.executeUpdate();
443+
} catch (Exception e) {
444+
// 예외 메시지에 "Duplicate entry" 또는 "Lock wait timeout"이 포함되어 있는지 확인
445+
String errorMessage = getRootCauseMessage(e);
446+
if (errorMessage != null && (errorMessage.contains("Duplicate entry") || errorMessage.contains("Lock wait timeout"))) {
447+
entityManager.clear();
448+
if (handleDuplicateKeyError(stock, e)) {
449+
result.updated++;
450+
log.debug("Updated stock after error in batch: {}", stock.getSymbol());
451+
} else {
452+
result.errors++;
453+
log.warn("Failed to handle error for stock in batch: {} - {}", stock.getSymbol(), errorMessage);
477454
}
478-
log.info("Synchronized sequence to max ID: {} (next_val: {})", maxId, nextVal);
479-
} catch (Exception e2) {
480-
log.warn("Failed to synchronize sequence using alternative method: {}", e2.getMessage());
481-
// 시퀀스 동기화 실패는 치명적이지 않으므로 계속 진행
455+
} else {
456+
result.errors++;
457+
log.warn("Failed to save stock in batch: {} - {}", stock.getSymbol(), errorMessage);
482458
}
483459
}
484-
} catch (Exception e) {
485-
// 시퀀스 동기화 실패는 치명적이지 않으므로 경고만 로깅
486-
log.warn("Failed to synchronize sequence: {}", e.getMessage());
487460
}
461+
462+
return result;
488463
}
464+
489465
}
490466

0 commit comments

Comments
 (0)