자바 성능 튜닝 이야기를 보고 한 번 포스팅한 내용인데 다시 요약해보면서 리마인드!

 

 

String 클래스

  • String 클래스는 문자열을 불변(immutable)하게 다룹니다.
    • immutable 객체는 생성 후에 내부 상태를 변경할 수 없습니다. 문자열의 내용이 변경될 때마다 새로운 String 객체를 생성해야 한다는 것을 의미하며 이러한 객체는 변경할 수 없으므로 여러 스레드에서 동시에 접근하더라도 안전하게 사용할 수 있습니다.
  • 한 번 생성된 String 객체는 변경될 수 없으며, 문자열의 변경이 필요할 경우 새로운 String 객체를 생성합니다.
  • 이로 인해 String 클래스를 이용한 문자열 연산이 빈번하게 일어나는 경우, 성능이 저하될 수 있습니다. (GC 대상이 늘어남에 따라..메모리 사용을 최소화 하는 것은 당연한 일!)

StringBuffer 클래스

  • StringBuffer 클래스는 문자열을 가변(mutable)하게 다룹니다.
  • 문자열의 변경이 필요할 경우, 기존 StringBuffer 객체를 변경하여 새로운 문자열을 만들지 않고도 문자열을 수정할 수 있습니다.
  • 여러 개의 문자열을 결합하거나, 문자열의 일부를 삭제하거나, 변경하는 등의 작업에 용이합니다.
  • 멀티스레드 환경에서 안전하게 동작합니다.
  • 클래스에 static으로 선언한 문자열을 변경하거나, singleton으로 선언된 클래스에 선언된 문자열일 경우!

StringBuilder 클래스

  • StringBuilder 클래스는 StringBuffer 클래스와 마찬가지로 문자열을 가변하게 다룹니다.
  • 문자열의 변경이 필요할 경우, 기존 StringBuilder 객체를 변경하여 새로운 문자열을 만들지 않고도 문자열을 수정할 수 있습니다.
  • StringBuffer 클래스와의 차이점은 멀티스레드 환경에서 안전하지 않다는 점입니다. StringBuilder 클래스는 단일 스레드 환경에서 사용하기에 적합합니다.
  • 예를 들면 메서드 내에서 사용하는 문자열인 경우!

요약하자면, String 클래스는 문자열을 변경할 수 없고, StringBuffer 클래스와 StringBuilder 클래스는 문자열을 가변하게 다룰 수 있지만, StringBuffer 클래스는 멀티스레드 환경에서 안전하며 StringBuilder 클래스는 단일 스레드 환경에서 사용하기에 적합합니다.

 

 

 

꽤 오랜만에 JPA를 사용하는 프로젝트를 진행중인데

얼마나 시간이 지났다고 연관관계 mapping하는 방법이 가물가물하다; (예전에 그렇게 학을 떼어놓고는..)

살짝 헷가리는 어노테이션 정리하고 렛츠고!

 

mappedBy와 @JoinColumn은 JPA에서 엔티티 간 관계를 매핑할 때 사용하는 어노테이션입니다.

둘 다 관계 매핑에 필요한 정보를 제공하고 있지만, 다음과 같은 차이가 있습니다.

  • mappedBy: 양방향 관계에서 "다(N)" 쪽의 엔티티에서 사용되며, "일(1)" 쪽의 엔티티에 대한 매핑 정보를 지정합니다. 이를 통해 연관된 엔티티 사이에 양방향 참조를 설정할 수 있습니다.
  • @JoinColumn: "일(1)" 쪽의 엔티티에서 사용되며, 조인 컬럼 이름을 지정하여 연관된 엔티티의 외래 키 컬럼을 매핑합니다. 이를 통해 단방향 관계에서 연관된 엔티티의 외래 키를 지정할 수 있습니다.

즉, mappedBy는 양방향 관계에서만 사용되며, 연관된 엔티티 사이의 관계를 맺을 때 사용하고, @JoinColumn은 단방향 관계에서도 사용 가능하며, 외래 키 매핑에 사용됩니다.

그러나 두 어노테이션은 함께 사용되기도 합니다. 예를 들어, @ManyToOne으로 매핑된 엔티티 클래스에서 @JoinColumn 어노테이션을 사용하여 조인 컬럼을 지정하면, 이에 대응하는 @OneToMany으로 매핑된 엔티티 클래스에서는 mappedBy를 사용하여 역방향 참조를 설정하는 경우가 많습니다.

 

