일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- File
- 내일배움캠프
- BFS
- Baekjoon
- 해시
- 누적합
- 코딩테스트
- programmers
- 계산기 만들기
- 일정 관리
- Elasticsearch
- ES
- 이분탐색
- Spring
- 객체지향
- 알고리즘
- Algorithm
- SpringBoot
- Generics
- binary search
- 완전탐색
- til
- 구현
- parametric search
- 프로그래머스
- Java
- 브루트포스
- querydsl
- 백준
- 이분 탐색
- Today
- Total
개발하는 햄팡이
[Spring] .csv 파일 읽어서 데이터를 DB에 저장 본문
이전글과 같은 프로젝트 하는 중.
상황 설명
현재 구현하려고 하는 기능은 주소 필터링을 하는 것!
그래서 공공데이터에서 법정동코드를 가져와서 그 데이터를 액셀로 만지작 해서 갖고 있는 상태이다.
데이터는 아래 링크에서 가져왔다.
https://www.code.go.kr/stdcode/regCodeL.do
법정동코드목록조회 - 행정표준코드관리시스템
정상적으로 로그아웃하지 않았거나, 동일 사용자가 로그인한 상태입니다. 강제로 로그인 하시겠습니까? (강제 로그인 시 다시 로그인을 하셔야 합니다.)
www.code.go.kr
우리의 데이터 파일은
[법정동 코드, 코드 이름]
예를 들면
[4413000000,충청남도 천안시]
이런 형태로 되어있다.
법정동 코드는 앞 두자리가 시도코드, 중간 세자리가 시군구 코드, 그 다음 5자리가 읍명동 코드 그리고 마지막 두자리가 리(구룡리 같은)코드로 되어있다.
프로젝트 기획상 그렇게 세밀하게 할 필요는 없을 것 같아서 읍면동까지만 저장하려고 했는데
팀원들이 읍면동까지는 저장하지 않을 것 같다고 하여 이미 만들어진 읍면동 테이블을 확장성을 위해 냅두고 시군구 까지만 저장할 것이다.
테이블은 아래와 같이 생겼다
create table sido
(
sd_cd varchar(2) not null
primary key,
sd_nm varchar(100) not null
);
create table sigungu
(
sgg_cd varchar(5) not null
primary key,
sd_cd varchar(255) not null,
sgg_nm varchar(100) not null,
constraint FK78wyvutcmoyml3fiovvik010n
foreign key (sd_cd) references sido (sd_cd)
);
create table eupmyeondong
(
emd_cd varchar(10) not null
primary key,
emd_nm varchar(100) not null,
sgg_cd varchar(255) not null,
constraint FKpqvm19u448969mkpc5l839o5m
foreign key (sgg_cd) references sigungu (sgg_cd)
);
이제 해야할 일은 코드를 저장하기
예시를 가지고 설명을 해보자면
[4413000000,충청남도 천안시]
sd_cd = 44
sd_nm = 충청남도
---------------------------------------
sgg_cd = 44130
sgg_nm = 충청남도 천안시
sd_cd = 44
---------------------------------------
emd_cd = 4413000000
emd_nm = 충청남도 천안시 전체
sgg_cd = 44130
이렇게 저잘할 예정이다.
이렇게 하면 시도 & 시군구 테이블이 필요가 없지 않나? 하지만
프론트가 드롭다운전글과 같은 프로젝트 하는 중.
이렇게 하면 시도 & 시군구 테이블이 필요가 없지 않나? 하지만
프론트가 사용자에게 필터 드롭다운을 보여준다던가 할때 사용할 수 있을 것 같아서 저장은 해뒀다.
서버에서는 emd테이블만 있어도 사용할 수 있지만 프론트가 원한다면 시도만 보여줄 수도 있게 된 것!
그래서 이제 해야할 일은
CSV파일을 읽어서 DB에 저장하기!
0. Dependency import
그냥 파일 읽어서 직접 파싱하고 할 수 있지만 나는 csv를 좀 더 안전하고 편하게 읽어들이기 위해 라이브러리를 사용할 것이다.
implementation 'com.opencsv:opencsv:5.9'
1. CSV 파일 준비
나는 resources 파일 하위에 data라는 폴더를 만들고 그 밑에 저장할 것이다.
그리고 application.yml 설정값에 따라서 값을 새로 저장할지 말지 결정하기 위해
address:
import:
enabled: true # CSV 읽어서 넣을지 여부
해당 설정값을 추가해줬다.
프로젝트를 시작할때 세팅하기 위한 작업이기 때문에 가져오는 메소드를 어플리케이션을 시작하면 실행할 수 있도록 할 것이다.
그런데 새로 시작할 때마다 무조건 저장하면 오래걸리고 쓸데 없는 일이니깐
설정값에 따라서 실행할지 말지를 결정하게 하는 것이다.
2. LoaderService 만들기
AddressCodeLoader클래스를 만들어서 주소를 읽을때 사용하는 로직을 구현해야한다.
해당 데이터는 참조 코드이기때문에 수정될 일이 거의 없고, 프로그램을 초기 세팅할 때에만 넣어주면 된다.
그래서 insert할때
1. INSERT IGNORE INTO ~~ 쿼리문을 사용하여 중복되는 PK일 경우 덮어쓰고, 새로운 값일 경우 추가
2. 그냥 싹다 비우고 저장하기
두가지 방법이 있는데
코드가 만개정도 되고 처음 세팅할때 필요한 코드이기 때문에
얘네의 데이터가 변경되어 다시 저장하는 상황이 온다면
그건 코드 구조나 자치도명이라던가 그런게 완전 확 바뀔때가 아닐까...?
그럼 그냥 싹 다 삭제하고 갈아엎는게 좋을 것 같다고 생각해서 기존의 것을 다 삭제하고 새로 저장하는 방식으로 하려고 한다.
Address Table과 연관관계 설정이 되어 있어 지울 수 없다...
그냥 1번으로 결정
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.explorer.gabom.domain.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class AddressCodeLoaderService {
private final CsvImporter csvImporter;
private final AddressUpsertService upsertService;
@Transactional
public void loadFromClasspath() throws Exception {
List<LawAddressCode> rows = csvImporter.readCsv();
Map<String, Sido> sdMap = new HashMap<>();
Map<String, Sigungu> sggMap = new HashMap<>();
Map<String, Eupmyeondong> emdMap = new HashMap<>();
for (LawAddressCode row : rows) {
if (row == null)
continue;
String code = row.getCode();
String name = row.getName();
code = code.trim();
name = name.trim();
if (!AddressCodeUtils.isValidLawCode(code))
continue;
String sdCd = AddressCodeUtils.sdCd(code);
String sggCd = AddressCodeUtils.sggCd(code);
// 시도
sdMap.put(sdCd, Sido.builder()
.sdCd(sdCd)
.sdNm(AddressCodeUtils.sdNm(name))
.build());
// 시군구
sggMap.put(sggCd, Sigungu.builder()
.sggCd(sggCd)
.sggNm(name)
.sdCd(sdCd)
.build());
// 읍면동
emdMap.put(code, Eupmyeondong.builder()
.emdCd(code)
.emdNm(name)
.sggCd(sggCd)
.build());
}
var sdList = new ArrayList<>(sdMap.values());
var sggList = new ArrayList<>(sggMap.values());
var emdList = new ArrayList<>(emdMap.values());
log.info("batch sizes: sd={}, sgg={}, emd={}", sdList.size(), sggList.size(), emdList.size());
upsertService.upsertSd(sdList);
upsertService.upsertSgg(sggList);
upsertService.upsertEmd(emdList);
}
}
package com.explorer.gabom.domain.batch.service;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.explorer.gabom.domain.address.entity.Eupmyeondong;
import com.explorer.gabom.domain.address.entity.Sido;
import com.explorer.gabom.domain.address.entity.Sigungu;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class AddressUpsertService {
private final JdbcTemplate jdbc;
@Transactional
public void upsertSd(List<Sido> list) {
if (list.isEmpty())
return;
String sql = """
INSERT INTO sido (sd_cd, sd_nm)
VALUES (?, ?)
AS new
ON DUPLICATE KEY UPDATE
sd_nm = new.sd_nm
""";
jdbc.batchUpdate(sql, list, list.size(), (ps, sd) -> {
ps.setString(1, sd.getSdCd());
ps.setString(2, sd.getSdNm());
});
}
@Transactional
public void upsertSgg(List<Sigungu> list) {
if (list.isEmpty())
return;
String sql = """
INSERT INTO sigungu (sgg_cd, sgg_nm, sd_cd)
VALUES (?, ?, ?)
AS new
ON DUPLICATE KEY UPDATE
sgg_nm = new.sgg_nm,
sd_cd = new.sd_cd
""";
jdbc.batchUpdate(sql, list, list.size(), (ps, s) -> {
ps.setString(1, s.getSggCd());
ps.setString(2, s.getSggNm());
ps.setString(3, s.getSdCd());
});
}
@Transactional
public void upsertEmd(List<Eupmyeondong> list) {
if (list.isEmpty())
return;
String sql = """
INSERT INTO eupmyeondong (emd_cd, emd_nm, sgg_cd)
VALUES (?, ?, ?)
AS new
ON DUPLICATE KEY UPDATE
emd_nm = new.emd_nm,
sgg_cd = new.sgg_cd
""";
jdbc.batchUpdate(sql, list, list.size(), (ps, e) -> {
ps.setString(1, e.getEmdCd());
ps.setString(2, e.getEmdNm());
ps.setString(3, e.getSggCd());
});
}
}
insert하는 부분은 JDBC의 batch기능을 사용했다.
JPA의 saveAll()보다 훨씬 빠르다.
JPA의 saveAll()은 언뜻 보면 많은 데이터를 한번에 저장하는 것 같지만 실제로 쿼리를 보면 100개의 데이터를 insert하게 되면 insert 요청이 100번 나간다.
그런데 batch는 PreparedStatement를 한번만 설정하고 거기에 차곡차곡 데이터를 담아 한번만 요청을 하기 때문에 훨씬 빠르다.
3. 실행시 호출되는 Loader서비스 구현
이제 서비스는 구현이 됐고, 실제로 프로그램이 실행 시작 후 자동으로 서비스를 호출할 컴포넌트가 필요하다.
CommandLineRunner를 implement해서 구현해보자
처음에 말했듯이 아무때나 실행하는게 아니라 설정값에 따라 실행할지 말지 결정해야한다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.explorer.gabom.domain.batch.service.AddressCodeLoaderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Order(2)
@Component
@RequiredArgsConstructor
public class AddressCodeLoader implements CommandLineRunner {
private final AddressCodeLoaderService service;
@Value("${address.import.enabled:false}")
private boolean enabled;
@Override
public void run(String... args) {
if (!enabled) {
log.info("[AddressCodeLoader] 비활성화됨. 스킵");
return;
}
try {
log.info("[AddressCodeLoader] 시작 (CSV → DB)");
service.loadFromClasspath();
log.info("[AddressCodeLoader] 완료");
} catch (Exception e) {
log.error("[AddressCodeLoader] 실패: {}", e.getMessage(), e);
throw new IllegalStateException("법정동 코드 초기화 실패", e);
}
}
}
@Order 어노테이션을 사용하여 Runner가 많아질 경우 중 몇번째로 실행할 지 설정할 수 있다.
이렇게 해서 실제 데이터는 아래와 같이 저장되는 것을 볼 수 있다.
원하는대로 저장 성공~~!!
이제 내일은 가짜 데이터를 만들어야겠다..
'Back-End > Spring' 카테고리의 다른 글
[SpringBoot3.5.X][Swagger]response status is 500 /v3/api-docs/auth (4) | 2025.08.04 |
---|---|
[Spring] 파일입출력 - 같은 파일 업로드 요청 시 계속 저장되는 문제 - Hash로 해결 (4) | 2025.07.29 |
[Spring][JPA][뉴스피드 만들기] JPA의 변경 감지 (set메소드) vs save() 호출 - 무엇을 선택할까? (5) | 2025.06.14 |
[Spring][일정 관리 앱 만들기] CRUD 생성 - QueryDsl 쿼리문 코드 작성 (1) | 2025.05.24 |
[Spring][일정 관리 앱 만들기] CRUD 생성 - QueryDsl 시작하기 (2) | 2025.05.24 |