서론 (Introduction)

JPA를 사용한 스프링 프로젝트를 기존에 존재하던 오라클 데이터베이스에 접속하자 JPA - Value '0000-00-00' can not be represented as java.sql.Date 에러가 발생했습니다. 이 에러가 발생한 원인과 해결과정에 대해서 다루어보겠습니다.


원인 파악 (Diagnosis)

먼저 상황을 재현해보도록 하겠습니다. 아래와 같이 LocalDate 타입을 가진 필드를 선언한 엔티티를 이용해 데이터베이스에서 데이터를 가져오는 과정에서 에러가 발생했습니다. 날짜가 포함된 컬럼에 '0000-00-00' 데이터를 가진 튜플이 있었는데, 이 튜블을 데이터베이스에서 조회해 클래스로 변환하는 과정에서 에러가 발생했습니다.

@Entity(name = "period_entity")
public class PeriodEntity {
    @Id
    Long id;
    Date startDate;
    Date endDate;
}


해결 과정 (Solution)

1. zeroDateTimeBehavior=convertToNull

구글링으로 해결방법을 검색한 결과, 대표적인 해결방법은 zeroDateTimeBehavior=convertToNull 를 다음과 같이 JDBC URL에 추가하는 것이었습니다. 이렇게 하면 데이터가 0000-00-00 값일때 JDBC 가 null을 대신 반환하도록 합니다. 이 방법은 간단하고 명확하게 에러를 해결하지만, 단점도 명확합니다. 이 방법은 모든 데이터베이스에 적용되는 것도 아니며, 0000-00-00 이라는 값을 null이 아닌 최소 시간으로 취급하는 경우에는 사용할 수 없습니다. 

 

2. AttributeConvert

제가 선택한 방법은 직접 변환과정을 설정하는 것이었습니다. AttributeConvert는 데이터베이스에서 읽은 데이터를 엔티티 필드와 데이터베이스 값 사이를 변환하는데 사용되는 인터페이스입니다. 아래와 같이 0000-00-00 값 일때 최소 값으로 변경함으로써 에러를 해결할 수 있습니다. 이 방법은 zeroDateTimeBehavior=convertToNull 와 달리 모든 데이터베이스에 적용할 수 있으며, 원하는 값으로 변경하 할 수 있습니다. 다음은 0000-00-00 값을 최소 시간으로 변경하는 코드입니다.

@Converter(autoApply = true)
public class ZeroDateConverter implements AttributeConverter<LocalDate,String> {

    public static final String ZERO_DATE_STRING = "0000-00-00";

    @Override
    public String convertToDatabaseColumn(LocalDate attribute) {
        return attribute.format(DateTimeFormatter.ISO_DATE_TIME);
    }

    @Override
    public LocalDate convertToEntityAttribute(String dbData) {
        if(dbData==null || dbData.equals(ZERO_DATE_STRING)){
            return LocalDate.MIN;
        }
        return LocalDate.parse(dbData,DateTimeFormatter.ISO_DATE_TIME);
    }
}


결론 (Conclusion)

지금까지 JPA를 사용하는 과정에서 0000-00-00 값을 변환하는 에러를 확인하고 이를 해결하는 과정을 살펴보았습니다. 저는 회사에서 기존에 운영하던 개발 데이터베이스에 접근하는 과정에서 위와 같은 에러가 발생했습니다. 만약 혼자만 사용하는 데이터베이스라면 0000-00-00 값을 파싱가능한 다른 값으로 변경하는 것도 좋겠지만, 하나의 디비에 여러 서비스가 물려있는 경우에는 적용할 수 없기에 위와 같은 솔루션을 사용하게 되었습니다.

 

 

상황

Spring MessageSource를 사용해 messages.properties 에서 메시지를 읽는 과정에서 한글이 아래와 같이 깨지는 상황이 생겼다.

분석

문자를 인코딩하는 과정에서 발생한 에러라고 생각되어 message.properties 파일의 인코딩을 확인한 결과 ISO-8859-1 이었지만, MessageSource는 UTF-8으로 읽어 문자열 형식이 일치하지 않는 것을 확인했다.

해결


IntelliJ에서 파일 인코딩할 때, UTF-8을 사용하도록 설정하니 한글 깨짐이 해결되었다.