예를 들어, Team과 Member가 일대다 관계를 가지고 있다고 가정해봅시다. 이때 Team엔티티에서 Member엔티티의 리스트를 매핑하고, Member엔티티에서는 Team엔티티와의 관계를 역방향 매핑하려면 다음과 같이 mappedBy를 사용할 수 있습니다.

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // getters, setters
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private Long id;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    // getters, setters
}

팀에서 처음으로 Spring Batch를 사용하다 보니 문제 상황이 생기면 물어볼수도 없어서 혼자 끙끙대는 중인데 가끔 스스로 에게 하는 질문이나 의문 자체가 잘못된 느낌도 들고, Spring Batch에 대해 다시 처음부터 공부하고 시작해야 하는게 아닌가 하는 불안감도 들고 개발 하는 내내 싱숭생숭하다; 개발하면서 또 이런 적은 처음이라 당황스럽다-_-; 일단 개발은 시작했는데 내일은 다시 좀 기본 컨셉, 개념들을 살펴보고 코드들을 전체적으로 훑어봐야겠다..

 

1) 복잡한 Query를 질의해야 한다.

 

단순 조인으로만 이루어진 쿼리가 아니라 union all이나 여러 개의 내부 select절이 포함된 복잡한 쿼리를 Reader에서 던져야 했다. (JPA를 사용하는 스프링 부트 프로젝트에선 QueryDSL로도 좀 만들기 까다로운 경우에는 Native Query를 사용해서 질의를 해왔다.) 후보에는 JpaPagingItemReader, JdbcPagingItemReader, JdbcCursorItemReader이 올랐고, Cursor의 경우는 Socket Time Out의 문제로 Connection 이 끊어지지 않게끔 설정을 해줘야 되는 번거로움이 존재했다. 다른 블로그에서도 Paging을 추천하기도 했고, Jpa가 익숙해서 JpaPagingItemReader를 사용하고 있었는데 JpaPagingItemReader로는 복잡한 쿼리를 질의하는 케이스를 아무리 찾아봐도 보이지가 않는다(그때는..) 여차 저차 비슷한 걸 찾아서 JpaNativeQueryProvider를 이용해보았는데 실패; (메인 쿼리 전에 Jpa 에서 사전? 쿼리를 날리는데 이게 문제가 된다) 그래서 JdbcPagingItemReader를 사용해보려고 했는데 setSelectClase()같이 쿼리를 구분해서 넣어주기엔 쿼리가 너무 복잡해서 어쩔 수 없이 JdbcCursorItemReader를 사용하게 됐다. 

 

    @Bean
    public JdbcCursorItemReader<TestEntity> jdbcCursorItemReader() {
        return new JdbcCursorItemReaderBuilder<TestEntity>()
                            .fetchSize(chunkSize)
                            .dataSource(dataSource)
                            .rowMapper(new TestEntityMapper()) // 원래는 이 부분에서 BeanPropertyRowMapper를 사용했었음
                            .sql(getQuery())
                            .name("jdbcCursorItemReader")
                            .build();
    }

간단하게 바꿔서 가져와 봤다. 여기서 또 다른 난관을 맞닥뜨리게 되는데; rowMapper 부분이다;

현재 DB는 PostgreSQL을 쓰고 있고, 컬럼 타입이 Array인 경우가 많다. 이런 경우에는 아래와 같이 타입을 선언해주고 CRUD를 처리했는데 JdbcCursorItemReader의 rowMapper로 전달한 BeanPropertyRowMapper가 해당 타입을 변환해주지 못하고 에러를 뱉는다. 

@Column(name = "emails", columnDefinition = "varchar[]")
@Type(type = "~~~.CustomStringArrayType")
private String[] emails;

그래서 RowMapper를 Implements한 Custom Mapper를 생성해주고 아래와 같이 Array.타입을 변환해 mapping해주게 하였다.

public class TestEntityMapper implements RowMapper<TestEntity> {
    @Override
    public RiExpirationTarget mapRow(ResultSet rs, int rowNum) throws SQLException {
        TestEntity testEntity = new TestEntity();
        testEntity.setEmails((String[]) rs.getArray("emails").getArray());
     	~~~~
        return testEntity;
    }
}

여기까지 왔는데 아래 포스팅을 발견...  갑자기 이런 의문이 든다;

아무리 복잡한 쿼리라고 해도 우아한 형제들에서 쓰는 쿼리보다 복잡하려나? 내가 혹시 이상한 길로 간게 아닐까 하는 의구심이 들며... 우선은 이번 프로젝트를 마치고 바로 해당 부분을 리팩토링 하는 것을 목표로 삼겠다.. 

https://techblog.woowahan.com/2662/

 

