본문 바로가기
Architecture

[Architecture] S3 버킷 검사 로직의 효율적인 설계: 다양한 패턴과 결합도 최적화

by gungle 2024. 9. 11.

S3 버킷 내부를 검사하는 로직을 Java로 구현할 때, 다양한 설계 패턴과 결합도 개념을 적용하여 효율적이고 유지보수가 용이한 설계를 만들 수 있다. 이 글에서는 여러 설계 패턴을 소개하고, 강한 결합과 느슨한 결합의 개념을 적용하여 더욱 유연한 구조를 만드는 방법을 설명한다.

 

설계 목표

  • 코드의 복잡성 감소
  • S3 관련 작업의 중앙화
  • 확장성 있는 구조 설계
  • 다른 모듈에서 쉽게 사용할 수 있는 인터페이스 제공
  • 결합도를 낮추어 유연성과 테스트 용이성 향상

 

설계 패턴 예시

Facade 패턴

public interface S3Operations {
    List<String> listBuckets();
    List<S3ObjectSummary> listObjects(String bucketName);
    void analyzeBucketContents(String bucketName);
}

public class S3Facade implements S3Operations {
    private AmazonS3 s3Client;

    public S3Facade(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    // 메서드 구현...
}

Strategy 패턴

public interface S3AnalysisStrategy {
    void analyze(AmazonS3 s3Client, String bucketName);
}

public class SizeAnalysisStrategy implements S3AnalysisStrategy {
    @Override
    public void analyze(AmazonS3 s3Client, String bucketName) {
        // 버킷 크기 분석 로직
    }
}

public class ContentTypeAnalysisStrategy implements S3AnalysisStrategy {
    @Override
    public void analyze(AmazonS3 s3Client, String bucketName) {
        // 컨텐츠 타입 분석 로직
    }
}

public class S3Analyzer {
    private AmazonS3 s3Client;
    private S3AnalysisStrategy strategy;

    public S3Analyzer(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    public void setStrategy(S3AnalysisStrategy strategy) {
        this.strategy = strategy;
    }

    public void analyze(String bucketName) {
        strategy.analyze(s3Client, bucketName);
    }
}

Template Method 패턴

public abstract class S3OperationTemplate {
    protected AmazonS3 s3Client;

    public S3OperationTemplate(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    public final void performOperation(String bucketName) {
        connectToS3();
        doOperation(bucketName);
        closeConnection();
    }

    protected abstract void doOperation(String bucketName);

    private void connectToS3() {
        // S3 연결 로직
    }

    private void closeConnection() {
        // 연결 종료 로직
    }
}

public class ListBucketOperation extends S3OperationTemplate {
    public ListBucketOperation(AmazonS3 s3Client) {
        super(s3Client);
    }

    @Override
    protected void doOperation(String bucketName) {
        // 버킷 리스팅 로직
    }
}

 

강한 결합과 느슨한 결합

강한 결합 예시

public class S3AnalyzerWithTightCoupling {
    private AmazonS3 s3Client;

    public S3AnalyzerWithTightCoupling() {
        this.s3Client = AmazonS3ClientBuilder.standard().build();
    }

    public void analyzeBucket(String bucketName) {
        List<S3ObjectSummary> objects = s3Client.listObjects(bucketName, "").getObjectSummaries();
        for (S3ObjectSummary obj : objects) {
            // 분석 로직...
        }
    }
}

이 예시에서 S3AnalyzerWithTightCoupling 클래스는 AmazonS3 클라이언트에 직접적으로 의존한다. 이는 다음과 같은 문제를 야기할 수 있다:

  • 테스트하기 어려움 (실제 S3 클라이언트가 필요)
  • 다른 구현으로 교체하기 어려움
  • 코드 재사용성 저하

느슨한 결합 예시

public interface S3ClientWrapper {
    List<S3ObjectSummary> listObjects(String bucketName);
}

public class AmazonS3Wrapper implements S3ClientWrapper {
    private AmazonS3 s3Client;

