데이터베이스 다중화

데이터베이스 다중화란?

데이터베이스 서버를 여러 대 배치해 다양한 목적을 달성하기 위한 방법입니다.

다중화 목적

  • 고가용성(High Availability)
    • 사용 기법 : 클러스터링, 페일오버
  • 데이터 백업
    • 사용 기법 : 리플리케이션
  • 성능 확장(Scalability)
    • 사용 기법 : 스케일 아웃

따라서 상기 목적을 위해 데이터베이스는 어떻게 다중화 되는지 알아보겠습니다.

고가용성(High Availability)

고가용성이란, 장애가 발생하더라도 서비스가 끊기지 않도록 보장하는 것을 의미합니다.

데이터베이스에서 자주 거론되는 SPOF(Single Point of Failure, 단일 장애 지점) 문제도 결국 고가용성을 충족하지 못해 발생하는 문제입니다.

특히 OLTP(Online Transaction Processing) 환경에서는 매 순간 사용자 요청이 발생하기 때문에 데이터베이스가 단 한번이라도 멈추면 서비스 전체에 심각한 영향을 끼칩니다. 이 문제를 방지하기 위해 다음과 같은 방식이 활용됩니다.

  • 클러스터링(Clustering) : 여러 대의 서버를 하나의 그룹처럼 묶어 운영하여, 특정 노드가 장애를 일으키더라도 다른 노드가 역할을 이어받도록 하는 방식
  • 페일오버(Failover) : 마스터 서버가 장애를 일으켰을 때 자동으로 대기 서버를 마스터로 승격시켜 서비스 연속성을 유지하는 방식

실 사용 예시 : PostgreSQL Patroni, MySQL InnoDB Cluster

데이터 백업 리플리케이션(Replication)

데이터를 안전하게 보관하고, 동시에 읽기 부하를 분산하기 위해 리플리케이션(Replication) 기법이 활용됩니다. 리플리케이션은 한 서버에 기록된 데이터를 다른 서버로 복제하는 방법입니다.

리플리케이션의 주요 형태는 다음과 같습니다.

  • Master-Slave Replication: 하나의 마스터 서버가 쓰기 연산을 처리하고, 복제된 슬레이브 서버들이 읽기 연산을 담당합니다. 읽기 성능 확장에 유리합니다.
  • Master-Master Replication: 두 개 이상의 서버가 동시에 쓰기를 처리할 수 있는 구조입니다. 하지만 충돌 관리가 필요하기 때문에 구현이 까다롭습니다.

또한 리플리케이션은 데이터 동기화 방식에 따라 나뉩니다.

  • 동기(Synchronous): 모든 Replica에 데이터가 반영될 때까지 트랜잭션 완료를 기다림 → 데이터 일관성 ↑, 성능 ↓
  • 비동기(Asynchronous): Master에만 쓰기를 완료한 뒤 Replica는 나중에 반영 → 성능 ↑, 데이터 손실 위험 존재

이러한 특성 때문에 리플리케이션은 단순 백업 목적뿐 아니라, 읽기 전용 트래픽 분산재해 복구(Disaster Recovery) 에도 널리 활용됩니다.

실 사용 예시 : MySQL Replication, MongoDB Replica Set

성능 확장(Scalability): 샤딩과 스케일 아웃

서비스 사용자가 늘어나면 데이터베이스에 저장되는 데이터 양과 요청 처리량도 함께 증가합니다. 이를 해결하기 위해 데이터베이스는 수직 확장(Scale Up) 또는 수평 확장(Scale Out) 을 필요로 하며, 수평 확장의 가장 대표적인 방법이 샤딩(Sharding) 입니다.

  • 수직 확장(Scale Up): 데이터베이스 서버의 하드웨어 장비를 더 좋은 장비로 교체하는 방법이므로 다중화와는 관련 없습니다. 또한, 수직 확장에는 한계가 있기에, 수평 확장이 도입된 것으로 볼 수 있습니다.
  • 수평 확장(Scale Out): 기존 서버의 성능을 높이는 대신, 여러 대의 서버를 추가해 병렬로 운영하는 확장 방식입니다. 샤딩은 스케일 아웃의 구체적인 구현이라고 볼 수 있습니다.
  • 샤딩(Sharding): 데이터를 특정 기준(예: 사용자 ID, 지역 등)에 따라 여러 서버에 분산 저장하는 기법입니다. 각 샤드는 전체 데이터의 일부만을 담당하므로, 개별 서버의 부하를 크게 줄일 수 있습니다.

