<개요>

- 기본적인 AWS Java SDK S3 사용예제

- 사용시 주의사항

 

<내용>

현재 버전(2022.05.19기준) 샘플 소스이며, 공식 가이드문서를 참고하는 것이 정확합니다.

1. Configuration

@Configuration
public class AmazonS3Config {
    
    @Value("${aws.credentials.access-key}")
    private String accessKey;

    @Value("${aws.credentials.secret-key}")
    private String secretKey;

    @Value("${aws.region.static}")
    private String region;

    public BasicAWSCredentials awsCredentials(){
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return awsCreds;
    }

    @Bean
    public AmazonS3 amazonS3Client(){
        AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
                .withRegion(this.region)
                .withCredentials(new AWSStaticCredentialsProvider(this.awsCredentials()))
                .build();
        return amazonS3;
    }
}

- 이와 같이 accessKey, secretKey, region정보를 세팅하여 기본적인 인증을 생성하고 이를 기반으로 S3 ClientBuilder를 통해서 객체를 생성 후 Bean으로 사용한다.

- dev / stg / prod 환경에 따라서 달라질 수 있는 정보들은 외부에 관리한다.

- 주의 : 해당 키값이 탈취당할 경우 매우 위험하니 (보안,비용 등등), Public 공간에 올리는것은 주의하고 올라가게 된다면 반드시 암호화를 한다!

 

2. Repository

@Repository
public class AwsFileRepository {

    private AmazonS3 amazonS3;

    @Value("${aws.s3.bucket}")
    private String bucket;

    @Autowired
    public AwsFileRepository(AmazonS3 amazonS3){
        this.amazonS3 = amazonS3;
    }

    public FileDto upload(MultipartFile file, String prefix) throws IOException {
        SimpleDateFormat date = new SimpleDateFormat("yyyyMMddHHmmss");
        String fileName = prefix +"-"+date.format(new Date())+"-"+file.getOriginalFilename();
        String s3location = bucket +"/"+ prefix;

        amazonS3.putObject(new PutObjectRequest(s3location, fileName, file.getInputStream(), null));

        FileDto fileDto = new FileDto();
        fileDto.setS3Location(amazonS3.getUrl(s3location,fileName).toString());
        fileDto.setS3Key(prefix +"/" + fileName);
        return fileDto;
    }

    public String delete(String s3Key){
        amazonS3.deleteObject(bucket, s3Key);
        return s3Key;
    }

}

- S3 역시 외부의 저장소에 접근하는 것이므로 Repository Layer로 정의하는 것을 추천한다.

- S3 는 버킷단위로 정책을 정의할 수 있으며, 내부에 별도 폴더로 분리 저장이 가능하다. (예제 소스에서 prefix부분)

- 저장할 때의 값 bucketName "/" 이하 부분을 해당 파일에 대한 Key로 관리하면 삭제 시 편리하게 활용할 수 있다.

- 예제에서는 ObjectMetadata를 null로 사용하였으며 필요에 따라서 공식가이드에 제공되는 값을 세팅할 수 있다.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/UsingMetadata.html#object-key-guidelines

 

객체 메타데이터 작업 - Amazon Simple Storage Service

PUT 요청 헤더는 크기가 8KB 이하여야 합니다. PUT 요청 헤더에 포함되는 시스템 정의 메타데이터의 크기는 2KB 이하여야 합니다. 시스템 정의 메타데이터의 크기는 US-ASCII로 인코딩된 각 키와 값의

docs.aws.amazon.com

 

<주의사항>

- 대부분의 Public Cloud는 Endpoint와 HTTP API를 제공하고 있으며, SDK는 이것을 감싸는 구조로 되어있다. 

- 따라서 url 인코딩을 주의하여 사용해야 한다.

- 예를 들어서 얼마전에 테스트하다가 파일명에 특이한 문자들을 넣게 되었는데 아래와 같은 오류가 발생하였다.

2022-05-19 10:15:15.447 DEBUG 30235 --- [nio-8900-exec-6] com.amazonaws.request                    : Received error response: com.amazonaws.services.s3.model.AmazonS3Exception: The request signature we calculated does not match the signature you provided. Check your key and signing method. (Service: Amazon S3; Status Code: 403; Error Code: SignatureDoesNotMatch; Request ID: S80BNXP50DTW9SJM; S3 Extended Request ID: , S3 Extended Request ID: 
2022-05-19 10:15:15.449 DEBUG 30235 --- [nio-8900-exec-6] com.amazonaws.retry.ClockSkewAdjuster    : Reported server date (from 'Date' header): Thu, 19 May 2022 01:15:15 GMT
.
.
.
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5054) [aws-java-sdk-s3-1.11.762.jar:na]
	at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5000) [aws-java-sdk-s3-1.11.762.jar:na]
	at com.amazonaws.services.s3.AmazonS3Client.access$300(AmazonS3Client.java:394) [aws-java-sdk-s3-1.11.762.jar:na]
	at com.amazonaws.services.s3.AmazonS3Client$PutObjectStrategy.invokeServiceCall(AmazonS3Client.java:5942) [aws-java-sdk-s3-1.11.762.jar:na]
	at com.amazonaws.services.s3.AmazonS3Client.uploadObject(AmazonS3Client.java:1808) [aws-java-sdk-s3-1.11.762.jar:na]
	at com.amazonaws.services.s3.AmazonS3Client.putObject(AmazonS3Client.java:1768) [aws-java-sdk-s3-1.11.762.jar:na]