    public AmazonS3Wrapper(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    @Override
    public List<S3ObjectSummary> listObjects(String bucketName) {
        return s3Client.listObjects(bucketName, "").getObjectSummaries();
    }
}

public class S3AnalyzerWithLooseCoupling {
    private S3ClientWrapper s3ClientWrapper;

    public S3AnalyzerWithLooseCoupling(S3ClientWrapper s3ClientWrapper) {
        this.s3ClientWrapper = s3ClientWrapper;
    }

    public void analyzeBucket(String bucketName) {
        List<S3ObjectSummary> objects = s3ClientWrapper.listObjects(bucketName);
        for (S3ObjectSummary obj : objects) {
            // 분석 로직...
        }
    }
}

이 설계는 다음과 같은 이점을 제공한다:

  • 테스트 용이성 (목 객체를 사용할 수 있음)
  • 다른 S3 클라이언트 구현으로 쉽게 교체 가능
  • 코드 재사용성 향상

 

최적화된 설계: Facade 패턴과 느슨한 결합의 조합

다음은 Facade 패턴과 느슨한 결합을 함께 적용한 최적화된 설계 예시다:

// S3 작업을 위한 인터페이스
public interface S3Operations {
    List<String> listBuckets();
    List<S3ObjectSummary> listObjects(String bucketName);
    void analyzeBucketContents(String bucketName);
}

// S3 클라이언트 래퍼 인터페이스
public interface S3ClientWrapper {
    List<String> listBuckets();
    List<S3ObjectSummary> listObjects(String bucketName);
}

// Amazon S3 클라이언트 래퍼 구현
public class AmazonS3Wrapper implements S3ClientWrapper {
    private AmazonS3 s3Client;

    public AmazonS3Wrapper(AmazonS3 s3Client) {
        this.s3Client = s3Client;
    }

    @Override
    public List<String> listBuckets() {
        return s3Client.listBuckets().stream()
                       .map(Bucket::getName)
                       .collect(Collectors.toList());
    }

    @Override
    public List<S3ObjectSummary> listObjects(String bucketName) {
        return s3Client.listObjects(bucketName).getObjectSummaries();
    }
}

// Facade 구현
public class S3Facade implements S3Operations {
    private S3ClientWrapper s3ClientWrapper;

    public S3Facade(S3ClientWrapper s3ClientWrapper) {
        this.s3ClientWrapper = s3ClientWrapper;
    }

    @Override
    public List<String> listBuckets() {
        return s3ClientWrapper.listBuckets();
    }

    @Override
    public List<S3ObjectSummary> listObjects(String bucketName) {
        return s3ClientWrapper.listObjects(bucketName);
    }

    @Override
    public void analyzeBucketContents(String bucketName) {
        List<S3ObjectSummary> objects = listObjects(bucketName);
        // 분석 로직 구현
        for (S3ObjectSummary obj : objects) {
            System.out.println("Analyzing: " + obj.getKey());
            // 추가 분석 로직...
        }
    }
}

// 사용 예시
public class S3AnalyzerModule {
    private S3Operations s3Operations;

    public S3AnalyzerModule(S3Operations s3Operations) {
        this.s3Operations = s3Operations;
    }

    public void analyzeBucket(String bucketName) {
        s3Operations.analyzeBucketContents(bucketName);
    }
}

// 메인 애플리케이션
public class MainApplication {
    public static void main(String[] args) {
        AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build();
        S3ClientWrapper s3ClientWrapper = new AmazonS3Wrapper(s3Client);
        S3Operations s3Operations = new S3Facade(s3ClientWrapper);
        S3AnalyzerModule analyzer = new S3AnalyzerModule(s3Operations);

        analyzer.analyzeBucket("my-bucket");
    }
}

이 설계는 다음과 같은 이점을 제공한다:

