1111import com .fund .stockProject .stock .repository .StockRepository ;
1212import jakarta .persistence .EntityManager ;
1313import jakarta .persistence .PersistenceContext ;
14- import jakarta .persistence .Query ;
1514import lombok .RequiredArgsConstructor ;
1615import lombok .extern .slf4j .Slf4j ;
1716import 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