Series/실전!

레거시 코드 개선하기 with delegate pattern

Hyunec 2022. 6. 5. 11:32
 

GitHub - Hyune-c/blogcode-delegate

Contribute to Hyune-c/blogcode-delegate development by creating an account on GitHub.

github.com

 

STEP 1. 개선 전 코드

https://github.com/Hyune-c/blogcode-delegate/commit/7ac445974e1fc497509813303d80151ce705ce1c

 

AWS S3 조작을 담당하는 서비스로 개선 전 코드는 중복 코드와 확장성에 큰 단점을 가지고 있었습니다.
초기에는 연관된 로직이 적어 괜찮았지만, 그것을 감안하더라도 가독성과 확장에 취약한 코드였습니다.
그러던 중 S3 버킷 추가의 요건이 생겼습니다.

 

STEP 2. 코드 개선

버킷 추가를 하기 전 기존 코드의 개선을 진행했습니다.

1. Properties 

기존의 코드는 service 내에 accessKey와 secretKey가 노출되어 있었고, 매 요청마다 amazonS3 오브젝트를 생성했습니다.
@Configuration
@RequiredArgsConstructor
public class AmazonS3Beans {

 public static final Regions CLIENT_REGION = Regions.AP_NORTHEAST_2;

 @Value("${cloud.aws.credentials.accessKey}")
 private String accessKey;

 @Value("${cloud.aws.credentials.secretKey}")
 private String secretKey;

 @Bean
 public AmazonS3 amazonS3() {
  final BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
  return AmazonS3ClientBuilder.standard()
    .withCredentials(new AWSStaticCredentialsProvider(credentials))
    .withRegion(CLIENT_REGION)
    .build();
 }
}
  • amazonS3을 재사용하기 위해 bean으로 선언했습니다.
  • 이후 코드에서는 accessKey, secretKey가 노출되지 않습니다.
public interface AmazonS3Properties {

 String getStagePath();
 String getBucketName();

 default String getProvendorContentUrl() {
  throw new UnsupportedOperationException("잘못된 호출 입니다.");
 }
}


@Component
public class AmazonS3PublicProperties implements AmazonS3Properties {

 @Value("${env.profile}")
 private String stagePath;

 @Value("${cloud.aws.provendor.content}")
 private String provendorContentUrl;

 @Value("${cloud.aws.bucket.public}")
 private String bucketName;
}
  • public 버킷은 url 연결이 가능한 도메인을 가집니다.
  • 인터페이스를 통해 버킷의 설정 값을 다르게 설정하고 가져올 수 있습니다.

 

2. Request 생성 책임 Provider

기존의 코드는 request object의 생성 책임 구분이 없었고, 비슷한 구현이 반복되었습니다.
public abstract class AbstractAmazonS3UploadRequestProvider {

 private final String stagePath;
 private final String bucketName;

 protected AbstractAmazonS3UploadRequestProvider(final AmazonS3Properties properties) {
  this.stagePath = properties.getStagePath();
  this.bucketName = properties.getBucketName();
 }

 public PutObjectRequest createRequest(final File file) {
  return createRequest(file.getName(), file);
 }

 public abstract PutObjectRequest createRequest(final String sourceName, final File file);

 protected PutObjectRequest createRequest(final String objectKey, final File file, final CannedAccessControlList cannedAclHeader) {
  final PutObjectRequest request = new PutObjectRequest(bucketName, objectKey, file);
  request.setMetadata(ObjectMetadataType.getMetadata(file));
  request.withCannedAcl(cannedAclHeader);

  return request;
 }

 protected String generateObjectKey(final String sourceName) {
  final LocalDateTime now = LocalDateTime.now();
  final String hash = LocalDateTimeUtils.toIsoFormat(now) + "_" + DigestUtils.md5Hex(String.valueOf(now.getNano()));
  return stagePath + "/" + hash + "/" + sourceName;
 }

 @Getter
 @RequiredArgsConstructor
 public enum ObjectMetadataType {

  CSV("text/csv; charset=UTF-8"),
  XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8"),
  XLS("application/vnd.ms-excel; charset=UTF-8"),
  HTML("text/html; charset=UTF-8");

  ...
 }
}


@Component
public class AmazonS3PublicUploadRequestProvider extends AbstractAmazonS3UploadRequestProvider {

 private static final CannedAccessControlList CANNED_ACL_HEADER = CannedAccessControlList.PublicRead;

 public AmazonS3PublicUploadRequestProvider(final AmazonS3PublicProperties properties) {
  super(properties);
 }

 @Override
 public PutObjectRequest createRequest(final String sourceName, final File file) {
  final String objectKey = generateObjectKey(sourceName);
  return createRequest(objectKey, file, CANNED_ACL_HEADER);
 }
}

S3 업로드를 위한 Request 생성 책임을 Provider로 모았습니다.

  • 생성자를 통해 설정을 주입하며, 중복 코드를 줄였습니다.
  • 사용자는 오픈된 2개의 createRequest() 메서드를 통해 PutObjectRequest를 생성합니다.
  • 나노초 값을 시드로 한 해시를 통해 중복되지 않는 object path를 구현했습니다.
  • 공개 여부는(CANNED_ACL_HEADER) 구현 클래스에서 정의합니다.

 

3. Delegate 클래스