- 인증토큰이나 이러한 부분에 문제인줄 알았는데 유사한 문제를 겪은 사람들이 상당히 많았고

https://stackoverflow.com/questions/30518899/amazon-s3-how-to-fix-the-request-signature-we-calculated-does-not-match-the-s

 

Amazon S3 - How to fix 'The request signature we calculated does not match the signature' error?

I have searched on the web for over two days now, and probably have looked through most of the online documented scenarios and workarounds, but nothing worked for me so far. I am on AWS SDK for PHP...

stackoverflow.com

- 결국 encoding과정의 문제로 발생한 것이었다.(에러메시지를 잘 주었다면 덜 고생했을텐데;)

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/object-keys.html

 

객체 키 이름 생성 - Amazon Simple Storage Service

Amazon S3 콘솔을 사용하여 키 이름이 마침표 '.'로 끝나는 객체의 경우 다운로드한 객체의 키 이름에서 마침표 '.'가 제거됩니다. 다운로드한 객체에 보존된, 키 이름이 마침표 '.'로 끝나는 객체를

docs.aws.amazon.com

 

<개요>

- 내가 사용하는 Dto에 실제 Json필드값과 연관이 없는 getter가 존재한다면

- HttpMessageNotReadableException을 만난적이 있다면

https://icthuman.tistory.com/entry/Redis%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BA%90%EC%8B%9C-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%A3%BC%EC%9D%98%EC%A0%90

 

Redis를 활용한 캐시 사용시 주의점