  1. 느슨한 결합: S3ClientWrapper 인터페이스를 통해 실제 S3 클라이언트 구현과의 결합도를 낮췄다.
  2. Facade 패턴: S3Facade는 복잡한 S3 작업을 단순화된 인터페이스로 제공한다.
  3. 확장성: 다른 클라우드 스토리지 서비스로 쉽게 전환할 수 있다.
  4. 테스트 용이성: 목(mock) 객체를 사용하여 S3ClientWrapper나 S3Operations를 쉽게 대체할 수 있다.
  5. 재사용성: 각 컴포넌트를 독립적으로 재사용할 수 있다.

이 설계는 강한 결합의 단점을 극복하면서 Facade 패턴의 이점을 최대한 활용한다. 또한 향후 요구사항 변경이나 시스템 확장에 유연하게 대응할 수 있는 구조를 제공한다.

 

결론

다양한 설계 패턴과 느슨한 결합을 적용한 설계는 S3 버킷 검사 로직을 효율적으로 구현하고 관리할 수 있게 해준다. 이러한 접근 방식은 다음과 같은 이점을 제공한다:

  1. 유연성: 다양한 전략이나 구현을 쉽게 교체하거나 추가할 수 있다.
  2. 테스트 용이성: 목 객체를 사용하여 단위 테스트를 쉽게 수행할 수 있다.
  3. 재사용성: 코드의 모듈성이 향상되어 다른 프로젝트에서도 재사용하기 쉽다.
  4. 유지보수성: 코드의 구조가 명확해져 유지보수가 용이해진다.

그러나 모든 상황에서 느슨한 결합만이 최선의 선택은 아니다. 때로는 강한 결합이 더 적합할 수 있는 경우도 있다:

  1. 성능 최적화: 특정 구현에 강하게 결합된 코드가 더 나은 성능을 발휘할 수 있다. 예를 들어, 인터페이스를 통한 간접 호출 대신 직접 메서드를 호출하는 것이 더 빠를 수 있다.
  2. 단순성: 작은 규모의 프로젝트나 변경 가능성이 낮은 부분에서는 강한 결합이 코드를 더 단순하고 이해하기 쉽게 만들 수 있다.
  3. 특정 기술에 종속된 기능: 특정 라이브러리나 프레임워크의 고유한 기능을 사용해야 할 때, 강한 결합이 불가피할 수 있다.
  4. 빠른 개발: 프로토타입을 만들거나 빠른 개발이 필요한 경우, 강한 결합을 통해 개발 속도를 높일 수 있다.
  5. 리소스 제약: 매우 제한된 리소스(예: 임베디드 시스템)에서는 추상화 계층을 줄이고 직접적인 구현을 사용하는 것이 효율적일 수 있다.

따라서 설계 결정을 내릴 때는 프로젝트의 요구사항, 규모, 향후 변경 가능성, 성능 요구사항 등을 종합적으로 고려해야 한다. 느슨한 결합과 강한 결합 사이의 균형을 잡는 것이 중요하며, 때로는 두 접근 방식을 혼합하여 사용하는 것이 최적의 해결책일 수 있다.

이러한 설계 원칙을 적용하면, 단순히 개별 함수를 직접 호출하는 것보다 더 체계적이고 확장 가능한 솔루션을 제공할 수 있다. 특히 대규모 프로젝트나 팀 작업 환경에서 코드의 품질과 유지보수성을 크게 향상시킬 수 있다.

앞으로의 요구사항 변화나 시스템 확장에 대비하여, 이러한 설계 패턴과 결합도 최적화 기법을 적절히 활용하는 것이 중요하다. 동시에 각 상황에 맞는 최적의 접근 방식을 선택할 수 있는 판단력을 기르는 것도 중요하다. 결국 좋은 설계는 이론적 원칙과 실제적 요구사항 사이의 균형을 찾는 데서 나온다.

'Architecture' 카테고리의 다른 글

[Architecture] 4가지 유형의 Event Driven Architecture  (0) 2023.09.11