본문 바로가기
Spring

Spring - 전략패턴을 활용한 Excel Export

by 오늘부터개발시작 2023. 3. 31.

문제점

클라우드 어드민 포탈을 담당하다 보니, 종종 VM 리스트를 Excel 파일로 요청해오는 경우가 있다. VM에 여러가지 정보가 엮여 있기 때문에 요청할 때 마다 요구하는 정보가 다르다. 예를 들면, 오늘은 VM의 Flavor 정보와 Volume 정보를, 어제는 VM의 Image와 Security Group 정보 요청이 왔다. 요청이 올 때 마다 요구사항이 달라서 코드를 많이 수정해야하는 불편함이 있었다. 이를 전략 패턴을 활용해서 코드를 수정하지 않고 필요한 리소스 타입만 변수로 넘겨주면 되도록 개선해보았다.

 

해결 방법

방법은 각 리소스 별로 Excel을 입력하는 기능을 가진 클래스를 FlavorColumnComposer, VolumeColumnComposer, 등으로 만들고, 필요에 따라 리소스 별 클래스를 호출하여 Excel을 생성하려고 한다. 이를 위해서 먼저 각 리소스마다 공통으로 처리하는 로직을 정리해봤다.

1. Excel Header 정보 입력

2. Excel Row 정보 입력

3. VM과 Aggregation 할 정보를 가질 Map 가공

4. 리소스 타입을 보고 support 여부를 판별할 함수

 

위의 공통 로직을 바탕으로 인터페이스를 만들어보면 다음과 같다.

public interface ExcelColumnComposer {
    /* 리소스 타입*/
    enum ResourceType {
        Identity, Flavor, IP, VM, Volume
    }
    /* Excel의 헤더 추가 */
    void addHeader(List<Object> listHead);

    /* Excel Row의 데이터 추가 */
    void addRow(Map<String, Object> server, List<Object> columns, Object resourceMap);
		
    /* Aggregation할 데이터 가공 */
    void makeAggregationResourceMap(Map<ExcelColumnType, Object> resourceMaps, String token, String regionId);

    /* 리소스 타입으로 support 여부 판별 */
    boolean support(ResourceType type);
}

 

이제 ExcelColumnComposer 리소스 타입 별로 각각 인터페이스를 구현해준다. 예를 들면 FlavorColumnComposer, ImageColumnComposer, VolumeColumnComposer 등으로 구현할 수 있다. 만약 새로운 리소스 타입이 추가되어도 구현체만 추가로 작성해서 확장해주면 된다. FlavorColumnComposer는 다음과 같이 작성할 수 있다.

 

@Component
public class FlavorColumnComposer implements ExcelColumnComposer {

    private final ApiClient apiClient;


    public FlavorColumnComposer(ApiClient apiClient) {
        this.apiClient = compute;
    }

    @Override
    public void addHeader(List<Object> listHead) {
        listHead.add("Flavor Name");
        listHead.add("Flavor vCPUs");
        listHead.add("Flavor Ram");
        listHead.add("Flavor Disk");
    }

    @Override
    public void addRow(Map<String, Object> server, List<Object> columns, Object resourceMap) {
        Map<String, String> flavorMap = ((Map<String, Map<String, String>>)resourceMap).get(server.getFlavorId());

        columns.add(flavorMap.get("name"));
        columns.add(flavorMap.get("vcpus"));
        columns.add(flavorMap.get("ram"));
        columns.add(flavorMap.get("disk"));
    }

    @Override
    public void makeAggregationResourceMap(Map<ResourceType, Object> resourceMaps, String token, String regionId) {
        List<Map<String, Object>> flavors = apiClient.getFlavors(token);
        Map<String, Map<String, String>> flavorMaps = new HashMap<>();
        for (Map<String, Object> flavor : flavors) {
            Map<String, String> flavorMap = new HashMap<>();
            flavorMap.put("name", (String) flavor.get("name"));
            flavorMap.put("vcpus", String.valueOf(flavor.get("vcpus")));
            flavorMap.put("ram", (Integer) flavor.get("ram") / 1024 + "GB");
            flavorMap.put("disk", String.valueOf(flavor.get("disk")));
            flavorMaps.put((String) flavor.get("id"), flavorMap);
        }
        resourceMaps.put(ResourceType.Flavor, flavorMaps);
    }

    @Override
    public boolean support(ResourceType type) {
        return type.equals(ResourceType.Flavor);
    }
}

 

위의 코드의 역할은 다음과 같다.
1. addHeader(), addColumn()은 Excel에 컬럼과 데이터를 넣는 메소드이다.
2. support()는 전략패턴에서 일반적으로 사용되는 메소드로, 선택된 전략이 컴포넌트가 담당하는 전략인지 아닌지 판별하는 메소드이다. 여기서는 Flavor 타입의 전략인지 확인하고 있다.

3. 마지막으로 makeAggregationResourceMap은 성능을 위한 캐싱 작업이다. VM에서 사용하고 있는 이미지, 볼륨 등의 리소스들의 상세 정보를 가져오기 위해서는 외부 API를 호출해야한다. 캐싱을 해놓지 않으면 루프가 돌 때 마다 상세정보 조회를 위해 외부 API를 호출해야한다. 만약 Excel Export 해야 할 VM이 1000개가 넘으면 N * 1000 번의 외부 API를 호출해야한다. 때문에 최초에 외부 API를 1번 호출해서 VM이 가지고 있는 리소스의 id 값과 리소스 상세 정보를 매칭할 수 있는 Map을 생성하는 작업을 makeAggregationResourceMap 메소드에서 실행하고 있다. 이 메소드는 리소스별로 최초 1번만 실행된다.

 

실행 코드

...생략

// Excel에서 보고 싶은 리소스를 원하는데로 순서에 맞게 설정
List<ResourceType> resourceTypes = Arrays.asList(
    ResourceType.VM,
    ResourceType.Flavor,
    ResourceType.Image
);

List<Map<String, Object> servers = apiClient.getServers();

for (int i = 0; i < servers.size(); i++) {
    Map<String, Object> server = servers.get(i);
    List<Object> excelRowList = new ArrayList<>();

    // 리소스 별로 Excel 정보 입력
    for (ResourceType type : resourceTypes) {
        for (ExcelColumnComposer composer : excelColumnComposers) {
            // 담당한 전략이 맞는지 확인
            if (composer.support(type)) {

                // index가 0일때 Excel 헤더 설정 및 aggregation할 리소스(Project, Domain, Flavor, 등)들 캐싱
                if (i == 0) {
                    // 헤더 추가
                    composer.addHeader(excelHeaderList);
                    // 리소스별 Aggragtion 정보 가공
                    composer.makeAggregationResourceMap(resourceMaps, authTokens.getToken(), regionId);
                }

                composer.addRow(server, excelRowList, resourceMaps.get(type));
                break;
            }
        }
    }
    excelList.add(excelRowList);
}

... 생략

 

결론

디자인 패턴을 활용해서 가독성 및 확장성을 확보하고 업무 처리 효율도 높일 수 있게되었다.