단순히 "종목 정보를 넣는다"는 생각으로 시작했지만, 실제 데이터를 다루다 보니 고려해야 할 변수가 많았습니다. 개발 과정에서 마주한 고민과 이를 해결하기 위한 시스템 구조를 정리합니다.
🚀 개발 비하인드: 왜 '단순 저장' 이상이 필요했나?
처음에는 단순히 종목 마스터를 한 번 가져와 DB에 넣으면 끝날 줄 알았습니다. 하지만 실제 시장은 생각보다 훨씬 역동적이었습니다.
- "아, 신규 상장이 있구나!" → 매일 새로운 기업이 시장에 진입합니다. 이를 수동으로 넣을 순 없기에 매일 실행되는 스케줄러를 도입했습니다.
- "스팩(SPAC)은 분석에 방해되는데?" → 기업 합병이 목적인 스팩주들이 섞여 들어오면 데이터 분석 노이즈가 발생합니다. 종목명과 코드를 체크해 스팩 정보는 저장 단계에서 제외했습니다.
- "상장 폐지나 관리 종목은 어떡하지?" → 사라지는 종목을 추적하고, 리스크가 있는 종목을 구분하기 위해 **관리 종목 여부(mangIssuYn)와 업데이트 로직(Upsert)**을 추가했습니다.
🏛️ 아키텍처(책임 분리) — 설계 의도와 코드 구현
이 시스템은 단일 책임 원칙(SRP)에 따라 네 가지 주요 레이어로 분리되어 있습니다.
1. 스케줄러 (StockMstScheduler.java)
역할: 정해진 시각에 배치를 실행하는 '트리거'입니다.
설계 의도: 개장 전과 장 마감 후에 데이터를 갱신하여 시스템이 항상 최신 시장 상태를 유지하게 합니다.
Java
@RequiredArgsConstructor
@Component
public class StockMstScheduler {
private final StockMstDownloadService stockMstDownloadService;
@Scheduled(cron = "0 0 9 * * *") // 개장 전: 신규 상장 및 관리 종목 반영
public void updateBeforeMarketOpen() {
stockMstDownloadService.downloadAndUpdateKospi();
stockMstDownloadService.downloadAndUpdateKosdaq();
}
@Scheduled(cron = "0 0 16 * * *") // 장 마감 후: 최종 데이터 동기화
public void updateAfterMarketClose() {
stockMstDownloadService.downloadAndUpdateKospi();
}
}
2. 도메인 서비스 (StockMstDownloadService.java)
역할: 파일 다운로드부터 필터링, DB 저장까지 전체 워크플로우를 지휘합니다.
설계 의도: 스팩주 제외 등 비즈니스 규칙을 적용하고, 데이터의 원자성을 보장합니다.
Java
@Service
@Transactional
public class StockMstDownloadService {
@Transactional
private int updateStocksFromMst(File mstFile, String market) throws Exception {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(mstFile), Charset.forName("EUC-KR")))) {
String line;
while ((line = reader.readLine()) != null) {
StockMstDto dto = StockMstDto.parseLine(line);
// 1. 필터링: 주식(ST)이 아니거나 스팩(SPAC) 종목인 경우 제외
if (dto == null || !"ST".equals(dto.getScrtGrpClsCode().trim())
|| dto.getHtsKorIsnm().contains("스팩")) continue;
// 2. Upsert 로직: 신규 상장은 Insert, 기존 종목은 정보(관리종목 여부 등) Update
var existing = stockMapper.selectStockByCode(dto.getMkscShrnIscd());
if (existing == null) {
stockMapper.insertStock(convertDtoToEntity(dto));
} else {
// 관리종목 여부 등 최신 상태 업데이트
stockMapper.updateStockBasicInfo(convertDtoToEntity(dto));
}
}
}
}
}
3. 파서 DTO (StockMstDto.java)
역할: 고정폭(Fixed-length) 바이너리 형식의 MST 파일을 객체화합니다.
설계 의도: 한글이 포함된 데이터이므로 바이트 오프셋을 직접 계산하여 데이터 깨짐을 방지합니다.
Java
public class StockMstDto {
public static StockMstDto parseLine(String line) {
byte[] b = line.getBytes(Charset.forName("EUC-KR"));
if (b.length < 280) return null;
StockMstDto dto = new StockMstDto();
dto.mkscShrnIscd = byteSub(b, 0, 9); // 종목코드
dto.htsKorIsnm = byteSub(b, 21, 40); // 종목명
dto.mangIssuYn = byteSub(b, 77, 1); // 관리종목 여부 (중요!)
return dto;
}
}
4. 영속 계층 (MyBatis Mapper)
역할: DB 테이블 매핑 및 SQL 실행을 담당합니다.
XML
<insert id="insertStock" parameterType="Stock">
INSERT INTO stocks (name, stock_code, std_code, market_type, is_managed)
VALUES (#{name}, #{stockCode}, #{stdCode}, #{marketType}, #{isManaged})
</insert>
🛠️ 운영 가이드라인 및 레슨런(Lesson Learned)
- 변화에 대응하기: 시장 데이터는 고정적이지 않습니다. 상장/폐지/관리종목 지정 등의 이벤트를 코드로 자동화하는 것이 운영 공수를 줄이는 핵심입니다.
- 데이터 정제(Cleaning): 분석 목적에 맞지 않는 스팩주 같은 데이터는 수집 단계에서 미리 걷어내는 것이 DB 건강에 좋습니다.
- 안정성: MST 파일 오프셋은 증권사 사정에 따라 변할 수 있으므로, 로그 모니터링을 통해 파싱 에러를 즉시 감지해야 합니다.
'Archive(완료된 내용) > 포트폴리오 강화' 카테고리의 다른 글
| [stock101] pdf 업로드 및 추출 -5일차 (0) | 2026.01.26 |
|---|---|
| [stock101] dart연동 및 ksi 연동 그리고 리팩토링 -4일차 (1) | 2026.01.22 |
| [stock101] LOCK을 누가 계속 잡는 문제. (0) | 2026.01.21 |
| [stock101] KIS API연동 웹소켓 - 2일차 (1) | 2026.01.20 |
| [stock101] pdf파일 업로드 및 AI 셋팅 - 1일차 (0) | 2026.01.19 |