2) Processor에서 DB에 접근해 데이터를 가져오는 것이 찝찝한 느낌.. 그래도 될까? (Reader에서 처리되어야 할 것 같은 너낌 적인 너낌)

 

Spring Batch의 Processor 단계에서 DB에 접속해서 데이터를 가져오는 로직이 존재해도 된다고 한다.

Processor는 일반적으로 데이터의 변환 및 가공을 수행하는 단계로 데이터 소스에서 필요한 데이터를 가져와 가공하는 것이 일반적이라고 한다.

 

그러나 다음과 같은 사항에 유의해야 한다고 한다.

  1. 성능 문제: Processor가 데이터 소스에 매번 접속하고 데이터를 가져오는 것은 성능상 좋지 않을 수 있습니다. 따라서 대량의 데이터를 처리할 때는 Chunk 단위로 처리하거나, 데이터베이스 쿼리 최적화를 고려해야 합니다.
  2. 트랜잭션 처리: Processor에서 데이터 소스에 접속하고 데이터를 가져올 때, 트랜잭션 처리에 대한 고민이 필요합니다. 

따라서 Processor에서 DB에 접속해서 데이터를 가져오는 로직이 존재해도 되지만, 성능과 트랜잭션 처리에 대한 고민이 필요한데 Spring Batch에서는 일반적으로 Chunk 단위로 트랜잭션 처리를 하므로, Processor에서도 Chunk 단위로 트랜잭션 처리를 해야한다. 

Processor에서 사용하는 메소드에 @Transactional을 선언할 수 있지만, 이 경우 Chunk 단위로 트랜잭션이 처리되지 않을 수 있다. 따라서 @Transactional을 사용할 경우, Processor가 Chunk 단위로 실행되도록 설정해야 한다.

 

Chunk 단위로 트랜잭션 처리를 하기 위해서는 ItemReader에서 데이터를 읽어올 때, 트랜잭션을 시작하고, ItemWriter에서 데이터를 처리한 후에 트랜잭션을 커밋하면 된다. 이렇게 Chunk 단위로 트랜잭션 처리를 하면, Processor에서 DB에 접속해서 데이터를 가져오는 로직을 작성해도 안전하게 사용할 수 있게 된다.

신규 서비스에 Java 17 버전을 사용할까 싶어 이참에 다시 한 번 자바 버전별 특징에 대해 정리해보기로 했다. 

 

 

Java 8 (2014년 3월 출시) - LTS

  • Lambda 표현식 추가 (익명 함수를 생성할 수 있는 기능)
  • Stream API 추가 (컬렉션 요소를 다루는 기능)
  • Date and Time API(Joda Time 기반) 추가 (thread-safe)
  • PermGen 메모리 영역 삭제, 메모리 구조 변경
  • Optional 클래스 (null 값을 처리)
  • 메서드 참조
  • 인터페이스 변경 (default 메서드와 static 메서드를 추가)
  • Completable Future (멀티 스레드 프로그래밍)

Java 9 (2017년 9월 출시)

  • 모듈 시스템 추가(필요한 모듈만 로드하여 불필요한 모듈을 로드하지 않아 보다 경량화된 애플리케이션 개발 가능)
  • 컬렉션 팩토리 메서드 강화
    • Set, List, Map 인터페이스에 Immutable 객체를 생성할 수 있는 새로운 메서드가 추가되었다.
      • List.of("a", "b", "c"), Set.of("a","b"), Map.of("a","abc", "가", "가나다")
  • 인터페이스의 private 메소드 지원
  • G1 GC 기본 GC로 설정 

Java 10 (2018년 3월 출시)

  • 지역 변수 형 추론(var) 추가
  • GC 인터페이스 개선 (Java 10에서는 G1 GC의 최대 힙 크기에 대한 기본값이 변경)

Java 11 (2018년 9월 출시) - LTS

  • HTTP 클라이언트 API 추가 (자바에서도 http로 api 호출이 가능했음 이전에는 라이브러리를 이용해야지만 가능했음)
  • String 클래스에 새로운 메소드 추가 (isBlank(), strip() 등)
  • Files 클래스에 새로운 메소드 추가 (writeString(~), readString() 등)
  • 컬렉션 인터페이스에 새로운 메소드 추가 (.toArray()) -> 원하는 타입의 배열을 선택하여 반환 가능해짐
    • String[] sampleArray = Arrays.asList("Java", "Kotlin").toArray(String[]::new);

Java 12 (2019년 3월 출시)

  • Switch 표현식 개선
  • G1 GC 개선

Java 13 (2019년 9월 출시)

  • Switch 표현식 개선(불필요한 break 문 없이 사용 가능)
