Side Project/SWYP

[SWYP] Ehcahe를 활용하여 기상청 API 호출 성능 개선하기

newtownboy 2024. 5. 16. 17:53


[Version]
⦁ 2024.05.16 / [SWYP] Ehcahe를 활용하여 기상청 API 호출 성능 개선하기

 

현재 개발 중인 프로젝트는 날씨와 관련된 커뮤니티를 제공하는 서비스다. 때문에, 현재 날씨에 대한 데이터를 호출하기 위해 기상청 API를 사용하였다. 나는 기상청에서 제공하는 다양한 API 중 `기상청 단기예보 조회서비스` API를 신청하였다. 

 

기상청 단기예보 조회서비스: https://www.data.go.kr/data/15084084/openapi.do

 

기상청_단기예보 ((구)_동네예보) 조회서비스

초단기실황, 초단기예보, 단기((구)동네)예보, 예보버전 정보를 조회하는 서비스입니다. 초단기실황정보는 예보 구역에 대한 대표 AWS 관측값을, 초단기예보는 예보시점부터 6시간까지의 예보를,

www.data.go.kr

 

신청 후 별도의 승인 절차를 기다릴 필요 없이 자동 승인이기 때문에 바로 사용할 수 있다. 이제 API 호출을 위해 Request Parameter 형태를 확인해 보자.

홈페이지에서 제공하는 요청변수를 보면 예보지점 X 좌표, 예보지점 Y 좌표 값을 필요로 하는 것을 확인할 수 있다. 보통의 좌표 값은 샘플 데이터와 같이 값이 딱 떨어지지 않고 소수점 이하로 표기된다. 때문에 기상청 API에서 해당 X, Y 좌표 값을 어떤 식으로 도출하는지 확인해 보았다.

 

활용신청 이후 API 상세를 들어가 확인해 보면, 서비스정보에 참고문서가 존재한다. 해당 파일을 다운로드해 보면 위경도에 대한 파일이 존재한다. 해당 파일을 열어보자.

 

파일을 열어보면 여러 개의 필드가 존재한다. 여기서 우리에게 필요한 데이터인 X 좌표와 Y 좌표에 대한 값을 얻기 위해서는 1단계, 2단계, 3단계에 해당하는 주소 값들이 정확하게 일치해야 한다.

 

처음에는 해당 값을 DB에 저장하여 관리하는 방법도 고민했으나 날씨 API가 호출될 때마다 X 좌표와 Y 좌표를 얻기 위해 데이터베이스에 접근하는 것이 비효율적이라 생각되었고, 주소에 대응하는 좌표 데이터는 변하지 않는 데이터이므로 캐시를 통해 접근하는 방법으로 결정했다.

 

캐시 서버는 Redis, Ehcache, Spring Cache 등 다양한 종류가 있었지만, 초기에는 사용자가 없을 것으로 생각되어 서버 구조를 단일 서버로 구성했기 때문에 Ehcache를 사용하였다. 추후 사용자가 늘어날 경우 Redis와 같이 별도의 캐시 서버를 구축할 예정이다.

 

캐시는 Key-Value 형태를 갖는다. 따라서 위 엑셀 파일을 가공할 필요가 있었다. 나는 1단계, 2단계, 3단계를 공백을 구분자로 사용하여 하나의 컬럼으로 묶고 그에 대응되는 X 좌표와 Y 좌표 값을 얻을 수 있도록 다음과 같이 기존의 엑셀 파일을 변경했다.

 

엑셀 파일 업로드 및 캐시 등록

엑셀 파일을 가공한 후 애플리케이션이 구동될 때 엑셀 파일에 있는 내용이 로컬 캐시에 등록이 되어야 한다. 엑셀 파일을 분석하기 위한 라이브러리인 poi와 로컬 캐시에 등록하기 위한 Ehcache 라이브러리를 build.gradle에 추가하였다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'net.sf.ehcache:ehcache:2.10.3'
    implementation 'org.apache.poi:poi-ooxml:4.1.2'
}

 