기존의 코드는 특정 목적을 위한 메서드를 새로 만들었습니다.
public abstract class AbstractAmazonS3Service {

 private final AmazonS3 amazonS3;
 private final String bucketName;

 protected AbstractAmazonS3Service(final AmazonS3Properties properties, final AmazonS3 amazonS3) {
  this.amazonS3 = amazonS3;
  this.bucketName = properties.getBucketName();
 }

 public PutObjectResult putObject(final PutObjectRequest request) {
  try {
   return amazonS3.putObject(request);
  } catch (final SdkClientException e) {
   log.error(e.getClass().getName(), e);
   throw new IllegalStateException();
  }
 }

 public String getObjectFullPath(final String objectKey) {
  return amazonS3.getUrl(bucketName, objectKey).getPath();
 }

 public void getObject(final String objectKey, final File targetFile) {
  amazonS3.getObject(new GetObjectRequest(bucketName, objectKey), targetFile);
 }
}


public class AmazonS3PublicService extends AbstractAmazonS3Service {

 public AmazonS3PublicService(final AmazonS3PublicProperties properties, final AmazonS3 amazonS3) {
  super(properties, amazonS3);
 }
}

S3를 조작을 위임하는 클래스를 구현했습니다.

  • 생성자를 통해 설정을 주입하며, 중복 코드를 줄였습니다.
  • S3만 조작하도록 수정하고, 특화 로직처럼 보이던 네이밍을 수정했습니다.
    • 오브젝트 추가, 오브젝트 주소 가져오기, 오브젝트 가져오기

 

STEP 3. 버킷 추가 

@Component
public class AmazonS3PrivateProperties implements AmazonS3Properties {

 @Value("${env.profile}")
 private String stagePath;

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


@Component
public class AmazonS3PrivateUploadRequestProvider extends AbstractAmazonS3UploadRequestProvider {

 private static final CannedAccessControlList CANNED_ACL_HEADER = CannedAccessControlList.Private;

 public AmazonS3PrivateUploadRequestProvider(final AmazonS3PrivateProperties properties) {
  super(properties);
 }

 @Override
 public PutObjectRequest createRequest(final String sourceName, final File file) {
  final String objectKey = generateObjectKey(sourceName);
  return createRequest(objectKey, file, CANNED_ACL_HEADER);
 }
}


@Service
public class AmazonS3PrivateService extends AbstractAmazonS3Service {

 public AmazonS3PrivateService(final AmazonS3PrivateProperties properties, final AmazonS3 amazonS3) {
  super(properties, amazonS3);
 }
}
  • 새로운 버킷이 추가되어도 Properties, Provider, Service만 추가하면 되어 영향도를 최소화할 수 있습니다.

 

STEP 4. 사용 코드

@Service
public class SomethingWithExcelService {

 private final AbstractAmazonS3UploadRequestProvider amazonS3DelegateProvider;
 private final AbstractAmazonS3Service amazonS3Delegator;

 public SomethingWithExcelService(final AmazonS3CommonPrivateUploadRequestProvider amazonS3DelegateProvider, final AmazonS3PrivateService amazonS3Delegator) {
  this.amazonS3DelegateProvider = amazonS3DelegateProvider;
  this.amazonS3Delegator = amazonS3Delegator;
 }

 public void doSomething(final File file) {
  // 전처리 ...

  final PutObjectRequest request = amazonS3DelegateProvider.createRequest(file);
  amazonS3Delegator.putObject(request);

  // 후처리 ...
 }
}
  • 파일 업로드에 필요한 버킷을 생성자 주입하기에 유연한 변경이 가능합니다.

 

조금 더 생각해 볼 것

  • 처음에는 파일 조작 방법으로 S3 이외의 것이 들어올 수 있다고 생각해 Facade 패턴도 고려했습니다.
    • Facade 패턴을 사용하면 사용되는 service를 package-private으로 숨길 수 있습니다.
    • 하지만 S3 이외의 파일 조작을 하는 것은 너무 먼 미래일 것 같아 지금의 개선을 유지했습니다.
  • Provider를 Service의 inner class로 이동하면 좀 더 간결한 코드가 될 수 있습니다.
    • 하지만 코드 리뷰 결과 클래스가 커지는 것보다는 분리하는 것이 좋겠다는 의견을 수렴했습니다.

 

 

220611 아래의 내용은 피드백을 받아 수정한 내용으로 회사코드까지는 적용되지 않았습니다.

 

STEP 5. 다중 상속을 활용한 Properties 구조화

  • STEP 3의 properties는 하나의 타입과 2개의 구현체로 설계했었습니다.
    • 슈퍼 타입과 서브 타입을 통해 개선했습니다.
  • STEP 3는 AmazonS3Properties가 3개의 메서드를 가지고 있었습니다.
    • getProvendorContentUrl() 메서드의 사용을 public 서브 타입에서만 접근할 수 있도록 개선했습니다.

 

조금 더 생각해볼 것 2

  • AmazonS3Client는 singleton bean으로 생성되지만, ThreadSafe하기에 지금의 코드를 유지해도 괜찮습니다.

 

 

AmazonS3Client (AWS SDK for Java - 1.12.237)

This action filters the contents of an Amazon S3 object based on a simple structured query language (SQL) statement. In the request, along with the SQL expression, you must also specify a data serialization format (JSON, CSV, or Apache Parquet) of the obje

docs.aws.amazon.com