티스토리 뷰
OpenCSV
dependency 추가
implementation("com.opencsv:opencsv:5.8")
CSVReader
CSVReader를 사용하는 여러 가지 방법
기본으로 제공하는 생성자 사용 : CSVReader()
val reader = CSVReader(Files.newBufferedReader(csvFilePath))
reader.readAll()
빌더 사용 : CSVReaderBuilder()
- 언제 사용?
- CSVReader에서 제공하지 않는 구분자를 사용할 경우
- ex.
.tsv
파일은 기본CSVReader()
를 생성해서 읽을 수 없기 때문에CSVReaderBuilder
의withCSVParser
를 지정해서CSVReader
를 생성해야 한다.
- ex.
- CSVReader에서 제공하지 않는 구분자를 사용할 경우
val reader = CSVReaderBuilder(Files.newBufferedReader(tsvFilePath))
.withCSVParser(CSVParserBuilder().withSeparator('\t').build())
.build()
reader.readAll()
CSVToBean
OpenCSV는 CSV 파일을 바로 빈(Bean) 목록으로 읽을 수 있다.
Bean Annotation
id,name,category,address,checkIn,checkOut,image1,image2,image3,facility
3000,A,HOTEL,주소1,15:00,11:00,3000 이미지1,이미지2,이미지3,수영장|피트니스 센터
3001,B,HOTEL,주소2,15:00,11:00,3001 이미지1,이미지2,이미지3,대형 회의실
3002,C,HOTEL,주소3,15:00,11:00,3002 이미지1,이미지2,이미지3,피트니스 센터|레스토랑
3003,D,PENSION,주소4,14:00,10:00,3003 이미지1,이미지2,이미지3,
헤더가 있으면 : @CsvBindByName, @CsvCustomBindName
HeaderColumnNameMappingStrategy 을 선택한다.
@NoArg
data class HotelDto(
@CsvBindByName(column = "id", required = true)
val id: Long,
@CsvBindByName(column = "name", required = true)
val name: String,
@CsvBindByName(column = "address")
val address: String,
@CsvBindByName(column = "checkIn")
val checkIn: String,
@CsvBindByName(column = "checkOut")
val checkOut: String,
@CsvBindAndJoinByName(column = "image[1-3]", elementType = String::class)
val images: MultiValuedMap<String, String>,
@CsvCustomBindByName(column = "facility", converter = ConvertSplitOnPipe::class)
val facilities: List<String>,
@CsvBindByName(column = "category")
val category: HotelCategory,
)
@CsvBindByName
- 컬럼 명 기반으로 필드를 바인딩한다
@CsvBindByName(column = "id", required = true)
val id: Long,
@CsvBindAndJoinByName
- 여러 컬럼을 하나의 필드로 Join하고 싶을 때 사용
- ex. image1, image2, image3 처럼 1,2,3의 규칙을 가진 여러 컬럼을 하나의 필드로 묶고 싶을 경우 column을
image[1-3]
으로 설정한다.
@CsvBindAndJoinByName(column = "image[1-3]", elementType = String::class)
val images: MultiValuedMap<String, String>,
@CsvCustomBindName
- 컬럼 값을 특정한 타입의 필드로 변환하고 싶을 때 사용
- converter를 지정해야 한다. 해당 클래스는
AbstractBeanField
타입이어야 한다.AbstractBeanField
를 상속받아 구현하는데,convert(String)
을 오버라이딩해서 어떻게 커스텀하게 변환할 건지 기술한다.
- ex. facility 컬럼에는
수영장|피트니스 센터
같이|
문자로 구분했다. 이 경우 converter 클래스를 지정해|
문자로 split 해서 List으로 변환할 수 있다.
@CsvCustomBindByName(column = "facility", converter = ConvertSplitOnPipe::class)\
val facilities: List<String>
class ConvertSplitOnPipe: AbstractBeanField<String, String>() {
override fun convert(value: String): List<String> {
return value.split("|")
}
}
헤더가 없으면 : @CsvBindByPosition, @CsvCustomBindByPosition
ColumnPositionMappingStrategy 를 사용한다.
@NoArg
data class HotelNoHeaderDto(
@CsvBindByPosition(position = 0)
val id: Long,
@CsvBindByPosition(position = 1)
val name: String,
@CsvBindByPosition(position = 3)
val checkIn: String,
@CsvBindByPosition(position = 4)
val checkOut: String,
@CsvBindAndJoinByPosition(position = "5-7", elementType = String::class)
val images: MultiValuedMap<Integer, String>
)
@CsvBindByPosition
- 컬럼의 위치(position) 기반으로 필드를 바인딩한다. 순서가 명확하고, 헤더가 없는 경우 사용한다.`
@CsvBindAndJoinByPosition
- 여러 컬럼을 하나의 필드로 Join하고 싶을 때 사용
- column이 아닌 position으로 묶는다.
CsvToBeanBuilder 생성
CsvToBeanBuilder<HotelDto>(reader)
.withType(HotelDto::class.java)
.withIgnoreEmptyLine(true)
.withVerifier(AvailableHotelVerifier())
.withVerifier(OnlyHotelCategoryVerifier())
.build() // CsvToBean<HotelDto>
withType
: 변환할 Bean 타입을 설정한다.- type 또는 mappingStrategy은 필수 지정해야 한다.
withIgnoreEmptyLine
- 기본은
false
- CSV 파일에 빈 라인이 있을 경우 에러가 발생한다.
CsvRequiredFieldEmptyException: Number of data fields does not match number of headers. ...(생략) Error parsing CSV line: {line}. []
- 빈 라인은 무시하고 파일을 읽고 싶을 경우
withIgnoreEmptyLine(true)
로 설정한다.
withVerifier()
- 생성된 Bean을 검증할 수 있다.
BeanVerifier
인터페이스를 구현해서 커스텀한 Verifier를 만든다.- 빈이 부정확할 경우
CsvConstraintViolationException
을 발생시킨다. - 생성된 빈을 필터링할 경우
false
를 리턴한다.
- 빈이 부정확할 경우
생성된 Bean을 확인하고 선택적으로 필터링합니다.
생성된 Bean이 어떤 방식으로든 일관성이 없어 허용되지 않는 경우 이 메서드는 CsvConstraintViolationException 발생시킵니다.
그러나 Bean이 본질적으로 정확하지만 어떤 논리적인 이유로 자동으로 필터링되어야 하는 경우 메소드는 false 반환해야 합니다.
- ex.
- 특정 호텔을 제외하고 수집하고 싶다.
- 카테고리가 호텔인 것만 수집하고 싶다.
// 블랙리스트 호텔에 포함되지 않는지 확인
class AvailableHotelVerifier: BeanVerifier<HotelDto> {
companion object {
val BLACK_LIST_HOTEL_IDS = listOf(3000L)
}
override fun verifyBean(bean: HotelDto?): Boolean {
return bean != null && BLACK_LIST_HOTEL_IDS.contains(bean.id).not()
}
}
// 카테고리가 호텔인지 확인
class OnlyHotelCategoryVerifier: BeanVerifier<HotelDto> {
override fun verifyBean(bean: HotelDto?): Boolean {
return bean != null && bean.category == HotelCategory.HOTEL
}
}
CsvToBean.parse(), CsvToBean.stream()
- parse()
- Bean 목록을
List<T>
로 반환한다.
- Bean 목록을
val hotelList: List<HotelDto> = CsvToBeanBuilder<HotelDto>(reader)
.withType(HotelDto::class.java)
.withIgnoreEmptyLine(true)
.build()
.parse()
public List<T> parse() throws IllegalStateException {
return stream().collect(Collectors.toList());
}
- stream()
- Bean 목록을
Stream<T>
로 반환한다. - List에 결과를 중간에 저장하는 연산이 없기 때문에 parse() 에 비해 약간의 시간, 공간을 절약할 수 있다.
- Bean 목록을
val hotelStream: Stream<HotelDto> = CsvToBeanBuilder<HotelDto>(reader)
.withType(HotelDto::class.java)
.withIgnoreEmptyLine(true)
.build()
.stream()
CsvToBean.iterator()
- iterator()
CsvToBeanIterator
가 반환된다.- 하나의 Line을 읽고 하나의 Bean을 반환한다.
Files.newBufferedReader(csvFilePath).use { val hotelCsvToBean = CsvToBeanBuilder<HotelDto>(reader) .withType(HotelDto::class.java) .withIgnoreEmptyLine(true) .build() .iterator() while (hotelCsvToBean.hasNext()) { val hotel = hotelCsvToBean.next() println(hotel) } }
CSV → Bean 성능 이슈는 없을까?
기본으로 CSVToBean
으로 읽을 때 멀티 스레드 기반으로 파일을 읽는다.
- Time vs Memory
- 메모리가 문제가 되지 않는 경우
CsvToBean.parse()
또는CsvToBean.stream()
을 사용하여 한꺼번에 모든 빈을 읽을 수 있다. 읽을 때 멀티 스레드로 처리된다. - 메모리가 제한적인 경우
CsvToBean.iterator()
를 사용하고 입력을 반복 처리한다. 한 번에 하나의 빈만 읽기 때문에 멀티 스레딩은 불가능하며 읽기가 느려지지만 한 번에 하나의 객체만 메모리에 있다.
- 메모리가 문제가 되지 않는 경우
- Ordered vs Unordered
- 기본적으로 OpenCSV는 주어진 데이터의 순서를 보존한다.
- 병렬 프로그래밍을 사용할 때 순서를 유지하려면 추가 작업이 필요하다.
- 순서가 중요하지 않다면
CsvToBeanBuilder.withOrderedResults(false)
를 사용하면 된다.
OpenCSV에서 파일 읽는 과정
OpenCSV에서는 위에서 언급한 것 말고도 CSV source를 읽는 과정에서 여러 Validator와 Processor를 사용할 수 있도록 제공한다.
참고자료
'Web' 카테고리의 다른 글
커넥션 연결 과정 및 타임아웃 알아보기 (feat. 와이어샤크) (0) | 2025.02.16 |
---|---|
WebSocket, SockJS, STOMP에 대해 알아보자 (0) | 2021.10.27 |
HTTP Live Streaming(HLS) 개념과 적용 예제 (0) | 2021.08.02 |
문자열 인코딩(character encoding) : ASCII, EUC-KR, UTF-8 (0) | 2020.12.13 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 도메인구성요소
- 문자집합변경
- hibernate 쿼리 로그
- 콜레이션
- 이모지입력오류
- 콜레이션변경
- 쿼리 파라미터 바인딩
- csv 라이브러리
- mysql 이모지
- mysql 온라인 ddl
- github actions components
- csv to bean
- spring retry
- jpa 쿼리 로그
- CGLIB프록시
- online ddl
- 코프링
- utf8mb3
- github actions 구성요소
- 엔티티와값객체
- spring boot3 쿼리 로그
- AOP
- http커넥션
- opencsv
- TCP연결
- github actions 기초
- tcp커넥션
- file
- 4Way Handshake
- read timeout
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함