실 사용 예시 : MongoDB Sharding, Cassandra

마무리

데이터베이스 다중화의 목적에 따른 특성을 요약했을 때

  • 장애 대응을 위해서는 고가용성
  • 안전한 데이터 보관과 읽기 성능 향상을 위해서는 리플리케이션
  • 대규모 트래픽과 데이터 처리를 위해서는 샤딩

위와 같은 목적에 따른 기법이 생기는데, 무조건 데이터베이스 다중화 방식을 채용하는 것은 잘못하면, 오버 엔지니어링이 될 수 있습니다.

따라서 데이터베이스 다중화는 운영중인 서비스에 알맞는 스케일의 아키텍처를 설계하는 것이 가장 좋을 것으로 생각됩니다.

반응형

서로 다른 계정 간 S3 To S3 복사작업 하는 방법 with php laravel

다른 언어도 동일하게 copyObject API를 사용하면 가능하다.

사전 데이터

aws-sdk-php
A 계정으로 만든 버킷
B 계정으로 만든 버킷
A,B 버킷에 접근할 iam 계정

사전 작업

AWS 콘솔에서 iam 계정 번호를 준비해, 사용할 버킷 A, B에 아래와 같이 your_iam_ID 위치에 권한을, your_bucket에 버킷 이름을 넣는다.

  • ListBucket : 디렉토리 복사코드를 준비하기 위한 리스트 조회 권한
  • GetObject : 파일을 다운로드할 수 있는 조회 권한
  • PutObject : 파일을 업로드 할 수 있는 권한

해당 위치 접근 방법은 Amazon S3 > 버킷 > 권한 > 버킷 정책

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAccountyour_iam_IDAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::your_iam_ID:root"
            },
            "Action": [
                "s3:ListBucket",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::your_bucket",
                "arn:aws:s3:::your_bucket/*"
            ]
        }
    ]
}

CopyObject API 구현

CopyObject API는 파일 단위로 A에서 B로 옮기는 작업이다. 따라서 단일 건에 대해서는 권한만 잘 오픈해준다면, 어렵지 않은 작업이다.

단일 파일 복사 작업

use Aws\\S3\\S3Client;

class S3ToS3Transfer{

    public function fileCopy(){
        $targetS3Client = new S3Client([
            'credentials' => [
                'key' => iam_key,
                'secret' => iam_secret_Key,
            ],
            'region' => 'ap-northeast-2',
            'version' => 'latest',
        ]);

        //source 위치에 있는 파일을 key 위치로 복사
        $targetS3Client->copyObject([
            'Bucket'     => 'target_bucket',
            'Key'        => 'target_path',
            'CopySource' => urlencode('source_bucket/' . $sourcePath),
        ]);

    }

CopySource의 버킷을 명시해줘야 정상 동작 가능

디렉토리 복사 작업

디렉토리 복사는 파일의 목록을 읽어오는 ListBucket API를 활용해야 한다.

흐름은 대략 이렇다