<개요> - 기본생성자 유무 - Serializer / Deserializer 적용 - 클래스 패키지 위치 <내용> Q1. 기본생성자 사용 Page findDataAll (@AuthenticationPrincipal LoginUserDetails loginUserDetails, Pageable pagea..

icthuman.tistory.com

- 이전 글에서 살펴본 Ser/DeSer 와도 연관이 있다.

 

<내용>

- 현재 사용중인 Dto는 아래와 같은 Abstract class를 상속하도록 되어있다.

public abstract class AbstractCommonTable {
    public abstract Integer getPrimaryKey();
    public boolean checkAvailablePrimaryKey(){
        return //condition;
    }

    public boolean checkNotAvailablePrimaryKey(){
        return !checkAvailablePrimaryKey();
    }
}

- 여러 비지니스 Dto 에 필요한 PK의 필드명은 각각 다르지만, 이를 활용한 공통로직을 구성할때 필요하여 작성했다.

- 일반적으로 Controller 레벨에서 혹은 이를 테스트하는 코드를 작성할때 우리는 객체를 Json string으로 변환하여 사용한다.

- 여러 종류의 오픈소스가 존재하는 데 여기서는 jackson을 사용하였다. (Spring default)

ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(productInsightDto);

 

<현상>

- 그러나 Controller Testcase를 수행하였을때 org.springframework.http.converter.HttpMessageNotReadableException 을 만나게 된다.

- 구글링으로 통해서 다양한 원인을 찾을 수 있는데 정리해보면 Json 형식 오류이다.

 a. body형식 -> JSON

 b. LocalDateTime -> module등록, @JsonSerialize, @JsonFormat/@DateTimeFormat 등등

 

- 내가 발생했던 오류는 getPrimaryKey()라는 메소드가 실제 Json Data Format과 불일치하기 때문에 발생하는 것으로

@JsonIgnoreProperties({"primaryKey"})

를 설정해주면 간단히 해결된다.

 

- 추가 : 간혹 application.yml에 설정해둔 내용이나 나의 의도와는 관계없이 동작하는 경우가 있는데 ObjectMapper가 여러개 만들어있지는 않은지 확인한다.

applicatoin.yml에 설정하는 내용은 Spring에서 자동생성하는 ObjectMapper의 설정으로, 본인이 별도의 Bean으로 생성하였다면 따로 관리해주어야 한다.

 

<참조>

https://stackoverflow.com/questions/51261809/spring-boot-jackson-non-null-property-not-working

<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 이전글 https://icthuman.tistory.com/entry/Transactions-개념정리-1

 

Transactions #1 (Basic)

<개요> - Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리. - 기존에 일반적인 내용들 (e.g RDB기준), 새로 추가되는 개념들 (e.g NoSQL)을 포함하여 정리 <내용> 1. Trans..

icthuman.tistory.com

 

Single-Object and Multi-Object Operation

 

Atomicity

- error 가 발생했을때 transaction은 중단이 되어야하고, 변경되었던 내용은 삭제가 되어야 한다.

- 부분적인 실패가 발생하면 안되는 것으로 이해하면 된다.

- 키워드는 all or nothing !

 

Isolation

- 동시에 수행되고 있는 transactions는 서로에 간섭하지 않아야 한다.

- 가장 많이 나오는 예제로 다음을 살펴본다.

- emails에는 우리가 받은 이메일이 저장되고, mailboxes에서는 읽지않은 메일의 수가 저장된다고 가정할때.

TimeLine 1 2 3 4
User#1 Req INSERT INTO emails
VALUES(2,'Hello', true)
    UPDATE mailboxes
SET unread=unread+1
WHERE recipient_id=2
User#1 Res OK     OK
User#2 Req   SELECT FROM emails
WHERE recipient_id='2'
SELECT * FROM mailboxes
WHERE recipient_id='2'
 
User#2 Res   'Hello',true 0  

 - emails, mailboxes 데이터간의 불일치가 발생하게 된다.

- 이러한 현상을 Dirty Read라고 하며 커밋되지 않은 데이터를 읽기 때문에 발생하는 현상이다.

 (흔히 말하는 가장 낮은 isolation level 0 : UNCOMMITED_READ)

 

Weak Isolation Levels

 

Read Committed

No dirty reads

- commit된 데이터만 읽어야 한다.

No dirty writes

- commit된 데이터만 overwrite한다.

 

COMMIT이 수행된 데이터만 읽도록 하여 Dirty Read를 막는다. 

 

Implementing read committed

- READ_COMMITED를 실제로 구현하는 방식은?

- 수정하려고 할때 lock을 얻는다. (row, document, table level )

 - 오직 한 트랜잭션만이 lock을 보유한다. 앞 트랜잭션이 끝날때까디 대기하는 것이 원칙

- Dirty Read를 방지하기 위해서 읽을 때 lock을 얻고, 읽고난 뒤 lock을 해제하는 방식은 응답시간에 악영향을 미친다.

 -> 객체들의 전/후 값을 기억하고, 트랜잭션이 진행되는 동안에 다른 트랜잭션에서 해당 객체를 읽을때에는 이전값을 제공하는 방식으로 개선할 수 있다.

 

Snapshot Isolation and Repeatable Read

Implementing snapshot isolation

- READ COMMITED의 방식대로 하면 Dirty Read의 문제를 해결할 수 있으나 아래와 같은 문제가 여전히 존재한다.

TimeLine 1 2 3 4
User#1 SELECT * FROM accounts
where id=1
    SELECT * FROM accounts
where id=2
Account#1 Account #1 = 500 Account #1 = 600    
Account#2 Account #2 = 500   Account #2 = 400 Account #2 = 400
Transfer   UPDATE accounts
SET bal = bal+100
WHERE id=1
UPDATE accounts
SET bal=bal-100
WHERE id=2;
COMMIT;
 

- User#1이 계좌를 조회했을때 각각 500, 400으로 조회가 되어 총 잔액이 900인 상태가 된다. ( 100원이 사라짐 )

- 이러한 상태를 Read Skew, Non Repeatable Read 라고 한다.

- 다시 재조회를 하면 문제가 없지만 다음과 같은 상황에서 주의해야한다.

 A) Backup : 대부분의 작업이 오래 걸리며, 일부 백업은 이전버전 데이터, 일부 백업은 최신버전 데이터를 가져가게 되면 불일치가 영구적으로 발생하게 된다.

 B) Analytics : 전체적인 통계작업등의 정합성이 맞지 않는 상황이 발생하게 된다.

 

이러한 현상을 해결하기 위해서 일반적으로 Snapshot Isolation이 사용된다.

- 아이디어는 해당 트랜잭션 시작시 Database에서 커밋된 모든 데이터를 보고 , 이후 다른 트랜잭션에 의해서 데이터가 변경되더라도 각 트랜잭션에서는 이전 데이터만 표시되는 형태이다.

- Read-Only Long query (백업,분석) 에서 유용하게 사용될 수 있다.

 

Implementing snapshot isolation

- Read Commited 처럼, Dirty Write 를 방지하기 위해서는 write lock이 필요하다.

