1515import lombok .extern .slf4j .Slf4j ;
1616import org .springframework .dao .DataIntegrityViolationException ;
1717import org .springframework .stereotype .Service ;
18- import org .springframework .transaction .annotation .Transactional ;
18+ import org .springframework .transaction .PlatformTransactionManager ;
19+ import org .springframework .transaction .support .TransactionTemplate ;
1920
2021import java .io .File ;
2122import java .io .IOException ;
@@ -31,9 +32,14 @@ public class StockImportService {
3132 private final ObjectMapper objectMapper ;
3233 private final ExperimentRepository experimentRepository ;
3334 private final PreferenceRepository preferenceRepository ;
35+ private final PlatformTransactionManager transactionManager ;
3436
3537 @ PersistenceContext
3638 private EntityManager entityManager ;
39+
40+ private TransactionTemplate getTransactionTemplate () {
41+ return new TransactionTemplate (transactionManager );
42+ }
3743
3844 /**
3945 * JSON 파일에서 종목 데이터를 읽어서 DB에 저장하고, 종목 마스터에 없는 종목은 isValid=false로 설정합니다.
@@ -389,66 +395,68 @@ private static class UpsertResult {
389395 /**
390396 * 작은 트랜잭션으로 배치를 처리합니다.
391397 * 각 배치는 독립적인 트랜잭션으로 처리되어 락 경합을 최소화합니다.
398+ * TransactionTemplate을 사용하여 명시적으로 트랜잭션을 관리합니다.
392399 */
393- @ Transactional
394400 private UpsertResult processBatchInTransaction (List <Stock > batch ) {
395- UpsertResult result = new UpsertResult ();
396-
397- for (Stock stock : batch ) {
398- try {
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 ());
401+ return getTransactionTemplate ().execute (status -> {
402+ UpsertResult result = new UpsertResult ();
403+
404+ for (Stock stock : batch ) {
405+ try {
406+ // symbol로 기존 종목 확인
407+ Optional <Stock > existingOpt = stockRepository .findBySymbol (stock .getSymbol ());
408+
409+ if (existingOpt .isPresent ()) {
410+ // 기존 종목 업데이트
411+ Stock existing = existingOpt .get ();
412+ existing .updateSymbolNameIfNull (stock .getSymbolName ());
413+ existing .setValid (true );
414+ if (stock .getDomesticSector () != null ) {
415+ existing .setDomesticSector (stock .getDomesticSector ());
416+ }
417+ if (stock .getOverseasSector () != null ) {
418+ existing .setOverseasSector (stock .getOverseasSector ());
419+ }
420+ stockRepository .save (existing );
421+ result .updated ++;
422+ } else {
423+ // 새 종목 삽입 (IDENTITY 전략 사용)
424+ stockRepository .save (stock );
425+ result .inserted ++;
412426 }
413- stockRepository .save (existing );
414- result .updated ++;
415- } else {
416- // 새 종목 삽입 (IDENTITY 전략 사용)
417- stockRepository .save (stock );
418- result .inserted ++;
419- }
420- } catch (DataIntegrityViolationException e ) {
421- // 중복 키 오류 발생 시 기존 종목으로 처리
422- entityManager .clear ();
423- if (handleDuplicateKeyError (stock , e )) {
424- result .updated ++;
425- } else {
426- result .errors ++;
427- log .warn ("Failed to handle duplicate key error for stock: {}" , stock .getSymbol ());
428- }
429- } catch (Exception e ) {
430- String errorMessage = getRootCauseMessage (e );
431- if (errorMessage != null && (errorMessage .contains ("Duplicate entry" ) ||
432- errorMessage .contains ("Lock wait timeout" ) ||
433- errorMessage .contains ("could not read a hi value" ))) {
427+ } catch (DataIntegrityViolationException e ) {
428+ // 중복 키 오류 발생 시 기존 종목으로 처리
434429 entityManager .clear ();
435430 if (handleDuplicateKeyError (stock , e )) {
436431 result .updated ++;
437432 } else {
438433 result .errors ++;
439- log .warn ("Failed to handle error for stock: {} - {}" , stock .getSymbol (), errorMessage );
434+ log .warn ("Failed to handle duplicate key error for stock: {}" , stock .getSymbol ());
435+ }
436+ } catch (Exception e ) {
437+ String errorMessage = getRootCauseMessage (e );
438+ if (errorMessage != null && (errorMessage .contains ("Duplicate entry" ) ||
439+ errorMessage .contains ("Lock wait timeout" ) ||
440+ errorMessage .contains ("could not read a hi value" ))) {
441+ entityManager .clear ();
442+ if (handleDuplicateKeyError (stock , e )) {
443+ result .updated ++;
444+ } else {
445+ result .errors ++;
446+ log .warn ("Failed to handle error for stock: {} - {}" , stock .getSymbol (), errorMessage );
447+ }
448+ } else {
449+ result .errors ++;
450+ log .warn ("Failed to save stock: {} - {}" , stock .getSymbol (), errorMessage );
440451 }
441- } else {
442- result .errors ++;
443- log .warn ("Failed to save stock: {} - {}" , stock .getSymbol (), errorMessage );
444452 }
445453 }
446- }
447-
448- // 트랜잭션 내에서 flush하여 ID 생성 확정
449- entityManager . flush ();
450-
451- return result ;
454+
455+ // 트랜잭션 내에서 flush하여 ID 생성 확정
456+ entityManager . flush ();
457+
458+ return result ;
459+ }) ;
452460 }
453461
454462
0 commit comments