String fruit = "banana";
int numLetters = switch (fruit) {
    case "apple", "pear" -> 5;
    case "banana", "kiwi", "orange" -> 6;
    case "avocado", "mango" -> 7;
    default -> 0;
};
  • Text Blocks 추가 (이후 14에서 공식 지원됨)
    • 문자열을 더욱 가독성 있게 작성할 수 있습니다. Text Blocks는 따옴표 대신 백틱(backtick) 기호(`)를 사용하고, 여러 줄의 문자열을 쉽게 작성할 수 있습니다.
String xml = """
    <document>
        <content>
            Text Blocks Example
        </content>
    </document>
""";

Java 14 (2020년 3월 출시)

  • Switch 표현식 개선(화살표 연산자 사용 가능)
  • Record 클래스 추가(데이터 저장 클래스로 final로 선언, getter 메서드와 equals() 및 hashCode() 메서드 자동 생성)
  • 패턴 매칭 instanceof 연산자
if (obj instanceof String) {
    String str = (String) obj;
    // str 객체를 사용합니다.
}

14버전에서는.. 아래와 같이 변경됨으로써 캐스팅 과정을 따로 거치지 않을 수 있게 되었다. 

if (obj instanceof String str) {
    // str 객체를 사용합니다.
}
  • NullPointerException 개선 (발생한 객체의 이름과 값이 함께 출력되어 오류 해결데 도움됨)

Java 15 (2020년 9월 출시)

  • Sealed 클래스 도입(클래스의 상속 제한 가능)
  • Hidden Classes 추가 (Reflection API를 통해 접근할 수 없는 클래스)

Java 16 (2021년 3월 출시)

  • (14의 기능으로부터 확정) instanceof 패턴 매칭 강화
  • Records 클래스 확장
  • Vector API 추가

Java 17 (2021년 9월 출시) - LTS

  • Sealed 클래스 확장
  • 패턴 매칭과 Switch 문 확장
  • AOT(Head of Time) 컴파일러 추가
  • ZGC, Shenandoah GC 개선 등 성능 향상

 

현재 11버전을 사용하고 있고, Optional이나 Stream, 디폴트인터페이스, 람다 등은 이제는 굉장히 유용하게, 빈번하게 사용하는 지라 17로 업그레이드를 할 경우 많은 부분에서 업그레이드가 되지 않을까 하는 기대감이 드는데 더욱 자세히 알아봐야겠다..

- AWS SES

 Amazon SES는 모든 애플리케이션에 통합하여 대량으로 이메일을 전송할 수 있는 클라우드 이메일 서비스 공급자입니다. Simple Mail Transfer Protocol(SMTP) 시스템을 구현하지 않아도 됩니다.

 

- 비용 & 호출 방법

애플리케이션이 Amazon Elastic Compute Cloud(Amazon EC2)에서 실행되는 경우 Amazon SES를 사용하여 추가 비용 없이 매월 62,000개의 이메일을 전송할 수 있습니다. AWS SDK를 사용하거나, Amazon SES SMTP 인터페이스를 사용하거나, Amazon SES API를 직접 호출하여 Amazon EC2에서 이메일을 보낼 수 있습니다.

 

AWS SES 클라이언트 설정

implementation platform('software.amazon.awssdk:bom:2.3.9')
implementation 'software.amazon.awssdk:sesv2:+'
    @Bean
    public SesV2Client getAmazonSimpleEmailServiceClient() {
        AwsCredentials credentials = null;
        if (StringUtils.isNotBlank(accessKey) && StringUtils.isNotBlank(secretKey)) {
            credentials = AwsBasicCredentials.create(accessKey, secretKey);
        }
        AwsCredentialsProvider provider = StaticCredentialsProvider.create(credentials);
        return SesV2Client.builder()
                .credentialsProvider(provider)
                .region(Region.of(sesRegion))
                .build();
    }

 

이메일 구성

try {
    Content subject = Content.builder().data(sendContent.getTitle()).build();
    Content content = Content.builder().data(thymeLeafHtml).build();
    Message msg = Message.builder().subject(subject).body(Body.builder().html(content).build()).build();
    EmailContent emailContent = EmailContent.builder().simple(msg).build();
    Destination destination = Destination.builder().toAddresses("test@naver.com").build();

    SendEmailRequest emailRequest = SendEmailRequest.builder()
                                                    .destination(destination)
                                                    .content(emailContent)
                                                    .fromEmailAddress("testSender@naver.com")
                                                    .build();

    sesV2Client.sendEmail(emailRequest);
} catch (SesV2Exception e) {
            log.error("Encountered an error processing sending email.\n");
            e.printStackTrace();
            throw e;
}

 

- 예외는 어떻게? (AWS SDK 사용의 경우)

예외를 사용하는 프로그래밍 언어로 AWS SDK를 사용하는 경우 SES 호출이 MessageRejectedException을 발생시킬 수 있습니다. 

 

- MIME

Multipurpose Internet Mail Extension으로 전자 우편을 위한 인터넷 표준 포맷입니다. 전자우편은 7비트 ASCII 문자를 사용하여 전송되기 때문에, 8비트 이상의 코드를 사용하는 문자나 이진 파일들은 MIME 포맷으로 변환되어 SMTP로 전송됩니다.

 

- MIME과 AWS SES의 관계?

AWS SES(Simple Email Service)는 이메일 메시지를 전송하기 위해 MIME(Multipurpose Internet Mail Extensions) 프로토콜을 사용합니다. MIME은 이메일 메시지를 텍스트, 이미지, 오디오, 비디오 등 다양한 멀티미디어 형식으로 변환하고, 인코딩 및 디코딩을 지원하는 표준 규약입니다. 이를 통해 다양한 종류의 파일을 이메일로 전송할 수 있습니다.

 

AWS SES는 MIME 형식을 준수하는 이메일 메시지를 전송할 수 있습니다. 이를 위해 AWS SES에서는 이메일 메시지 본문과 첨부 파일을 MIME 형식으로 구성해야 합니다. 이를 위해서는 이메일 메시지의 콘텐츠 유형(Content-Type), 인코딩 방식(Content-Transfer-Encoding), 그리고 각 파트의 경계선(Boundary)을 설정해야 합니다.

 

AWS SES에서는 이러한 MIME 형식으로 구성된 이메일 메시지를 전송하기 위해 다양한 API 및 SDK를 제공합니다.

이를 통해 개발자는 자신이 사용하는 프로그래밍 언어에 맞는 인터페이스를 통해 이메일을 보낼 수 있습니다.

 

따라서, AWS SES와 MIME은 이메일 전송에서 밀접한 관계가 있으며, MIME의 규약을 준수하는 이메일 메시지를 AWS SES에서 전송함으로써 다양한 형식의 파일을 이메일로 전송할 수 있습니다.\

 

-AWS SES를 이용해 이메일을 전송할 때 quoted printable( 이메일 본문에 특수 문자나 이메일 주소와 같은 ASCII 이외의 문자가 포함될 경우, 이메일 내용을 안전하게 전송하기 위해 필요한 인코딩 방식) 을 고려해야 하나요?

 

일반적으로 AWS SES(Simple Email Service)를 사용하여 이메일을 보낼 때, quoted-printable 인코딩은 자동으로 적용됩니다. 따라서 별도로 quoted-printable 인코딩을 고려할 필요는 없습니다. AWS SES를 사용하는 경우, 이메일을 전송할 때 필요한 인코딩 방식은 자동으로 선택되므로, 이메일의 내용이 안전하게 전송되도록 보장됩니다.

 

그러나, 이메일의 특정 부분에 대해 다른 인코딩 방식을 사용하고자 하는 경우, AWS SES는 이를 수행할 수 있는 API를 제공합니다. 이를 통해 별도의 인코딩 방식을 선택할 수 있습니다. 따라서 필요에 따라 인코딩 방식을 선택하여 이메일을 전송할 수 있습니다.

 

PostgreSQL

 

관계형 DBMS 중 하나로 MySQL이 오라클에 인수됨에 따라 요즘 더욱 각광을 받고 있는 추세이다.

주요 특징은 무료 오픈소스, 타 DBMS보다 안정성과 트랜잭션 및 ACID가 좋으며

JSONB나 ARRAY같은 타입을 사용할 수 있다는 장점이 있다. 

 

내가 사용하는 버전은 13.7으로 정확히 말하면 AmazonAurora Postgresql 13이다. Amazon Aurora(Aurora)는 MySQL 및 PostgreSQL과 호환되는 완전 관리형 관계형 데이터베이스 엔진이다 .

(Aurora는 볼륨 크기를 증가시킬 수 있고 또한 데이터베이스 구성 및 관리의 가장 어려운 측면 중 하나인 데이터베이스 클러스터링 및 복제를 자동화하고 표준화하는 이점이 있습니다).

 

더보기

ACID

  • 원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 예를 들어, 자금 이체는 성공할 수도 실패할 수도 있지만 보내는 쪽에서 돈을 빼 오는 작업만 성공하고 받는 쪽에 돈을 넣는 작업을 실패해서는 안된다. 원자성은 이와 같이 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency)은 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것을 의미한다. 무결성 제약이 모든 계좌는 잔고가 있어야 한다면 이를 위반하는 트랜잭션은 중단된다.
  • 독립성(Isolation)은 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 은행 관리자는 이체 작업을 하는 도중에 쿼리를 실행하더라도 특정 계좌간 이체하는 양 쪽을 볼 수 없다. 공식적으로 고립성은 트랜잭션 실행내역은 연속적이어야 함을 의미한다. 성능관련 이유로 인해 이 특성은 가장 유연성 있는 제약 조건이다. 자세한 내용은 관련 문서를 참조해야 한다.
  • 지속성(Durability)은 성공적으로 수행된 트랜잭션은 영원히 반영되어야 함을 의미한다. 시스템 문제, DB 일관성 체크 등을 하더라도 유지되어야 함을 의미한다. 전형적으로 모든 트랜잭션은 로그로 남고 시스템 장애 발생 전 상태로 되돌릴 수 있다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주될 수 있다.
Elasticsearch

 

Lucene을 기반으로 한 검색 엔진이다.

내가 생각하는 주요 특징으로는 전문검색(full-text), 역색인, 분산처리로 실시간성 빠른 검색 가능,

Schemaless, RESTful API, Multi-tenancy가 있다.

  • 전문 검색
    • 내용 전체를 색인해서 특정 단어가 포함된 문서를 검색할 수 있다.
  • 역색인 
    • 일반적인 색인은 책의 목차를 말하는 것이라면 역색인은 책 뒤편의 단어별 색인 페이지라고 보면 된다. 문서의 위치를 찾아 빠르게 접근하겠다는 목적이 아니라 문서 내의 문자 내용과 같은 내용 물에 빠르게 접근하겠다는 목적에서 나온 개념이다.
  • Schemaless
    • 비정형 문서도 자동으로 색인하고, 검색할 수 있게 한다. 
  • Multi-tenancy
    • 여러 인덱스에 걸쳐 검색이 가능하다 (필드명이 같다는 전제 하에)

 

 

참고 :

https://jaemunbro.medium.com/elastic-search-%EA%B8%B0%EC%B4%88-%EC%8A%A4%ED%84%B0%EB%94%94-ff01870094f0

 

 

1) 데이터 일부가 read/write이 안된다..

같은 Entity를 Reader로 읽고, Writer로 Update하려니까 전체가 100개라고 하면 약 50개 밖에 Update가 안되는 문제가 있었다. JpaPagingItermReader를 사용해서 그런 것이었고, 원인은 update를 chunk 단위로 함에 따라 limist과 offset를 이용해 select하는 결과값이 달라져서였다. 해결 방법은 Cursor를 사용하거나 PagingReader를 Overriding 하는 것이고, 자세한 내용은 아래에 글에 나와있다. (빨리 원인을 찾고 해결해서 다행..ㅠ)

 

https://jojoldu.tistory.com/337

 

 

Spring Batch Paging Reader 사용시 같은 조건의 데이터를 읽고 수정할때 문제

안녕하세요. 이번 시간에는 Spring Batch를 사용하시는 분들이 자주 묻는 질문 중 하나인 같은 조건의 데이터를 읽고 수정할때 어떻게 해야하는지 에 대해서 소개드리려고 합니다. 모든 코드는 Githu

jojoldu.tistory.com

 

2) 데이터를 가공해야만 한다..

DB에 저장된 원본 데이터 1row를 읽고 다른 테이블에 1row를 저장하는 설계를 바탕으로 개발을 했는데 N rows : 1 row의 데이터 연관관계가 있었다; 이에 따라 N rows를 1rows로 만드는 Step(2개)을 만들어 메인 Step전에 수행시켜 줬다. 

 

 

Main Step이 실행되기 전에 Sub1과 Sub2를 거쳐야 하며 Sub1이 실패가 나던, Sub2가 실패가 나던 무조건 StepMain은 실행해야 되는 논리구조다.

  • Sub1, Sub2 둘 다 실패가 나더라도 Step Main 은 실행
  • Sub1이 실패하면 Sub2->Main, Sub2가 실패하면 면 Sub1->Main

 

 

 

위와 같은 논리 구조를 아래와 같이 구현해봤는데, 살짝 의구심이 들었지만 테스트를 해보니 원하는 대로 동작한다. 

    @Bean
    public Job expirationNotificationJob() {
        return jobBuilderFactory.get("expirationNotificationJob")
                                .start(sub1ExpirationStep())
                                    .on("*")
                                    .to(sub2ExpirationStep())
                                    .on("*")
                                    .to(mainExpirationStep())
                                    .on("*")
                                    .end()
                                .end()
                                .build();
    }

Youtube 개발자 인큐티비의 SpringBatch 편을 참고하여 질문지를 리스트업 했습니다. 

 

  • 왜 스프링 배치를 사용하는가?
    • 대용량 데이터를 처리해야 함
    • 사용자 개입 없이 동작
    • 로깅, 통계처리, 트랜잭션 등의 비즈니스 로직 외에 배치 어플리케이션에 필요한 기능 사용 가능
    • 지정한 시간 내에 다른 어플리케이션을 방해하지 않고 수행
    • 충돌이나 중단 되었을 때 컨트롤이 가능함
  • 멱등성은 어떻게 유지하는가?
    • 멱등성 : 연산을 여러번 적용하더라도 결과가 달라지지 않는 성질
    • Spring Batch Job Parameter를 사용하여 외부에서 값을 주입받도록 하여 제어가 불가능한 코드를 제거한다.
      • ex) LocalDateTime.now()
    • 멱등성이라는 패러다임이 Spring Batch와는 적합하지 않다는 내용의 의견도 많다. (https://namocom.tistory.com/752)
      • Job Parameter를 이용해 외부에서 주입하는 것은 책임의 이동 정도로 이해하자는 의견도..
  • Spring Batch 메타 데이터 테이블은 어떤 것이 있는가?
    • (맨 하단 ERD 참고)
    • BATCH_JOB_INSTANCE (최상위)
      • Job Instance 객체의 정보를 담고 있음 
    • BATCH_JOB_EXECUTION_PARAMS
      • Job Parameter 정보를 담고 있음, Job 실행 시 사용된 파라미터 저장
      • 정규화되지 않은 형태의 테이블로 TYPE_CD 컬럼에서 저장되는 파라미터 타입을 가진다. 
    • BATCH_JOB_EXECUTION
      • Job Execution 객체의 정보를 가지고 있음, Job이 run 할 때마다 row가 추가됨
    • BATCH_STEP_EXECUTION
      • Step Execution 객체의 정보와 대응되는데 하나의 Job Execution에서 사용하는 Step 개수만큼 테이블의 row에 추가된다.
      • 배치가 돌아서 처리한 개수를 알고 싶을 때는 이 테이블을 찾아보면 된다. 
    • BATCH_JOB_EXECUTION_CONTEXT
      • 하나의 Job Execution 에 대해 하나의 Job Execution Context가 존재하며, Job레벨의 모든 데이터를 다 가지고 있다.
    • BATCH_STEP_EXECUTION_CONTEXT
      • 하나의 Step Execution 에 대해 하나의 Step Execution Context가 존재하며, 
      • JobInstance가 중단된 위치에서 다시 시작할 수 있도록 실패 후 검색되어야 하는 정보도 담고 있음
  • 배치 중간 실패하면 어떻게 처리하는지?
    • skip
      • 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능
      • default 값은 0으로 사용을 원하는 경우 반드시 0 이상의 숫자를 입력해주고, 어떤 Exception을 skip 할 것인지 반드시 명시해주어야 한다. 
      • chunk 내부에서 이뤄지는데 Read/Process/Write 하는 과정에 설정해줄 수 있다. 
    • retry (아래 그림 참고)
      • 데이터를 Process/Write 하는 과정에서 설정된 Exception이 발생했을 경우, 지정한 정책에 따라 데이터 처리 재시도 하는 기능.
      • Read과정에서 주로 발생하는 FlatFileParseException 에 대한 문제는 대부분  Skip에서 처리가 된다.
      • 예를 들면 DeadlockLoserDataAccessException 발생 시 Retry가 일어나도록 설정할 수 있다. 다른 프로세스에서 처리중인 데이터에 새로운 프로세스가 접근하면 Locak이 걸려 있어 에러가 발생하는데 Retry를 하면 성공할 수 있을 것이다. 
  • 트랜잭션 관리 왜 청크단위로 하는가?
    • Chunk 
      • 각 커밋 사이에 처리되는 row 수
    • Chunk 지향 처리란?
      • Chunk 단위로 트랜잭션을 다루는 것 
      • Reader와 Processor에서는 1건씩 다뤄지고, Writer에선 Chunk 단위로 처리
        • Reader에서 데이터를 하나 읽어옵니다
        • 읽어온 데이터를 Processor에서 가공합니다
        • 가공된 데이터들을 별도의 공간에 모은 뒤, Chunk 단위만큼 쌓이게 되면 Writer에 전달하고 Writer는 일괄 저장합니다.
    • Why Chunk?
      • 커밋을 매번 하면 비용이 많이 듭니다. 데이터가 많은 경우라면 (Spring Batch를 사용한다면 당연하겠죠?) 매번 커밋을 하는 것은 이상적이지 않고, 각 트랜잭션에서 가능한 한 많은 항목을 처리하는 것이 바람직합니다. 이러한 이유로 한 커밋 내에서 처리하는 수를 chunk로 관리하게 된 것입니다. 
      • Chunk-oriented 프로세싱을 하게 되면 다양한 기능들을 사용할 수 있는 것이 장점인데 skip, retry, 특정 Exception에 대한 Rollback, 다양한 ItemReader 그리고Cursor, Paging 등이 대표적이다.
    • Page Size와 Chunk Size
      • PagingItemReader를 사용하면 보이는 Page Size는 한번에 조회할 Item의 양이고, Chunk Size는 한번에 처리될 트랜잭션 단위이다.
      • 보편적으로 두개의 사이즈 크기는 일치하는게 좋다. 
  • Cursor 기반 vs Paging 기반
    • Cursor는 한칸씩 커서를 옮기면서 데이터 1 Row씩을 가져온다.
      • 배치 처리가 완료될 때까지 데이터를 읽어오기 때문에 DB Connection Time이 Paging보다 길다. 
      • 모든 데이터를 메모리에 저장하기 때문에 메모리 사용량이 많다.
    • Paging은 설정한 PageSize 만큼 데이터를 가져오며 데이터 결과의 순서가 보장될 수 있도록 order by 사용 필요
      • PageSize만큼 DB Connection을 읽고 종료한다. 따라서 Cursor에 비해 상대적으로 DB Connection Time이 적다. 
      • 하지만 트랜잭션을 여러번 타야하는 단점이 있긴 하다. 
  • Multi Thread & Partitioning
    • 정해진 시간 안에 많은 데이터를 처리하기 위해 성능을 높이기 위해 사용하는 방법
    • 서비스에 적재된 데이터가 적을 경우에는 Spring Batch의 기본 기능들만 사용해도 큰 문제가 없으나, 데이터가 엄청나게 많이 쌓일시 배치 애플리케이션 역시 확장이 필요
    • Multi-threaded Step (아래 그림 참고)
      • 단일 Step을 수행할 때, 해당 Step 내의 각 Chunk를 별도의 여러 쓰레드에서 실행하는 방법
      • 정한 개수(throttleLimit)만큼의 스레드를 생성하여 수행하는데 ItemReader는 반드시 Thread-safe인지 확인해야 하며(데이터를 중복으로 읽어 오지 않게 하기 위해) 스프링 배치에서 제공하는 것중 JdbcPagingItemReader, JpaPagingItemReader가 Thread-safe하다.
    • Partitioning
      • Master가 데이터를 파티셔닝 한 다음 Slave가 개별 스레드를 통해 각 파티션을 처리하는 방식
      • 각 SlaveStep은 ItemReader / ItemProcessor / ItemWriter 등을 갖고 동작하며 작업을 독립적으로 병렬 처리합니다.
    • Multi-threaded 는 Thread-safe를 신경써야 하나 Partitioning은 Thread-safe하지 않아도 됩니다. 
  • tasklet model vs chunk (reader,processor,writer)
    • Step은 Tasklet 혹은 Chunk로 처리할 수 있다. 
    • When to use
      • Tasklet 
        • 실행할 작업이 간단하여 집계가 필요없고, 실행만 필요한 경우
      • Chunk
        • 실행할 작업이 복잡하고 청크 지향 처리를 사용하는 읽기, 처리 및 쓰기와 관련된 작업 실행이 포함된다고 가정합니다.
  • Batch 실행은 어떻게 하는지 
    • Jenkins로 
  • 모니터링은 어떻게? 
    • 예전에는 Spring Batch Admin이 있었는데 deprecated 되었고, 현재는 spring에서 Spring Cloud Data Flow 를 사용하라고 한다. 
    • 요건 직접 사용해보고 내용 추가해보겠습니다~
 
 

 

Spring Batch Meta Table


 

Spring Batch Retry


Multi-thread Step


 

AsyncItemProcessor / AsyncItemWriter


Cursor vs Paging

 


참고

https://jojoldu.tistory.com/489

https://docs.spring.io/spring-batch/docs/current/reference/html/step.html#commitInterval

https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte2:brte:batch_core:skip_repeat_retry 

https://velog.io/@backtony/Spring-Batch-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1

+ Recent posts