- 하지만 읽기에서는 lock이 필요하지 않음으로 성능을 향상시킬 수 있는 포인트가 된다. 

 (Readers never block writers, Writers never block Readers) -> Lock경합없이 쓰기를 처리하면서 장시간 실행되는 Read Query를  처리할 수 있다.

 

MVCC

- Dirty Read를 방지하기 위해 사용되는 유형을 일반화 시킨 것

- 다양하게 진행되고 있는 트랜잭션들이 서로 다른 시점에서 상태를 확인하기 위해서는 결국 Object의 여러 커밋된 버전을 유지해야 한다.

- Database가 Read Comiited만 제공하고 Snapshot Isolation을 제공하지 않는 다면 단순하게 두 가지 버전으로 접근하면 된다.

 the commited version / the overwritten-but-not-yet-commited version

- 일반적으로 Snapshot Isolation을 지원한다면 Read Commited에서도 MVCC를 사용한다.

- 전체 트랜잭션에 대해서 동일한 Snapshot을 사용함으로 Snapshot Isolation을 적용하게 된다.

- 이 때 내부적으로는 어느 transaction에 의해서 데이터가 created,deleted되었는지 기록하는 방식으로 접근하며

 update 역시 create / delete의 형태로 관리하게 된다. 

 

Repeatable read and naming confusion

- Snapshot Isolation의 경우 매우 유용한 Isolation Level임에도 불구하고 많은 Database에서 각자의 이름과 방식으로 구현하고 있다.

- 예를 들어서 Oracle : serializable, PostgerSQL/MySql : Repetable Read

- 그 이유는 SQL 표준이 만들어 질때에는 snapshot isolation의 개념이 포함되지도 않았으며 만들어 지지도 않은 상태였기 때문..

- 결국 이 책에서는 Isolation Level의 표준 정의자체에 결함이 있다고 이야기한다. (모호함, 부정확함)

 특히 우리가 표준이라고 부르는 것들에는 (e.g RFC문서) 구현에 독립적이어야 하는데 그렇지 못하며, 각 Database들이 표면적으로는 표준화 되어있지만 실제로 guarantee 하는 부분에도 많은 차이가 있다.

실제로 과거에 어플리케이션 개발을 하면서 JDBC Driver의 각 구현체의 차이에 따라서 의도치 않은 결과나 버그를 얻은 적이 있었는데 아마도 DB의 영역만큼 각 벤더사에 특화된 부분이 있을까 싶다.

 

<정리>

- Database가 READ_COMMITED, SNAPSHOT ISOLATION (REPEATABLE_READ) 을 통해서

- 동시에 Write연산들이 수행되는 상황 속에서, Read only Transaction에게 어떤 방식으로 데이터를 제공하여

- 그것들을 guarantee 할 수 있는지에 대해서 정리해봤다.

- 다음 글에서는 동시에 Write연산들이 수행되면서 발생하는 문제인 Lost Updates 에 대해서 정리하도록 한다.

<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 기존에 일반적인 내용들 (e.g RDB기준), 새로 추가되는 개념들 (e.g NoSQL)을 포함하여 정리

 

<내용>

1. Transactions

- read/write 가 일어날때 논리적인 하나의 단위로 묶어서 생각해본다.

- application에서 database에 접근할때 프로그래밍을 좀더 단순화하는데 목적이 있다고 본다. (추상화)

 

2. ACID

Atomicity, Consistency, Isolation, Durability

위와 같은 것은 일반적으로 트랜잭션의 특성으로 보고 있으며, 실제 각 DBMS에서 구현하는 방법들은 조금씩 차이가 있다.

특히, 이 책에서는 수년간 사용해왔던 이 단어들에 역시 모호한 부분들이 존재하며, 관점에 따라 차이가 있음을 소개하고 있다.

 

Atomicity

- 통상적으로 더이상 나눌수 없는 단위로 본다. 그러나 그 세부의 이해에는 약간의 차이가 있다.

- 예를 들어서 멀티쓰레드 환경에서는 하나의 쓰레드가 atomic operation을 수행할때 다른 쓰레드에서는 그 중간값을 참조할 수 없음을 의미한다. 그러나 ACID내에서 Atomicity는 concurrency의미가 아니다. 이 개념은 Isolation에 포한된다.

- ACID내에서 Atomicity는 write가 수행될때 (내부적으로 process, network, disk등의 여러 에러가 발생할 수 있는 요인이 있지만) 성공 혹은 실패가 하나로 묶이는 것을 의미한다. (committed / aborted)

 즉, "어디까지는 성공하고 어디까지는 실패하고" 가 발생하지 않도록 하는 것이 그 의미에 가까워서 이 책에서는 abortability가 좀 더 의미적으로 가깝지 않나라고 이야기한다.

 