  1. Source S3 스토리지 리스트 불러오기(ListObjectsV2 API)
  2. 불러온 리스트에서 size가 0인 디렉토리 제외하고 모두 배열에 저장(1000개 이상일 경우 페이지네이션 처리)
  3. foreach로 각 object를 돌며 단일 copyObject API 요청
    public function directoryCopy(array $data){
        $sourceS3Client = new S3Client([
            'credentials' => [
                'key' => iam_key,
                'secret' => iam_secret_key,
            ],
            'region' => 'ap-northeast-2',
            'version' => 'latest',
        ]);

        $targetS3Client = new S3Client([
            'credentials' => [
                'key' => iam_key,
                'secret' => iam_secret_key,
            ],
            'region' => 'ap-northeast-2',
            'version' => 'latest',
        ]);

        // source S3 디렉토리 탐색 시작(1000개 단위 페이징 되어있음)
        $requestParams = [
                'Bucket' => $sourceStorageInfo->virtual_path,
                'Prefix' => $sourcePath, // 조회할 디렉토리 지정
            ];

        do{
            $objects = $sourceS3Client->listObjectsV2($requestParams);

            // 다음 페이지 존재확인 IsTruncated : 1 or 0
            if ($objects['IsTruncated']){
                $requestParams = [
                    'Bucket' => $sourceStorageInfo->virtual_path,
                    'Prefix' => $sourcePath, // 조회할 디렉토리 지정
                    'ContinuationToken' => $objects['NextContinuationToken'] // 다음 페이지 토큰
                ];

                $objectsNextPageFlag = true;

            } else {
                $objectsNextPageFlag = false;
            }

            //오브젝트 리스트 파싱
            if (isset($objects['Contents'])) {
                foreach ($objects['Contents'] as $object) {
                    // 디렉토리 오브젝트 제외
                    if ($object['Size'] == 0) {
                        continue;
                    }

                    $notIncludeDirectoryObjects[] = $object;
                }
            }
            
        } while($objectsNextPageFlag); // 페이징 끝날 때까지 반복

        //배열 전체 탐색해 target S3에 파일 복사
        foreach ($notIncludeDirectoryObjects as $object) {
            $objectKey = $object['Key']; // 버킷 제외 파일 경로
            $fileNameWithExt = basename($objectKey); // 파일명 추출

            //source 위치에 있는 파일을 key 위치로 복사
            $targetS3Client->copyObject([
                'Bucket'     => 'target_bucket',
                'Key'        => 'target_path',
                'CopySource' => urlencode('source_bucket/' . $sourcePath), // 버킷 + 소스 위치
            ]);
        }
    }

리스트 조회한 데이터를 그대로 복사요청 진행하면 되는 로직

반응형

'공부 > Laravel' 카테고리의 다른 글

Laravel 5.8 TestCode  (0) 2024.06.21
Pusher를 활용한 Laravel5.8 + Javascript 소켓통신  (0) 2024.06.20

@Async 어노테이션은 Spring Framework에서 비동기 처리를 지원하기 위해 사용되는 기능

주로 시간이 오래 걸리는 작업을 별도의 쓰레드에서 실행하여 메인 쓰레드의 응답성을 향상시키기 위해 사용

기본 개념

  • @Async는 메서드를 비동기로 실행하게 만들어주는 어노테이션
  • Spring이 관리하는 TaskExecutor를 사용해 별도의 쓰레드에서 메서드를 실행
  • 메서드를 호출한 쪽은 즉시 반환되며, 실제 작업은 백그라운드에서 진행

사용 방법

  1. 설정 활성화
@Configuration
@EnableAsync
public class AsyncConfig {
    // 커스텀 TaskExecutor 설정도 가능
}
  1. 비동기로 실행할 메서드에 @Async 추가
@Service
public class MyService {

    @Async
    public void asyncMethod() {
        // 오래 걸리는 작업
        System.out.println("비동기 작업 수행 중...");
    }
}
  1. 호출 예시
@RestController
public class MyController {

    @Autowired
    private MyService myService;

    @GetMapping("/start")
    public String startAsyncTask() {
        myService.asyncMethod(); // 비동기 호출
        return "요청 처리 완료 (비동기 작업 중)";
    }
}

반환값이 있는 경우

  • Future<T>, CompletableFuture<T> 또는 ListenableFuture<T>로 감싸서 리턴 가능
@Async
public CompletableFuture<String> asyncReturnMethod() {
    return CompletableFuture.completedFuture("비동기 결과");
}

주의사항

  • @Async 메서드는 프록시 기반이므로 같은 클래스 내부에서 호출하면 비동기로 동작하지 않음
  • → 자기 자신을 @Autowired로 주입받아 사용하거나 구조 분리를 권장
  • 기본 쓰레드 풀은 제한적이므로 커스텀 TaskExecutor를 정의해주는 것이 좋음
  • @Async는 public 메서드에서만 제대로 작동함

커스텀 Executor 왜 사용?

기본 Executor는 SimpleAsyncTaskExecutor를 사용

  • Spring은 @Async를 사용할 때 별도 설정이 없으면 SimpleAsyncTaskExecutor를 사용
  • 이 Executor는 쓰레드 풀을 사용하지 않고, 호출할 때마다 새로운 쓰레드를 생성

⇒ 따라서 쓰레드 새로 무한정 생성 및 재사용 불가능

  • 쓰레드 재사용이 안 됨
  • 많은 요청이 들어오면 OutOfMemoryError 위험
  • 성능과 안정성 모두 낮음

커스텀 Executor 사용 예시

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("AsyncExecutor-");
        executor.initialize();
        return executor;
    }
}
반응형

+ Recent posts