<개요>

- 일반적으로 많이 사용하는 지리좌표계는 위도, 경도로 이루어져있다.

  이는 실제 정확한 위치를 측정하는 것이 목표이기 때문에 소수점 표현에 제한이 없이 무한하게 표현한다. (보통 6자리)

- 특정 영역(Area) 에 대한 처리를 하기에는 적합하지 않기 때문에, 영역기반의 검색/표현에 적합한 구조가 필요하다.

- 즉, (무한->유한) 한정된 공간내에서 필요한 만큼만의 데이터(의미있는 데이터) 를 관리할 수 있도록 개선이 필요하다.

 

<내용>

- 지도뷰를 기반으로 하는 시스템에서 좌표를 기반으로 검색하는 것은 속도/공간에서 많은 손해를 본다. (대부분 특정영역 내 검색)

- 지도내에서 살짝만 움직여도 소수점 값이 변경되는데 실제 서비스내에서 의미를 갖는 값으로 보기 어려울때가 있다.

- 또한 Round처리를 통해서 어느정도 고정적으로 표현할 수는 있으나 연산에 역시 불편함이 있다.

- 지역내 검색이나 가까운 위치 등을 계산할때도 복잡한 수식을 사용해야 하며 Index 나 Key를 사용하기에 쉬운 구조는 아니다.

 

<GeoHash Concept>

전 세계를 잘라내기

- 이를 보완하기 위해서 전 세계 지역을 특정영역 단위로 잘라낸 것이 GeoHash의 기본사상이다.

- 좌표값을 특정 해시값으로 변경하여 지역기반 검색 이나 캐시 활용에서 편리하게 활용 할 수 있다. ( f(x) -> y )

- GeoHash의 결과는 특정 영역(Area) 이다.  지점(Point)이 아니다.

 

<GeoHash Algorithm>

- 알고리즘은 간단히 설명하면 Index Tree, Binary Search등과 비슷하다.

1. Latitude (-90 ~ 90), Longitude (-180 ~ 180) 범위 내에서 Binary Search를 수행한다.

2. 왼쪽에 속하면 0, 오른쪽에 속하면 1 이다. (bits)

3. 다음 구간으로 이동하여 1,2를 반복한다.

Longtitude / Latitude 변환

4. 이렇게 해서 얻은 각 bit를 하나씩 꺼내어 결합한다.

- Geohash level이 높아질 수록 더 자세한 위치를 표현해야 하기 때문에, 더 많은 bit를 필요로 하게 된다.

- latitude 1개, longitude 1개 순으로 번갈아가면서 결합한다.

 (Longitude의 값의 범위가 더 넓기 때문에 Level에 따라서 Bit가 1개 더 필요한 경우가 있고, 이 때 마지막 두 개를 연속해서 붙인다. )

 

5. 마지막으로 얻은 bits를 5개씩 나눠서 BASE32 encoding으로 변환하여 알파벳 문자를 얻을 수 있다. (2^5=32)

BASE32

6. Binary Search를 많이 반복할 수록 더욱 정확한 숫자를 얻게 되고, GeoHash 의 길이는 길어진다고 볼 수 있다.

 - GeoHash Stirng의 길이가 GeoHash Level로 생각하면 된다.

 

7. 위의 연산을 통해서 얻은 결과값은 다음과 같다.

위/경도 좌표 (37.385595, 127.122759) -> Level 2 (wy), Level 8 (wydkstzf)

위/경도 좌표 (37.384887, 127.123689) -> Level 2 (wy), Level 8 (wydksv8w)

 

*장점

- 매우 길고 큰 값을 상대적으로 짧고 저장공간을 적게 차지하는 String으로 바꿀 수 있게 된다. (Hash의 기본사상)

- GeoHash알고리즘의 특성상 prefix비교를 통해서 이웃인지 판별할 수 있다.

 예를 들어서 GeoHash를 통해서 gbsuv (Level 5)값을 얻은경우 아래의 지역들은  Neighbours 로 판별할 수 있다.

gbsvh gbsvj gbsvn
gbsuu gbsuv gbsuy
gbsus gbsut gbsuw

 

<코드>

전체코드는 다음 위치에서 확인 가능합니다.

https://github.com/ggthename/geohash

 

GitHub - ggthename/geohash: get a geohash value from a coordinate (latitude,longitude)

get a geohash value from a coordinate (latitude,longitude) - GitHub - ggthename/geohash: get a geohash value from a coordinate (latitude,longitude)

github.com

위도/경도 기반의 좌표
GeoHash Level에 따른 Binary Search 응용

 

<정리>

- 이를 통해서 특정 지역 내의 데이터값을 관리할때 Key값으로 사용할 수 있다.

 e.g) 특정지역내 위치한 상점 검색, 50 x 50내 인구 등

 

- 해당 지역간의 인접성도 복잡한 계산없이 판별할 수 있다.

 e.g) 현재 위치에서 10km내 이동가능한 곳에 있는 주유소 위치 등

 

- 최대 Level 12로 32.2mm x 18.6mm의 지역까지 표현할 수 있다. 일반적으로 Level 9 까지 사용한다.

 

<참고>

- https://en.wikipedia.org/wiki/Geohash#Technical_description

 

Geohash - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search This article is about the system for encoding geographic coordinates. For the game, see Geohashing. Public domain geocoding invented in 2008 Geohash is a public domain geocode system i

en.wikipedia.org

- https://en.wikipedia.org/wiki/Base32

 

Base32 - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Binary-to-text encoding scheme using 32 symbols Base32 is the base-32 numeral system. It uses a set of 32 digits, each of which can be represented by 5 bits (25). One way to represent

en.wikipedia.org

https://www.movable-type.co.uk/scripts/geohash.html

 

Geohash encoding/decoding

Movable Type Scripts Geohashes A geohash is a convenient way of expressing a location (anywhere in the world) using a short alphanumeric string, with greater precision obtained with longer strings. A geohash actually identifies a rectangular cell: at each

www.movable-type.co.uk

 

<개요>

이전글

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 에 대해서 정리하도록 한다.

+ Recent posts