Consistency

- 역시 다양한 영역에서 혼용되어 사용하고 있다. 예를 들어서 eventual consistency, consistent hashing, CAP, 그리고 ACID내에서 같은 단어들이지만 다른의미를 가지고 있다.

- ACID내 C의 경우 invariants가 항상 참인것에 집중한다고 볼 수 있다. 그 예로 여러가지 constraint를 이야기하고 있는데 이 책에서는 이러한 영역이 database보다는 application이 보장해야하는 것으로 보고 있다.

Data에 어떤 내용을 담을 것인가는 결국 application 에서 정해지며 A,I,D는 database의 속성이지만 consistency는 그렇게 볼수 없다는 것이다. 

- 나도 여기에 동의하는 것이 최근 NoSQL에서는 한 application의 기능에서 다루는 단위(set)를 기준으로 데이터를 구성하는 것이 일반적이며(JOIN X), 그에 따라서 각 데이터의 key값에 대한 정합성을 보장하는 것은 응답속도나 reliability에 따라서 타협하여 구현하는 모습을 통해서 체감할 수 있다고 생각한다.

 

Isolation

- 같은 record에 대해 동시에 여러 clients가 접근하여 경합이 발생했을 때 어떻게 해결할 것인가.

- 특히 우리가 일반적으로 알고 있는 Isolation level과 비슷하면서도 다른 용어, 추가적인 개념설명으로 재미있었던 부분이었다.

 (e.g dirty write, read write skew 등)

- 용어에서 설명하듯이 기본적으로 여러 transactions이 동시에 수행되더라도 각각은 별도로 격리 되어야 한다는 것이 기본적인 원칙이다. 이 때 발생할 수 있는 여러가지 데이터 이상현상이 있으며 (DIRTY READ, NON REPETABLE READ, PHANTOM READ등) 각 현상을 제거할 수 있도록 Level을 조정하여 성능과 정합성을 타협하는 것이다.

- 다만 각 database가 정의하는 isolation level는 차이가 있으며, 그것을 구현하는 방법도 다르다는 내용을 소개하고 있다.

 (e.g snapshot isolation, Inno DB consistent read 등)

 

Durability

- 결국 모든 저장소 (File, Database, Storage)는 안전하게 데이터를 저장하는 것이 목적이다.

- single-node database 에서는 이것을 보장하기 위해서 저장공간 (hdd,ssd 등)에 데이터를 기록하고, write-ahead log를 기록하고 recover event하는 형태로 접근했으며

- replicated database 에서는 복제하여 다른 노드에 저장하고, 각 transaction이 이루어질 때 write / replication을 수행하는 형태로 관리하고 있다.

 

특히 여기에는 개발자들이 놓치는 문제들이 간혹 있는데 간단히 살펴보면 다음과 같다.

- Disk에 기록한뒤 다운될 경우 Data는 잃어버리지 않으며 접근이 불가능한 것으로 접근 가능한 다른 시스템으로 대체할 수도 있다.

- Memory에 저장된 데이터는 휘발성이기 때문에 Disk가 필요하다.

- 비동기로 복제하는 시스템에서는 Leader의 상태에 따라서 최근 변경사항이 누락될 수 있다.

- Storage에도 버그나 문제는 존재하며 완벽은 없다. (e.g 정전, 펌웨어 버그 등)

- Disk는 생각보다 자주 망가진다. 갑자기 죽기도 하지만.. 천천히 망가지기도 한다. (bad sector!) 물리적인 매체가 망가진다는 것은 replicas, backup도 손상되는 것을 의미하고, 그래서 historical backup전략이 존재한다.

- 특히 SSD는 빠르지만 불량블록이 생각보다 자주 발생하고, HDD는 갑자기 죽기도 한다.
- SSD는 전원이 공급되지 않으면 온도에 따라서도 손실이 발생한다고 한다!?


결국 절대적인 보장은 이세상에 존재 하지않기 때문에 우리는 여러 기술을 복합적으로 사용하는 것이다.

 

<정리>

-  ACID는 과거부터 쭉 사용해온 용어이지만 Computing Area가 다양해지면서 비슷한 용어들의 모호함이 존재한다.

- Database에서 보장해야하는 개념들은 결국 각 벤더마다 다를 수도 있다. 

  즉 JDBC 드라이버의 실제 구현은 내가 생각하고 기대한 것과는 다를 수 있다. (과거에 update 쿼리실행 후 결과값이 변경된 건수로 나와야 하는데 0으로 나왔던 기억이 있다.)

- Consistency는 application의 영역으로 보는 것이 어떨까 라는 의견이 있다.

 (유효성을 위한 제약조건 같은 기능들은 Database가 제공할 수 있지만 결국 어떤값을 기록하는가는 Application에 달려있다.)