또한 Ehcache를 사용하기 위해 ehcache.xml 파일을 생성하였다. 위치에 대응하는 좌표 값은 애플리케이션 구동 후 계속해서 사용할 예정이므로 eternal 속성을 true로 설정해 주었다.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="false">
         
    <cache name="addressCache"
           maxEntriesLocalHeap="10000"
           maxEntriesLocalDisk="1000"
           diskSpoolBufferSizeMB="20"
           eternal="true"
           memoryStoreEvictionPolicy="LFU">
    </cache>
</ehcache>

 

마지막으로 가장 중요한 엑셀 파일을 분석하여 캐시에 어떻게 등록하는지 확인해 보자. 내가 작성한 코드는 다음과 같다.

@Component
public class ExcelCacheLoader {
    private final Cache cache;

    public ExcelCacheLoader() {
        CacheManager cacheManager = CacheManager.getInstance();
        this.cache = cacheManager.getCache("addressCache");
    }

    public void extractExcelData() throws IOException {
        try (InputStream inputStream = getClass().getResourceAsStream("/address.xlsx")) {
            ZipSecureFile.setMinInflateRatio(0);
            Workbook workbook = new XSSFWorkbook(inputStream);
            Sheet sheet = workbook.getSheetAt(0);

            // 첫 번째 행은 헤더이므로 스킵
            Iterator<Row> rowIterator = sheet.iterator();
            if (rowIterator.hasNext()) {
                rowIterator.next();
            }

            while (rowIterator.hasNext()) {
                Row row = rowIterator.next();
                Cell keyCell = row.getCell(0);
                Cell nxCell = row.getCell(1);
                Cell nyCell = row.getCell(2);

                if (keyCell != null && nxCell != null && nyCell != null) {
                    String key = keyCell.getStringCellValue();
                    String nx = nxCell.getStringCellValue();
                    String ny = nyCell.getStringCellValue();

                    AddressPosition addressPosition = AddressPosition.builder()
                            .nx(nx)
                            .ny(ny)
                            .build();

                    Element element = new Element(key, addressPosition);
                    cache.put(element);
                }
            }
        }
    }

    public AddressPosition getPositionFromAddressCache(String key) {
        Element element = cache.get(key);
        if (element != null) {
            return (AddressPosition) element.getObjectValue();
        }
        return null;
    }
}

 

@Getter
public class AddressPosition {
    String nx;
    String ny;

    @Builder
    public AddressPosition(String nx, String ny) {
        this.nx = nx;
        this.ny = ny;
    }
}

 

여기서 가장 중요한 extractExcelData()에 대해 알아보자. extractExcelData() 메서드는 엑셀 파일에서 데이터를 추출하여 캐시에 저장하는 역할을 수행한다.

 

먼저, /resources/address.xlsx 파일을 읽어 Workbook 객체로 로드한다. 엑셀 파일에서 첫 번째 행은 헤더이기 때문에 스킵하고, 그다음 행부터 반복하며 각 셀의 값을 읽어와 AddressPosition 객체를 생성하고 이를 Element로 래핑 하여 캐시에 추가한다. AddressPosition은 x 좌표와 y 좌표를 갖는 객체이다.

 

그다음으로 캐시에 저장된 주소에 대응되는 좌표 값을 얻기 위한 메서드인 getPositionFromAddressCache()에 대해 알아보자. 코드는 정말 간단하다. key에 해당하는 value 값을 리턴하는 구조이기 때문에 파라미터로 key 값을 받아 그에 해당하는 AddressPosition 객체를 반환한다.

 

아쉬운 점

기상청 API가 파일에 작성된 X, Y 좌표에 대해서만 데이터를 응답할 수 있기 때문에, 실제로 자신의 위치에 해당하는 주소를 엑셀 파일에 기술된 주소로 변경해야 하는 번거로움이 존재했다. 이 부분은 기상청 API에서 X, Y좌표를 받았을 때 이를 가장 근접한 기지국의 좌표로 변환해 주는 과정을 추가하면 이 API를 사용하는 입장에서 조금 더 사용성이 좋아질 것 같다.