목차

  1. 소개
  2. $UnionWith의 지원 버전 확인
  3. 네이티브 쿼리를 이용한 Union 연산
    • 네이티브 쿼리 실행 방법
    • MongoCollection.aggregate를 사용한 네이티브 쿼리 실행
  4. 코드 예시
    • MongoClient 설정
    • MongoCollection 가져오기
    • 매치 쿼리 생성
    • 매치 쿼리를 List<Document>로 변환
    • $unionWith로 쿼리 감싸기
    • 집계 연산 쿼리 추가
    • 최종 쿼리 실행

1. 소개

MongoDB 4.4부터 SQL의 Union all과 유사한 연산인 $UnionWith를 지원합니다. 이 기능은 데이터베이스의 여러 컬렉션을 합치는 데 사용됩니다. 그러나 Spring Data MongoDB의 최신 버전인 spring-boot-start-data-mongodb:3.0.7은 spring-data-mongodb:4.0.6에 의존하고 있어 $UnionWith와 관련된 기능을 사용할 수 없습니다.

따라서 네이티브 쿼리를 사용해야 합니다. 네이티브 쿼리는 MongoDatabase 인터페이스의 runCommand 메서드나 MongoCollectionaggregate 메서드를 사용하여 실행할 수 있습니다. 이를 통해 네이티브 쿼리 요청을 MongoDB 쿼리로 전달할 수 있습니다.

이 글에서는 MongoCollectionaggregate 메서드를 사용하여 네이티브 쿼리를 실행하는 방법을 알아보겠습니다.

2. $UnionWith의 지원 버전 확인

$UnionWith 연산을 사용하기 전에 MongoDB 서버의 버전을 확인해야 합니다. $UnionWith는 MongoDB 4.4 이상에서 지원되므로 해당 버전 이상이어야 합니다.

3. 네이티브 쿼리를 이용한 Union 연산

3.1. 네이티브 쿼리 실행 방법

네이티브 쿼리를 실행하는 방법은 MongoDatabaserunCommand 메서드나 MongoCollectionaggregate 메서드를 사용하는 것입니다. 이 중 MongoCollection.aggregate를 사용하여 네이티브 쿼리를 실행해보겠습니다.

3.2. MongoCollection.aggregate를 사용한 네이티브 쿼리 실행

MongoTemplate에서 사용할 컬렉션을 지정하여 MongoCollection 인터페이스의 객체를 가져옵니다. 그런 다음, 네이티브 쿼리를 작성하고 List<Document> 형식으로 변환한 후, MongoCollection.aggregate(List<Document>) 메서드에 전달하여 쿼리를 실행합니다.

4. 코드 예시

아래는 Spring Data MongoDB에서 Union 연산을 수행하는 예시 코드입니다.

4.1. MongoClient 설정

먼저, MongoClient를 생성하여 Spring Data MongoDB가 접근해야 할 데이터베이스를 확인합니다.

@Configuration
public class MongoConfig {

    @Bean
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017/test");
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
            .applyConnectionString(connectionString)
            .build();

        return MongoClients.create(mongoClientSettings);
    }
}

4.2. MongoCollection 가져오기

MongoTemplate에서 사용할 컬렉션을 지정하여 MongoCollection 인터페이스 객체를 가져옵니다.

mongoTemplate.getDb().getCollection(COLLECTION_NAME)

4.3. 매치 쿼리 생성

Union에 사용할 매치 쿼리를 생성하는 메서드를 작성합니다.

private List<String> generateMatchQuery(List<Target> targets) {
    return targets.stream().map(target -> {
        return "{\n"
            + "  $match :{\n"
            + "    where : \"" + target.getPageUrl() + "\",\n"
            + "    \"what.uniqueSelector\" : \"" + target.getHtmlElement() + "\",\n"
            + "    when : {\n"
            + "      $gte : ISODate(\"" + target.getApplyStart() + "\"),\n"
            + "      $lte : ISODate(\"" + target.getApplyEnd() + "\"),\n"
            + "    },\n"
            + "    \"what.eventType\" : \"" + target.getEventType() + "\"\n"
            + "  }\n"
            + "}";
    }).toList();
}

4.4. 매치 쿼리를 List<Document>로 변환

생성된 여러 개의 매치 쿼리를 바탕으로 List<Document>를 생성합니다. 첫 번째 쿼리는 그대로 추가하고, 나머지 쿼리는 $unionWith로 감싸서 리스트에 추가합니다.