- 우리는 물리적인 세상에서 살고 있기 때문에 100% 보장이라는 것은 있을 수 없다.

 

Transcation에 대해서 대략적으로 살펴보았으며 다음 글에는 Atomicity와 Isolation에 대해서 좀 더 자세히 정리하도록 하겠다.

<개요>

- 기본생성자 유무

- Serializer / Deserializer 적용

- 클래스 패키지 위치

 

<내용>

Q1. 기본생성자 사용

Page<DataDto> findDataAll (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                               Pageable pageable)  {
        if(loginUserDetails.isAdmin()){
            return dataService.findAllDatas();
        }
        return new PageImpl(new ArrayList(),Pageable.unpaged(),0);
    }

- Paging처리를 위해서 JPA에서 제공되고 있는 Pageable를 사용하고 있었다.

- 여기에 Redis를 저장소로 하여 캐시를 적용하면 다음과 같은 형태로 사용이 가능하다.

@Cacheable(value="findDataAll", key="#pageable.pageSize.toString().concat('-').concat(#pageable.pageNumber)")

- 당연히 잘 될것으로 예상하고 수행하였으나 다음과 같은 Exception을 만나게 되었다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot 
 construct instance of `org.springframework.data.domain.PageImpl` (no 
 Creators, like default construct, exist): cannot deserialize from Object 
 value (no delegate- or property-based Creator)
 at [Source: (byte[])"

- 기본 생성자가 존재하지 않기 때문에 발생하는 오류로

 

A1. 기본 생성자를 가진 wrapper class를 활용하여 문제를 해결할 수 있다.

@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPage<T> extends PageImpl<T> {
    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestPage(@JsonProperty("content") List<T> content,
                    @JsonProperty("number") int page,
                    @JsonProperty("size") int size,
                    @JsonProperty("totalElements") long total) {
        super(content, PageRequest.of(page, size), total);
    }

    public RestPage(Page<T> page) {
        super(page.getContent(), page.getPageable(), page.getTotalElements());
    }
}

 

Q2. Page클래스는 다음과 같은 형태로 해결하였으나 Java 8 부터 제공하는 LocalDateTime에 대해서도 비슷한 오류가 발생한다.

기본적으로 LocalDataTime을 저장하면 "yyyy-MM-dd'T'HH:mm:ss" 와 같은 형태로 처리되기를 원하지만 아래와 같은 형태로 저장이 된다. 그래서 json 역직렬화를 할때 문제가 발생하게 된다.

{ 
  "lastPartDate":{ 
    "year":2020,
    "month":"JANUARY",
    "dayOfMonth":1,
    "dayOfWeek":"WEDNESDAY",
    "dayOfYear":1,
    "monthValue":1,
    "hour":0,
    "minute":0,
    "second":0,
    "nano":0,
    "chronology":{ 
      "id":"ISO",
      "calendarType":"iso8601"
    }
  }
}

 

A2. 역직렬화 방안

- Spring boot 1.x 대에서는 JSR-310을 지원하지 않기 때문에 dependency 에 모듈 추가가 필요하다.

-  참조사이트에 나와있는 것처럼 Custom Serializer를 구현하는 방법도 있으며,

- Spring boot 2.x 대를 사용중이면 다음과 같이 쉽게 처리를 할 수도 있다.

@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime creationDateTime;

 

 

Q3. 혹시라도 Redis에 저장하는 Class의 패키지명등을 수정하면 아래 Exception 을 만나게 된다.

com.fasterxml.jackson.databind.exc.InvalidTypeIdException

이유는 우리가 Redis에 실제 저장된 값을 조회해보면 알 수 있는데 다음과 같다.

redis 저장정보

역직렬화에 필요한 클래스 정보가 같이 들어가있는데 기존 클래스와 다르기 때문에 에러가 발생한다.

 

A3. 해당 Cache를 evict해주도록 하자. 일반적으로 데이터가 변경되는 경우에 대해서는 Refresh를 해주지만 리팩토링작업중 클래스가 변경되었을 경우에 까먹는 케이스가 많다.

 

<정리>

- 정리를 해보면 결국 Serializer / Deserializer 에 대한 이야기이다. RedisCacheConfiguration설정을 보면 대부분 다음과 같다.

RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

 

- 특히 Json내부의 상세타입을 다시 객체화 하기 위해서는

 a. 저장되어 있는 클래스 정보가 일치하고,

 b. 기본 생성자 (혹은 그 역할을 수행할 수 있는 무언가) 가 존재해야 하며, 

 c. Serialize한것과 같은 방법으로 Deserialize를 수행해야 한다.

- Redis외의 기타 저장소를 사용할때에도 동일한 

 

<참조>

https://stackoverflow.com/questions/55965523/error-during-deserialization-of-pageimpl-cannot-construct-instance-of-org-spr

 

Error during Deserialization of PageImpl : Cannot construct instance of `org.springframework.data.domain.PageImpl`

Issue is when using Spring cache with redis cache manager, not able to deserializer Spring Pageable response due to no default constructor The spring boot version used is 2.1.4.RELEASE Redis con...

stackoverflow.com

https://github.com/HomoEfficio/dev-tips/blob/master/Java8-LocalDateTime-Jackson-%EC%A7%81%EB%A0%AC%ED%99%94-%EB%AC%B8%EC%A0%9C.md

1. maven repository 연결

- https 연결시 인증서 문제

- 인증서 추가

 

2. netty

java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled

--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
-Dio.netty.tryReflectionSetAccessible=true

 

<참조>

https://stackoverflow.com/questions/57885828/netty-cannot-access-class-jdk-internal-misc-unsafe

<개요>

https://icthuman.tistory.com/entry/Spring-EventListener-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Refactoring

 

Spring @EventListener 를 활용한 Refactoring

<개요> - 서비스 내에서 로직 처리후 타 서비스 연동을 위해서 Service Call을 다시 하는 구조 - Spring에서 제공하는 ApplicationEvent, ApplicationListener를 활용하여 리팩토링 <내용> - 현재 서비스내에서 다..

icthuman.tistory.com

- 이후 Slack API를 호출하는 중 오류가 간헐적으로 발생함

 

<내용>

2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Enabled protocols: [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1]
2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Enabled cipher suites:[TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_RSA_WITH_AES_256_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384, TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDH_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_DSS_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA]
2022-02-15 13:01:43.176 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Starting handshake
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   : Secure session established
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   :  negotiated protocol: TLSv1.3
2022-02-15 13:01:43.221 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.c.ssl.SSLConnectionSocketFactory   :  negotiated cipher suite: TLS_AES_128_GCM_SHA256
2022-02-15 13:01:43.223 DEBUG 9926 --- [nio-8800-exec-3] o.a.h.conn.ssl.DefaultHostnameVerifier   : peer not authenticated

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
	at java.base/sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:526) ~[na:na]
	at org.apache.http.conn.ssl.DefaultHostnameVerifier.verify(DefaultHostnameVerifier.java:97) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:503) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:437) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.13.jar:4.5.13]
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.13.jar:4.5.13]
	at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-5.3.7.jar:5.3.7]
	at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:109) ~[spring-web-5.3.7.jar:5.3.7]

