@@ -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