private List<Document> convert(List<Target> targets){
    if(targets.size()==0){
        throw new AnalyzeServiceException();
    }
    List<String> matchQuery= generateMatchQuery(targets);
    List<Document> documents=new ArrayList<>();
    documents.add(Document.parse(matchQuery.get(0)));
    for(int idx=1;idx<targets.size();idx++){
        documents.add(Document.parse(wrapUnionCommand(matchQuery.get(idx))));
    }
    return documents;
}

private String wrapUnionCommand(String query){
    return "{ $unionWith : { coll : \""+COLLECTION_NAME+"\", pipeline: ["
        +query+"]}}";
}

4.5. 집계 연산 쿼리 추가

중복 데이터를 제거하기 위해 집계 연산 쿼리를 추가하는 기능을 구현합니다. $unionWith 쿼리는 중복 데이터를 제거하지

않으므로 집계 연산을 추가하여 중복을 제거해야 합니다.

public String query(List<Document> convertResult, String aggregationQueryJson) {
    List<Document> fullQuery= new ArrayList<>(convertResult.size()+2);
    convertResult.forEach(document -> fullQuery.add(document));
    fullQuery.add(Document.parse("{ $group: {_id: \"$$ROOT\"} }"));
    fullQuery.add(Document.parse(aggregationQueryJson));
    return convert(runAggregate(fullQuery));
}

이제 네이티브 String 쿼리를 List<Document> 형식으로 변환하는 과정을 마쳤습니다. 이를 MongoCollection.aggregate(List<Document>)에 전달하여 Union 연산을 수행할 수 있습니다.

목차

소개

이 글은 Gitlab에 러너를 등록하고 설정하는 방법에 대해 안내합니다. Gitlab의 CI/CD를 활용하기 위해서는 러너를 등록해야 하며, 이를 통해 자동화된 빌드, 테스트, 배포 등의 작업을 실행할 수 있습니다.

러너 등록하기

  1. Gitlab 러너 설치 문서를 참고하여 해당 환경에 맞게 러너를 설치합니다.
  2. Gitlab 레포지토리로 이동하여 “Settings” 메뉴에서 “CI/CD” 탭으로 이동합니다.
  3. “Runner” 항목에서 등록할 레포지토리 URL 및 등록 토큰을 확인합니다.
  4. 터미널에서 gitlab-runner register 명령을 실행합니다.
  5. URL과 토큰을 입력하고, executor는 원하는 설정으로 선택합니다. Docker를 사용하는 것을 추천합니다.
  6. 기본 이미지로 ubuntu:latest를 설정합니다.
  7. gitlab-runner verify 명령을 실행하여 러너가 정상적으로 등록되었는지 확인합니다.
  8. gitlab-runner start 명령을 실행하여 러너 서비스를 시작합니다.
  9. 다시 Gitlab 레포지토리의 “Settings” 메뉴에서 “CI/CD” 탭으로 이동합니다.
  10. 초록색 불이 나타나는지 확인합니다. 성공적으로 등록된 경우 아래 이미지와 같은 화면이 표시됩니다.
    Runner Configuration
  11. “Edit” 버튼을 클릭하여 러너를 편집합니다.
  12. “Run untagged jobs” 옵션을 선택합니다. 이 옵션은 태그가 없는 작업도 실행할 수 있도록 해줍니다.

러너 구성하기

  1. 레포지토리의 루트 디렉토리에서 .gitlab-ci.yml 파일을 생성합니다.
  2. 다음 코드를 입력하여 테스트 작업을 실행해보세요.
stages:
  - deploy

deploy:
  stage: deploy
  script:
    - echo 'hello runner'
  only:
    - main
  1. 위 코드는 간단한 테스트용 작업을 실행하는 것입니다. 다음과 같은 결과가 나타나면 성공입니다.
    Runner Test

문제 해결

만약 러너가 정상적으로 작동하지 않는다면, 다음 단계를 다시 확인해보세요:

  • Gitlab 러너 설치 문서를 참고하여 올바르게 설치되었는지 확인합니다.
  • 등록할 레포지토리 URL 및 토큰을 정확하게 입력했는지 확인합니다.
  • 편집한 러너 구성이 올바른지 다시 확인합니다.

이 글을 통해 Gitlab에 러너를 등록하고 설정하는 방법에 대해 알아보았습니다. Gitlab의 CI/CD를 효율적으로 활용하여 개발 작업을 자동화할 수 있습니다.

+ Recent posts