- 동기방식으로 순차처리할 경우 문제가 없으나 비동기식으로 대량 동시호출시 해당 오류가 간헐적으로 발생하고 있음

 

<정리>

- 관련이슈를 살펴보면 다음과 같다.

TLSv1.3 handshake is failing, but passes on TLSv1.2 on JDK 11. It also doesn't fail in TLSv1.2 on JDK 10

TLS 1.3 사용시 SSLSessionImpl 생성할때 PostHandshakeContext로 부터 값을 가져와서 생성하게 됨 (not ClientHandshakeContext)

PostHandshakeContext은 requestedServerNames 에 값을 할당하지 않고 있어서 빈값으로 문제가 발생함

TransportContext의 conSession필드에서 서버목록을 가져오도록 하여 해결할 수 있음

 

<참조>

https://github.com/slackapi/java-slack-sdk/issues/482

https://github.com/slackapi/java-slack-sdk/issues/465#issuecomment-632150000

https://github.com/slackapi/java-slack-sdk/pull/452

 

Add a workaround for OpenJDK 11's bug by seratch · Pull Request #452 · slackapi/java-slack-sdk

Summary Sending HTTP requests to Slack API server on OpenJDK 11 fails due to https://bugs.openjdk.java.net/browse/JDK-8211806 Requirements (place an x in each [ ]) I've read and understood th...

github.com

https://bugs.openjdk.java.net/browse/JDK-8211806

<개요>

- 서비스 내에서 로직 처리후 타 서비스 연동을 위해서 Service Call을 다시 하는 구조

- Spring에서 제공하는 ApplicationEvent, ApplicationListener를 활용하여 리팩토링

 

<내용>

- 현재 서비스내에서 다음과 같이 특정 상황이 발생했을 때 Noti하기 위해서 Slack으로 메시지를 보내는 로직들이 구성되어 있다.

 @Async("executorBean")
    public CompletableFuture<Boolean> refreshCount(){

        log.info("Before Current API Call remains {} ", countCallService.getCallCount());
        if(countCallService.getCallCount() > 0){
            sendSlackMessageInfo("");
        }
        List<MetaDto> metaList = filterService.findAllMetas();

        sendSlackMessageInfo( String.format("%s %s", metaList.size(), "metas") );
        log.info(...);
        ...
        
    }
    
    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }

 

