프로젝트를 진행하며 csv파일을 DB에 파싱해서 넣기 위해 Batch를 사용했다.
생각했던 것 보다 내용이 많은 부분이였고, 나중에 시간이 된다면 배치에 대해 더 자세히 공부해 봐야겠다..
배치란 ?
- 말 자체는 일괄처리 란 뜻을 가지고 있다.
- 대용량의 데이터를 처리하게 되면 해당 기능이 실행될 때 서버의 자원을 거의 다 쓰게 된다.
- 만약, 이런 대용량의 데이터를 불러오고 저장하는 과정이 하루에 한 번만 진행된다면 굳이 API를 사용해서 호출하는 것 보단, 배치 어플리케이션을 만들어 사용하는 것이 좋다.(spring schedueling쓰면 매일 자정에 실행하게끔도 할 수 있다.)
- 동일한 배치 작업은 여러번 실행되지 않는다 ! (스프링에서 알아서 동일 Job 재실행인지 확인함 (jobname으로 판단하는 듯)
- .allowStartIfComplete(true)
- 이렇게 설정하면 재실행 시켜줌
- 스프링 배치는 기본적으로 단일 스레드 방식으로 작업을 처리 한다.
- 성능 향상 및 대규모 데이터 작업을 위해 비동기 처리 및 scale out기능을 제공한다.
- AsyncProcessor/ AsyncWriter
- Item Processor에 별도의 스레드가 할당되어 작업을 처리한다
- implementation ‘org.springframework.batch:spring-batch-integration (의존성 추가 필요)
- step설정(stepBuilderFactory에서 get)
- 청크 설정
- Itemreader설정( 비동기 아님 얘는)
- AsyncItemProcessor 설정
- 스레드 풀 개수 만큼 스레드가 생성되어 비동기적으로 실행된다
- 내부적으로 ItemProcessor에게 실행을 위임하고 결과를 Future에 저장
- AsyncItemWriter설정
- 비동기 실행 결과를 모두 받아올 때까지 대기
- 내부적으로는 ItemWriter에게 실행을 위임한다.
- 빌드(TaskletStep생성)
- Multi-threaded Step
- Step내 chunk구조인 item reader, processor, writer마다 여러 스레드가 할당되어 실행하는 방식
- step내에서 멀티 스레드로 실행되고 chunk기반으로 처리를 한다
- ItemReader는 thread-safe한지 확인해야 한다.( 스레드마다 다른 데이터를 읽도록)
- 스레드마다 새로운 청크가 할당되어 데이터 동기화가 보장됨
- Remote chunking
- 분산환경처럼 step 처리가 여러 프로세스로 분할되어 외부로 전송되어 처리되는 방식
- Parallel Step
- step 마다 스레드가 할당되어 step을 병렬로 실행하는 방법
- SplitState를 사용해서 여러개의 Flow들을 병렬적으로 실행하는 구조
- flow를 생성하고 다른 플로우를 생성해서 add하면 플로우 개수만큼 스레드를 생성해서 각 플로우를 실행시킨다.
- next로 추가한 플로우는 add로 추가한 플로우가 다 처리된 후 실행됨
- Partitioning
- master-slave구조로 master가 파티셔닝하고 slave들에게 할당하는 방식
- slavestep은 각 스레드에 의해 독립적으로 실행 됨
- 각 slavestep은 itemreader,processor,writer을 갖고 동작하며 작업을 독립적으로 실행한다
- AsyncProcessor/ AsyncWriter
구현
배치사용을 위해 dependency에 다음 추가
implementation 'org.springframework.boot:spring-boot-starter-batch’
배치 사용을 위해 부트 applicaiton에 어노테이션 추가
@SpringBootApplication
@EnableBatchProcessing // 배치 사용을 위한 선언
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
배치 작업을 실행 할 config 생성
@Configuration
@Slf4j
@RequiredArgsConstructor
public class FileReadJobConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final CsvReader csvReader;
private final CsvWriter csvWriter;
private static final int chunkSize = 1;
//Job : 하나의 배치 작업 단위
//하나의 Job안에는 여러 Step이 존재하고 Step안에 Reader,Writer등이 포함된다
@Bean
public Job csvFileItemReaderJob() {
return jobBuilderFactory.get("DataFileItemReaderJob")
.start(csvFileItemReaderStep())
.build();
}
@Bean
public Step csvFileItemReaderStep() {
return stepBuilderFactory.get("DataFileItemReaderStep")
//CsvEntity를 Reader에서 읽어오고 CsvEntity를 writer에게 넘겨줄 것이다
.<CsvEntity, CsvEntity>chunk(chunkSize)
.reader(csvReader.csvFileItemReader())
.writer(csvWriter)
.build();
}
}
Reader
@Configuration
@RequiredArgsConstructor //사실 required한 args가 없으니 얘는 필요없긴 할 듯?? 아닌가..
public class CsvReader {
@Bean
public FlatFileItemReader<CsvEntity> csvFileItemReader() {
FlatFileItemReader<CsvEntity> flatFileItemReader = new FlatFileItemReader<>();
//resources/csv/dataset.csv 를 resource경로로 지정
flatFileItemReader.setResource(new ClassPathResource("/csv/dataset.csv"));
//맨 윗줄은 column 이름이니 skip
flatFileItemReader.setLinesToSkip(1);
flatFileItemReader.setEncoding("UTF-8");
//각 줄을 CsvEntity로 매핑한
DefaultLineMapper<CsvEntity> defaultLineMapper = new DefaultLineMapper<>();
// 각 Column을 ,로 구분
DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer(",");
delimitedLineTokenizer.setNames("id","trackId", "artists","albumName","trackName","popularity","duration","explicit","dance","energy","keyValue","loudness","mode","speech","acoustic","instrument","live","valance","tempo","timeSignature","trackGenre");
defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
/* beanWrapperFieldSetMapper : Tokenizer에서 가지고온 데이터들을 VO로 바인드하는 역할 */
BeanWrapperFieldSetMapper<CsvEntity> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
beanWrapperFieldSetMapper.setTargetType(CsvEntity.class);
defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
/* lineMapper 지정 */
flatFileItemReader.setLineMapper(defaultLineMapper);
System.out.println("ooooooo");
return flatFileItemReader;
}
}
Writer
@Configuration
@RequiredArgsConstructor
@Slf4j
public class CsvWriter4 implements ItemWriter<CsvEntity> {
private final AlbumRepository albumRepository;
private final GenreRepository genreRepository;
private final MusicRepository musicRepository;
private final SingerRepository singerRepository;
private final FeatureRepository featureRepository;
private final MusicSingerRepository musicSingerRepository;
private List<Album> albumList =new ArrayList<>();
private Map<String,Singer> singerMap =new HashMap<>();
//singerList로 가수가 기존에 존재하던 가수인 지 확인하는 작업 만 하고, 가수 이름의 중복을 애초에 허용하지 않았으니 map
private Map<String,Genre> genreMap=new HashMap<>();
//얘도 장르 이름으로 구분하고 장르 이름에 중복을 허용하지 않는다.
private Map<String,String> checkDuplicates=new HashMap<>();
@Override
public void write(List<? extends CsvEntity> items) throws Exception {
List<Album> iterAlbumList =new ArrayList<>();
List<Singer> iterSingerList =new ArrayList<>();
List<MusicFeature> iterFeatureList=new ArrayList<>();
List<Genre> iterGenreList=new ArrayList<>();
List<MusicSinger> iterMusicSingerList=new ArrayList<>();
List<Music> iterMusicList=new ArrayList<>();
items.forEach(item->{
//중복된 row는 제거
Optional<String> rowCheck = Optional.ofNullable(checkDuplicates.get(item.getTrackId()));
if(!rowCheck.isPresent()){
checkDuplicates.put(item.getTrackId(),"checked");
}else{
log.warn("Duplicate row");
return;
}
String albumName = item.getAlbumName();
List<String> artists = getArtistName(item.getArtists());
//사실 여기 list보단 set이 더 빠르긴 하지만, 가수는 매 item마다 몇명안되니 pass
//가수길이가 너무 긴 경우 그냥 저장 안하고 Pass
if(artists.isEmpty()) {
log.warn("No artists");
return;
}
//음악 특징 저장
MusicFeature musicFeature = MusicFeature.builder().popularity(item.getPopularity()).duration(item.getDuration()).explicit(item.getExplicit()).dance(item.getDance())
.energy(item.getDance()).keyValue(item.getKeyValue()).loudness(item.getLoudness()).mode(item.getMode()).speech(item.getSpeech())
.acoustic(item.getAcoustic()).instrument(item.getInstrument()).live(item.getLive()).valence(item.getValence())
.tempo(item.getTempo()).timeSignature(item.getTimeSignature()).build();
iterFeatureList.add(musicFeature);
//앨범의 주인은 첫번째 등장하는 가수만으로 한정
String mainSinger=artists.get(0);
Album saveAlbum;
//메인 가수(앨범 주인)와 앨범의 관계 저장
List<Album> sameAlbum = albumList.stream().filter(album -> album.getAlbumName().equals(albumName)).collect(Collectors.toList());
if(sameAlbum.isEmpty()){
Optional<Singer> findSinger = Optional.ofNullable(singerMap.get(mainSinger));
if(findSinger.isPresent()){
//이미 존재하는 가수
Album albumBuild = Album.init().albumName(albumName).singer(findSinger.get()).build();
albumList.add(albumBuild);
iterAlbumList.add(albumBuild);
saveAlbum=albumBuild;
}else{
Singer buildSinger = Singer.init().singerName(mainSinger).build();
Album buildAlbum = Album.init().albumName(albumName).singer(buildSinger).build();
albumList.add(buildAlbum);
iterAlbumList.add(buildAlbum);
buildSinger.getAlbums().add(buildAlbum);
singerMap.put(buildSinger.getSingerName(),buildSinger);
iterSingerList.add(buildSinger);
saveAlbum=buildAlbum;
}
}
else{
Optional<Album> opt = sameAlbum.stream().filter(album -> album.getSinger().getSingerName().equals(mainSinger)).findAny();
if(opt.isPresent()){
saveAlbum=opt.get();
}else{
//동일한 앨범이 존재는 하나, 현재 가수의 앨범은 아닌 경우
//마찬가지로 현재 가수가 이미 존재하는 가수인지 신규 가수인지 판단 후 저장 필요
Optional<Singer> findSinger = Optional.ofNullable(singerMap.get(mainSinger));
if(findSinger.isPresent()){
//이미 존재하는 가수
Album albumBuild = Album.init().albumName(albumName).singer(findSinger.get()).build();
albumList.add(albumBuild);
iterAlbumList.add(albumBuild);
saveAlbum=albumBuild;
}else{
Singer buildSinger = Singer.init().singerName(mainSinger).build();
Album buildAlbum = Album.init().albumName(albumName).singer(buildSinger).build();
albumList.add(buildAlbum);
iterAlbumList.add(buildAlbum);
buildSinger.getAlbums().add(buildAlbum);
singerMap.put(buildSinger.getSingerName(),buildSinger);
iterSingerList.add(buildSinger);
saveAlbum=buildAlbum;
}
}
}
Genre genreBuild;
//장르 이름 중복 확인
Optional<Genre> optGenre = Optional.ofNullable(genreMap.get(item.getTrackGenre()));
if(!optGenre.isPresent()){
//새로운 장르 인 경우
genreBuild=Genre.init().genreName(item.getTrackGenre()).build();
genreMap.put(genreBuild.getGenreName(),genreBuild);
iterGenreList.add(genreBuild);
}else{
//기존 있던 장르인 경우 추가할 필요가 없으니 itergenre리스트에도 추가하면 안된다.
genreBuild=optGenre.get();
}
Music buildMusic = Music.init().title(item.getTrackName()).trackId(item.getTrackId()).album(saveAlbum).feature(musicFeature).genre(genreBuild).build();
iterMusicList.add(buildMusic);
//가수 list 저장 ( 모든 가수 list에 곡 추가) (메인가수는 어차피 이미 존재하니 pass)
artists.remove(0);
List<Singer> nowSingerList=new ArrayList<>();
artists.forEach(singer->{
Optional<Singer> opt = Optional.ofNullable(singerMap.get(singer));
if(opt.isPresent())
nowSingerList.add(opt.get());
else{
Singer buildSinger = Singer.init().singerName(singer).build();
singerMap.put(buildSinger.getSingerName(), buildSinger);
iterSingerList.add(buildSinger);
nowSingerList.add(buildSinger);
}
});
nowSingerList.forEach(singer -> {
//현재 들어온 가수들에게 현재 노래를 추가한다..
MusicSinger build = MusicSinger.init().music(buildMusic).singer(singer).build();
//연관관계 init에서 정리하니까 ..
iterMusicSingerList.add(build);
});
});
featureRepository.saveAll(iterFeatureList);
singerRepository.saveAll(iterSingerList);
albumRepository.saveAll(iterAlbumList);
if(!iterGenreList.isEmpty())
genreRepository.saveAll(iterGenreList);
else log.info("There is no genre for add");
musicRepository.saveAll(iterMusicList);
musicSingerRepository.saveAll(iterMusicSingerList);
}
private static List<String> getArtistName(String artists) {
List<String> artistList=new ArrayList<String>();
if(artists.length()<200){
StringTokenizer st=new StringTokenizer(artists, ";");
while(st.hasMoreTokens()){
artistList.add(st.nextToken());
}
return artistList;
}else return new ArrayList<>();
}
- 전체 csv파일을 저장소를 이용해 저장하기 위해 저장소를 DI받고, 중복 검사를 위해 List,Map을 생성함
- 그 후, writer내부에서 청크 단위 마다 각 저장소에 저장할 List를 생성한 후, 받아온 chunk의 item마다 분류 작업을 시작한다
- 우선, csv파일에 중복된 노래가 꽤 있기에 row마다 고유한 trackid를 기준으로 중복 검사를 한다
- 그 후, varchar한도를 넘지 않기 위해 받아온 singer를 ;을 기준으로 파싱하고 길이 검사를 한 후 List에 담아 artists에 넣는다
- 음악 특징을 저장한다
- 노래의 메인 가수( 앨범의 주인이 될 가수)는 가수 리스트 중 제일 처음 가수로 결정한다.
- 전체 앨범 중 현재 item과 같은 이름의 앨범이 있는지 확인한다
- 만약 있다면 그 중 앨범의 가수가 item의 현재 가수(mainsinger)와 같은지 확인한다
- 같다면 이미 존재하는 가수의 이미 존재하는 앨범에 새로운 노래를 추가하는 것이므로 별다른 작업을 하지 않는다
- 다르다면, 현재 item의 가수가 이미 있는 가수인지 확인한다
- 이미 존재하는 가수라면 앨범을 추가한다
- 새로운 가수라면 가수와앨범을 추가한다
- 없다면, 가수가 이미 존재하는지 확인한다
- 이미 존재하는 가수라면 앨범을 추가한다
- 새로운 가수라면 가수와앨범을 추가한다
- 만약 있다면 그 중 앨범의 가수가 item의 현재 가수(mainsinger)와 같은지 확인한다
- item의 장르가 이미 등록된 장르인지 확인한다
- 이미 등록되어 있다면 별다른 작업을 하지 않는다
- 신규 장르라면 장르로 등록한다
- 음악을 build하고 저장list에 추가한다
- 현재 들어온 가수 이름 목록으로 가수를 찾아 신규 가수 여부를 확인 후 신규 가수라면 생성 후 등록한다.
- 현재 가수 목록을 이용하여 음악-가수 관계 테이블에 음악과 가수를 등록한다.
- item에 대한 모든 iter가 끝났으니 각 레포지토리에 현재 chunk의 데이터를 write한다.
CsvEntity
@Data
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity
@Getter
@Setter
@AllArgsConstructor
public class CsvEntity {
@Id
@Column(name = "id")
private String id;
private String trackId;
private String artists;
private String albumName;
private String trackName;
private Long popularity;
private Long duration;
private String explicit;
private Double dance;
private Double energy;
private Long keyValue;
private Double loudness;
private Long mode;
private Double speech;
private Double acoustic;
private Double instrument;
private Double live;
private Double valence;
private Double tempo;
private Long timeSignature;
private String trackGenre;
}
만났던 오류들
application설정에 이걸 안써주면 배치 작업에 필요한 테이블이 생성되지 않음
- 필요한 테이블 목록
BATCH_JOB_EXECUTION
BATCH_JOB_EXECUTION_CONTEXT
BATCH_JOB_EXECUTION_PARAMS
BATCH_JOB_EXECUTION_SEQ
BATCH_JOB_INSTANCE
BATCH_JOB_SEQ
BATCH_STEP_EXECUTION
BATCH_STEP_EXECUTION_CONTEXT BATCH_STEP_EXECUTION_SEQ
- 근데, H2는 알아서 생성해준다고 함.. 개발 초기단계에선 H2만큼 가벼운게 없는 것 같다.
spring:
batch:
jdbc:
initialize-schema: always
mysql예약어 문제
- private String keyValue;
- 원래 이걸 그냥 key라고 써놨는데, 계속 실행해도 SQL bad grammar오류가 떴음
- 근데, 오류 명세에 예약어 관련 이라고 설명이 뜨진 않음..
- 구글링으로 해결!
DelimitedTokenizer setName오류
- 위의 오류를 해결하면서 분명 변수 이름이 잘못지어진 것 같아서 긴 이름들을 좀 짧게 줄였는데 이 과정에서 기존에 setName에 적어 놓았던 컬럼 이름들과 테이블의 컬럼이름이 다른 부분들이 생겼다.
- Duplicate match with distance <= 5 found for this property in input keys
- 위의 오류가 발생했고 처음엔 무슨 말인지 잘 몰랐는데, danceability를 dance로 줄이면서 맞는 부분이 5개 이하가 되어 생긴 문제라고 생각해서 아래와 같이 이름들을 다 바꾸고 나니 해결 됨
delimitedLineTokenizer.setNames("id","trackId", "artists","albumName","trackName","popularity","duration","explicit","dance","energy","keyValue","loudness","mode","speech","acoustic","instrument","live","valance","tempo","timeSignature","trackGenre");
varchar초과 문제
2023-03-11 21:16:15.388 WARN 36787 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1406, SQLState: 22001
2023-03-11 21:16:15.388 ERROR 36787 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data truncation: Data too long for column 'artists' at row 1
- 왜 길이 초과라고 뜨지 라고 생각했는데, 가수가 255바이트가 넘는 튜플이 존재함 !
private static List<String> getArtistName(String artists) {
List<String> artistList=new ArrayList<String>();
if(artists.length()<200){
StringTokenizer st=new StringTokenizer(artists, ";");
while(st.hasMoreTokens()){
artistList.add(st.nextToken());
}
return artistList;
}else return new ArrayList<>();
}
- 이렇게 가수 이름을 파싱해서 오류를 피했음
배열 초기화 문제
java.lang.UnsupportedOperationException: null
at java.base/java.util.AbstractList.add(AbstractList.java:153) ~[na:na]
at java.base/java.util.AbstractList.add(AbstractList.java:111) ~[na:na]
- 단순히 객체를 list에 add하는 동작이 오류를 일으킴
- 뭔가 stream foreach에서 동시성 보장이 안되는 것 때문인지 생각해서 찾아봄
- 근데, list,map 이런거에 더하는 경우는 원자성을 보장해준다.
- 좀 생각해보다,자세히 보니 AbstractList의 add를 호출한다고 적혀 있었고, 전에 @Builder를 사용하면 컬럼의 초기화가 진행되지 않는다는 경고문구를 봤었던게 기억남
- 그냥 무작정 클래스위에 빌더 어노테이션 박지말고, (그리고 데이터 어노테이션도 금지!) 따로 생성자 만들어서 빌더를 만드는게 현명한 것 같다..
- 빌더를 제거하고 따로 생성자에 직접 만들었는데도 오류가 사라지지 않음
- 해결책
- 만약 처음 객체를 생성해서 ArrayList를 초기화 할 때, new ArrayList로 초기화 하는 것이 아니라 List.asList 뭐 이런거나 Collection.SingletonList이런걸로 초기화를 하는 경우
- 소스를 뜯어보면 extends AbstractList라는 부분을 볼 수 있음
- 즉, AbstractList를 상속 받은 객체임
- 근데 얘는 add기능을 지원하지 않는다..( 직접 구현하는게 아니라면 )
- 아래처럼 구현되어 있음
- public void add(int index, E element) { throw new UnsupportedOperationException(); }
- 따라서, 초기화는 new ArrayList로 직접 해주고 addAll이나 add로 더해주는 형식으로 만들어야 한다 !!
Job의 실행 속도 문제
- savAll시, 상당히 많은 양의 select 쿼리가 발생한다.
- 청크 사이즈 키워보니 조금 빨라지긴 했지만 여전히 느리다.
- saveAll 호출 시 entity의 상태가 isNew면 persist하고 false면 merge를 하게 되는데 이 과정에서 더티체킹을 하게 되어 조회쿼리가 많이 나가게 된다.
- Persistable 구현해서, isNew를 처음에 true로 하고 @Prepersist@PostLoad 달아서 persist되기 전에 false로 바꾸는 로직을 만들었음
- 근데, 이래도 해결이 되지 않았다.
- Id에 값이 있다면 isNew=false라고 단정짓고 들어가게 된다고 한다.
- 근데 id를 generator로 만들어서 DB에 삽입되는 시점(정확히는 영속화 되는 시점)에 생성될 테니 난 isNew=false일 이유가 없어진다…
- 첫 청크에서는 이상하게 select쿼리가 나가지 않았던 것에 초점을 두고 생각을 해봤더니, 청크 크기와 상관없이 모든 첫 청크 write에서는 조회 쿼리가 나가지 않았고 이유를 생각하다 문제점을 생객해냈다.
- 즉, saveAll을 여태까지 비교를 위해 저장해둔 리스트의 모든 객체에 대해서 수행하던게 문제였다
- DB에 객체를 저장하면 해당 객체의 id가 생성됨 → 다음 chunk를 write할 때 이전 chunk에서 add한 객체들도 다 같이 다시 저장을 시도→ id가 존재하기에 select로 조회하고 update여부를 확인한 후 update가 필요하면 update한다. → 그 후, 금번 chunk로 add한 애들은 바로 insert
- 따라서, chunk마다 쓸 List를 따로 생성하여 매 chunk write시 초기화 되게끔 하고 write작업 끝나면 해당 list를 saveAll →해결 !
- List의 탐색 로직이 시간을 많이 잡아 먹는다..
- saveAll을 하지 않고 시간을 측정해보니 List 분류 및 저장하는 로직이 잡아먹는 시간이 2분정도 이상으로 매우 오래 걸렸다.
- 구글링을 하다보니 정말 큰 size의 데이터를 순차 조회하는 경우 traditional for-loop이 stream보다 성능이 빠르다고 한다.
- 만약 원시타입(int)이라면 말도안되게 빨라지고
- wrapped type(Integer , ..그냥 객체형)이면 조금 빨라진다고 한다
- 두 타입간 차이가 나는 이유는, 원시 타입의 경우 stack에 저장되기에 최적화된 for-loop이 stream보다 빠르다. 하지만. wrapped type은 heap에 저장된다. 그렇기에 직접 참조가 되는 stack보다 가져오는 시간이 더 걸리게 된다. 즉, iteration cost가 커져 둘 사이의 성능 차이가 묻히게 되는 것
- 근데, 뭐 내부 계산 로직에도 시간이 걸리게 되면 이 둘의 차이는 더더욱 줄어들게 된다고 함..
- stream 의 for each는 본래 스트림의 종료를 의미하는 print정도만을 위해 만들어진 것으로 내부에 로직을 태우는 것은 바람직 하지 않다.
- 근데 오히려 더 시간이 늘어났다.
- 아마, 단순히 반복문을 도는 것이 아닌 반복문을 돌며 여러 조건을 체크하고 하다보니 로직이 실행되는 시간 때문에 이 둘의 차이는 무의미했다.
- Map,Set같은 다른 자료형으로 데이터를 저장해보자
- Set은 중복을 허용하지 않고 객체를 set에 저장하게 되면 equal비교를 하기 까다로워 지니 pass
- Map : key를 String(가수이름/장르이름 ..) value를 객체로 두고 item의 가수가 이미 가수map에 존재한다면(key로 get해서 나온다면) 중복으로 판단한다
- 이렇게 하면 객체도 사용할 수 있게 되고, 탐색 속도 또한 O(1)로 기존 List의 O(n)과는 비교도 안 될 정도로 빨라지게 된다.
- 실제로 가수 중복과 장르 중복, row중복 체크에 map을 사용하니 시간이 엄청나게 줄었다.
- 장르의 saveall을 호출하면 시간이 꽤나 걸린다.
- 청크 5만 기준 전체 소요시간 단위(ms) (list로직)
- 1 : 42997 ( write(28000) + saveAll(15746))
- 2: 95360 (write(78000) + saveAll(16750))
- 3: 42556( write(38000) + saveAll(4270))
- 컬럼도 몇 개 없는 장르가 오래 걸리는게 이상해서 저장 직전에 저장하는 장르 리스트의 크기를 조회해 보니 거의 모든 장르를 저장하고 있었다.(심지어 중복으로)
- 중복된 장르를 저장하다보니 거의 청크 갯수와 맞먹는 save를 하는 꼴이 되었고
- 사실 이건 별로 상관 없었음
- 문제는, List에 중복과 상관없이 모든 genre를 저장하고 있었는데, 이걸 saveAll하는 경우 객체가 애초에 중복되어 저장된다.
- 만약 1번객체가 리스트에 있는데, 이걸 find로 찾아서 다시 list에 add하면 List에는 똑같은 객체가 두 개 저장됨.. (신기하네;)
- 그렇기에 저장을 쭉 하려하는데 이미 id가 있는 (먼저 영속화된 객체와 같은 객체)놈을 저장하려면 merge를 해야 함(근데 왜 select는 명시적으로 안나가지.. 뭐 bulk insert때문인가?) 따라서, 저장 전 확인하는 부분에서 딜레이가 많이 생기는 것이었다 !
- 장르 중복저장 이슈해결
- 청크 5만 기준 전체 소요시간 단위(ms) (list로직)
- DB에 없어야 할 중복이 존재한다
- 음악, 앨범 테이블에 중복이 발생함
- csv열어서 중복 생긴 부분 실제로 보니 진짜 그냥 데이터가 중복된거임(LCD soundsystem-losing my edge 4개 있음..;)
- 각 row마다 trackid는 고유해야 하니 이거로 중복성 검사 →해결
참고
https://gksdudrb922.tistory.com/153
https://renuevo.github.io/spring/batch/spring-batch-chapter-2/
https://sigridjin.medium.com/java-stream-api는-왜-for-loop보다-느릴까-50dec4b9974b
https://backtony.github.io/spring/2022-01-29-spring-batch-11/
'Spring' 카테고리의 다른 글
spring boot v3에 스웨거 적용하기 (0) | 2023.04.18 |
---|---|
[Spring Boot] SMTP를 이용해 이메일 인증을 구현해보자 (0) | 2023.04.17 |
ExceptionHandling (0) | 2023.04.05 |
Spring Security - Cors. setAllowCredentials (0) | 2023.04.05 |
What is JWT?? (2) (With OAuth2.0) (0) | 2023.04.05 |