<개요>

이전글

https://icthuman.tistory.com/entry/AWS-Java-SDK-S3-File-upload

 

AWS Java SDK - S3 File upload

<개요> - 기본적인 AWS Java SDK S3 사용예제 - 사용시 주의사항 <내용> 현재 버전(2022.05.19기준) 샘플 소스이며, 공식 가이드문서를 참고하는 것이 정확합니다. 1. Configuration @Configuration public class..

icthuman.tistory.com

 

<내용>

- 기존 버전에서는 ObjectMetadata를 null로 처리하였으나 추가요건을 처리하다보니 개선해야 할 부분이 있었습니다.

- CacheControl, ContentType

 

1. CacheControl

- 해당 파일을 이용하여 서비스할 경우 CacheControl 속성으로 max-age값을 주어서 캐시를 활용할 수 있습니다.

 

2. ContentType

- contentType을 지정하지 않을 경우 file upload시 octet-stream으로 동작하여 특정 이미지포멧의 경우 에러가 발생하는 경우를 볼수 있습니다. 

 

그 외에도 필요에 따라서 추가할 수 있는 값들을 AWS Document에 따라서 적절하게 사용하면 됩니다.

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;

    ObjectMetadata objectMetadata = new ObjectMetadata();
    objectMetadata.setCacheControl(cacheControl);
    objectMetadata.setContentType(file.getContentType());

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

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

 

<참조>

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

 

이전글
 
 - 기존에 Spring boot starter 2.1.x 버전에서는 발생하지 않았던 Exception이 2.3.x 로 오면서 발생하였다.

Caused by: org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer 

- 해당 오류는 WebClient를 통해서 API Call후 받아오는 응답의 크기가 일정이상이 될 경우 발생한다. 

 현재 사용중인 API의 응답사이즈가 1~3MB 수준으로 조정이 필요한 상태이다.

 

a. 주의해야할 점이 yml 파일내에서 다음 옵션을 사용하여 조정이 가능한 버전이 있으나

spring.codec.max-in-memory-size=20MB

 

 b. 지금 사용중인 버전에서는 동작하지 않아서 별도 설정을 적용하도록 하였다.

ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
                .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(3* 1024 * 1024))
                .build();

        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl( )
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();

 이와 같이 WebClient builder내에 추가하여 정상동작 하는 것을 확인하였다.

 

2.  Timeout 설정

- Spring WebClient를 사용할때 여러종류의 Timeout이 있다.

HttpClient httpClient = HttpClient.create()
                .tcpConfiguration(
                        client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, -A- ) //miliseconds
                                .doOnConnected(
                                        conn -> conn.addHandlerLast(new ReadTimeoutHandler( -B- , TimeUnit.MILLISECONDS))
                                                .addHandlerLast(new WriteTimeoutHandler( -C- , TimeUnit.MILLISECONDS))
                                )
                );


        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl(webClientProperties.getBaseUri())
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
Mono<String> response = webClient
                .get()
                .uri(" ")
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new Exception())
                .onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new Exception())
                .bodyToMono(String.class)
                .timeout(Duration.ofMillis( -D- ))
                .onErrorMap(ReadTimeoutException.class, e -> ))
                .onErrorMap(WriteTimeoutException.class, e -> ))

A. Connection Timeout

- Client / Server 간의 Connection을 맺기 위해 소요되는 시간을 의미한다.

- Server에서 새로 Connection을 맺을 자원의 여유가 없다면 발생할 수 있다.

- HTTP Connection에 대한 내용이기 때문에 keep-alive 옵션역시 사용가능하다.

 

B. Read Timeout

 - 데이터를 읽기 위해서 기다리는 시간을 의미한다.

 - 내가 호출한 API 가 응답을 주지 못하면 발생할 수 있다.

 - 너무 길다면? 적절히 설정해주지 않으면 응답을 받을때까지 계속 대기하게 되고, 자원이 고갈되는 현상이 발생한다.

 - 너무 짧다면? 요청을 받은쪽에서는 처리가 되었으나, 응답을 기다리던 쪽에서는 Timeout이 발생하게 되어서 불일치 상태가 발생한다. 

 

C. Write Timeout

 - 데이터를 쓰기 위해서 기다리는 시간을 의미한다.

 - 주어진 시간동안 Write Operation을 완료하지 못하면 발생할 수 있다.

 - 즉, Connection 연결 후 데이터를 보내기 시작하였으나 해당시간보다 길어지게 되면 중단된다.

 

D. reactor timeout

 - Reactive Stream은 Publisher, Subscriber, Subscription 을 통해서 비동기 / 넌블러킹 / back pressure 처리하는 개념이다.

 - 우리가 다루는 Spring WebFlux는 reactive stream의 구현체로 reactor를 사용하고 있으며 Mono / Flux가 Publisher의 구현체이다.

 - 따라서 Exception , Retry등을 처리할때도 기존 방식 대신 reactive stream의 기능을 활용해주는 것이 장점을 충분히 살릴 수 있는 방법이라고 생각한다.

 - Spring WebFlux에서는 WebClient의 호출결과를 받았을때 결과 Body를 Mono로 감싸주어 데이터를 전달하는 형태가 되는데, 해당 시간동안 데이터를 전달하지 못하게 되면 timeout 이 발생하게 된다.

 

E. Transaction Timeout과 비교 (개인적인 생각 , 틀릴 수 있음)

 - 우리가 일반적으로 DB transaction timeout을 설명할 때 Transaction Timeout > Statement Timeout > Socket Timeout 로 각 구간을 나누어서 설명하고 상위 Layer( ? 포장레이어?) 에서는 하위 Layer보다 크거나 같게 설정하는 것이 일반적이다.

 - Web 호출 역시 비슷하게 살펴본다면 Publisher Timeout > Read/Write Timeout > Connection Timeout 정도로 비슷하게 정리해 볼 수 있지 않을까 생각했다.

 

<정리>

- MSA구조에서 각 API의 응답시간과 사이즈는 적절하게 설정해야 한다.

- 특히, 각 레벨에서의 적절한 수준의 timeout 설정은 필수이다. 

- 너무 짧으면 많은 오류가 발생하게 되고, 이에 따른 side-effect (데이터 불일치, 로직의 복잡도 증가) 가 생기게 되며

- 너무 길거나 무제한으로 설정하게 되면 리소스 자원의 낭비가 발생한다.

<개요>

- 기본적인 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에 대해서 좀 더 자세히 정리하도록 하겠다.

+ Recent posts