* 문제점

- 비지니스와 관계없는 SlackService와 Dependency가 발생한다.

- Slack 이외에 다른 연동이나 로직이 추가될 수록 코드가 지저분해진다. 

- 실제 비지니스 로직과 관계없는 Message 조립에 대한 로직이 포함되고 있다.

 

<Refactoring #1>

1. Service

 @Async("executorBean")
    public CompletableFuture<Boolean> refreshCount(){

        log.info("Before Current API Call{} ", countCallService.getCallCount());
        if(countCallService.getCallCount() > 0){
             eventPublisher.publishEvent(new RefreshMetaErrorEvent(""));
        }
        List<MetaDto> metaList = filterService.findAllMetas();

        eventPublisher.publishEvent(new RefreshMetaStartEvent(metaList.size()));
        log.info(...);
        ...
        
    }

Biz Service내에서는 Event만 publish 하도록 변경하였다.

 

2. EventHandler

public class RefreshMetaEventHandler {

    private final SlackMessageService slackMessageService;

    @EventListener
    public void errorMessage(final RefreshMetaErrorEvent refreshMetaErrorEvent){
        final String message = refreshMetaErrorEvent.getMessage();

        sendSlackMessageInfo(message);
    }

    @EventListener
    public void startMessage(final RefreshMetaStartEvent refreshMetaStartEvent){
        final int count = refreshMetaStartEvent.getCount();
        final String message =  String.format("%s %s", count, "filters");
        sendSlackMessageInfo(message);
    }

    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }
}
@Getter
@RequiredArgsConstructor
public class RefreshMetaStartEvent {

    private int count;
}
@Getter
@RequiredArgsConstructor
public class RefreshMetaErrorEvent {

    private String message;
}

- 상황에 따라 Event를 수신하여 처리하는 로직은 모두 EventHandler로 모아서 구성하였다. (Service Dependency정리)

- slack연동외의 다른 로직을 추가하기 간결한 구조로 정리되었다.

 

* 남은 문제점

- Event가 많아질 수록 메소드가 늘어난다.

- EventHandler나 SlackMessageService의 경우 각 Message의 전달만을 책임져야 한다.

- Message조립에 대한 로직은 분리되는 것이 맞다.

 

<Refactoring #2>

1. Abstract Class 및 Concrete Class 정의

public abstract class AbstractRefreshMetaEvent {

    public abstract String getMessage();
}
public class RefreshMetaStartEvent extends AbstractRefreshMetaEvent{

    private final int count;

    @Override
    public String getMessage() {
        return String.format("%s %s", count, "metas");
    }
}
public class RefreshMetaDemoEvent extends AbstractRefreshMetaEvent {

    private final String svcColumnName;
    private final int count;

    @Override
    public String getMessage() {
        return String.format("%s has %s data",svcColumnName, count);
    }
}
public class RefreshMetaErrorEvent extends AbstractRefreshMetaEvent{

    private final String message;
}

- 각 Event별로 Message 조립에 대한 역할을 가져가도록 분리한다.

- 필요에 따라서 세부 Event를 추가로 정의하고 각 상황에 맞게 상세구현을 진행할 수 있다.

 

2. EventHandler

public class RefreshMetaEventHandler {

    private final SlackMessageService slackMessageService;

    @Async
    @EventListener
    public void sendMessage(final AbstractRefreshMetaEvent refreshMetaEvent){
        final String message = refreshMetaEvent.getMessage();

        sendSlackMessageInfo(message);
        // 연동 및 로직 추가구성 가능 
    }

    private void sendSlackMessageInfo(String text){
        log.info(text);
        SlackMessage message = SlackMessage
                .builder()
                .text(text)
                .color(SlackMessage.Color.GOOD)
                .build();
        slackMessageService.postMessage(message);
    }
}

- AbstractRefreshMetaEvent 타입으로 정리하고 Slack으로 전송하는 로직만 남게된다.

- 추가 연동이 발생하거나 로직 수정시 타 소스에 영향도가 낮다.

 

<정리>

- Event 발행구조 : 해당 Event발생에 따른 후속 상세로직은 Biz Service에서 알 필요가 없다.

- AbstractClass 를 활용하여 추상화하고, 세부 메세지 조립에 대한 로직은 상세클래스에서 구현한다.

- 후속 로직은 요건에 따라 비동기로 처리하여 Biz Logic에 영향이 없도록 한다.

 (비동기/넌블러킹 기술스택으로 변경하여 throughput 을 증가시키는 것은 덤!)

- 구조가 기술보다 우선이다.

 

+ Recent posts