본문 바로가기
PROJECT

[JAVA] 레이더 이미지 자동 저장 모듈

by amoomar 2024. 12. 20.
반응형

 

해당 게시글은 특정 시스템에 모듈 형태로 추가할 어플리케이션을 개발하다, 처리 내용과 결과를 정리해두면 좋을 것 같은 마음에 작성하였다.

 

우선 본문을 들어가기에 앞서, 이 모듈의 기능에 대해 정리한 readMe와 실행 결과를 아래 첨부하였다.

프로그램 수행 결과


 

목차는 다음과 같다.

1. 프로젝트 구조
2. 이미지 처리
  1) 이미지 다운로드
  2) 오래된 이미지 삭제
3. 로그 처리
  1) 로그 핸들러 생성
  2) 로그 핸들러 삭제
4. 스케줄러 처리
5. 추후 개선 예정

 

1. 프로젝트 구조

프로젝트 구조는 알아보기 용이하도록 이미지에 메모한 형태로 정리하였다.

 



2. 이미지 처리

이미지의 처리는 api 호출 결과 이미지를 특정 경로에 저장하는 다운로드 로직과, 오래된 이미지를 삭제 처리하는 로직으로 구분하였다.


1) 이미지 다운로드

downloadImage라는 메서드를 통해, 인자로 전달된 url을 호출하여 이미지 파일을 반환받고 해당 이미지 파일을 지정한 파일명과 경로에 저장하는 기능을 수행한다.

 

상세 내용은 주석을 통해 확인 가능하다.

package app.image;

import app.Application;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.logging.Logger;

public class ImageDownloader {
    private static final Logger logger = Application.logger; //로그 핸들러 객체

    /**
     * @apiNote api 반환 이미지 다운로더
     * @param imageUrl http://global.amo.go.kr/radar/cgi-bin/nph-rdr_cmp_img?생략
     * @param directoryPath D:\WEBDATA\RADAR\202412\06\
     * @param fileName HSR_202412060000.jpg
     */
    public static void downloadImage(String imageUrl, String directoryPath, String fileName) {
        // 파일 경로 생성
        String filePath = directoryPath + File.separator + fileName;
        File destinationFile = new File(filePath);

        // 이미 파일이 존재하는지 확인
        if (destinationFile.exists()) {
            logger.fine("이미 저장한 파일: " + fileName);
            return;
        }

        try {
            // 디렉토리 확인 및 생성
            File directory = new File(directoryPath);
            if (!directory.exists() && directory.mkdirs()) {
                logger.fine("디렉토리 생성: " + directory.getAbsolutePath());
            }

            // URL 객체 생성
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);

            // HTTP 응답 코드 확인
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                // 입력 스트림 생성
                try (InputStream inputStream = connection.getInputStream();
                     FileOutputStream outputStream = new FileOutputStream(destinationFile)) {

                    // 버퍼를 사용해 파일 쓰기
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                }
                logger.info("이미지 저장 성공: " + filePath);
            } else {
                logger.severe("HTTP 요청 실패. 응답 코드: " + responseCode);
            }

            connection.disconnect();
        } catch (Exception e) {
            logger.severe("이미지 다운로드 중 오류 발생: " + e.getMessage());
        }
    }
}

 



2) 오래된 이미지 삭제

1. 파일명에 저장된 레이더 관측일자 정보를 분석하여, 변수로써 선언된 지정 일자보다 과거의 파일인 경우 TRUE, 그렇지 않은 경우 FALSE를 반환하는 함수2. 각 조건에 따라 삭제 로직을 수행하는 함수를 연계하여 최종적으로 사용자가 원하는 시간 범위 이전의 파일을 제거하는 기능을 수행하도록 하는 클래스이다.

 

상세 내용은 주석을 통해 확인 가능하다.

package app.image;

import app.Application;

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Logger;

public class ImageDelete {
    private static final Logger logger = Application.logger;
    private static final String FILE_NAME_DATE_FORMAT = "yyyyMMddHHmm"; // 파일 이름에 포함된 날짜 형식
    private static final long SEVEN_DAYS_IN_MILLISECONDS = 7L * 24 * 60 * 60 * 1000; // 7일(밀리초)

    /**
     * @apiNote 디렉토리 이미지 제거
     * @param filePath D:\WEBDATA\RADAR\202412\06\
     */
    public static void deleteOldImages(String filePath) {
        // 디렉토리 객체 생성
        File directory = new File(filePath);

        if (!directory.exists() || !directory.isDirectory()) {
            logger.severe("유효하지 않은 디렉토리: " + filePath);
            return;
        }

        // 디렉토리 내 파일 리스트 가져오기
        File[] files = directory.listFiles();
        if (files == null || files.length == 0) {
            logger.fine("디렉토리에 파일 없음: " + filePath);
            return;
        }

        // 파일들 확인 및 삭제
        for (File file : files) {
            if (file.isFile() && isOldFile(file)) {
                try {
                    if (file.delete()) {
                        logger.fine("파일 삭제 성공: " + file.getName());
                    } else {
                        logger.fine("파일 삭제 실패: " + file.getName());
                    }
                } catch (Exception e) {
                    logger.severe("파일 삭제 중 오류 발생: " + file.getName());
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * @apiNote 파일명의 시간 정보가 SEVEN_DAYS_IN_MILLISECONDS 이전인지 검토
     * @param file
     * @return 삭제대상 여부
     */
    private static boolean isOldFile(File file) {
        String fileName = file.getName();

        try {
            // 파일 이름에서 시간 정보 추출 (예: cmp_yyyyMMddHHmm.jpg)
            int underscoreIndex = fileName.indexOf('_');
            int dotIndex = fileName.lastIndexOf('.');
            if (underscoreIndex == -1 || dotIndex == -1 || dotIndex <= underscoreIndex) {
                logger.severe("파일 이름 형식이 올바르지 않습니다: " + fileName);
                return false; // 예상된 형식이 아님
            }

            // 시간 정보 추출 및 파싱
            String datePart = fileName.substring(underscoreIndex + 1, dotIndex);
            SimpleDateFormat dateFormat = new SimpleDateFormat(FILE_NAME_DATE_FORMAT);
            Date fileDate = dateFormat.parse(datePart);

            // 현재 시간과 파일 생성 시간 비교
            long currentTime = System.currentTimeMillis();
            long fileTime = fileDate.getTime();
            return (currentTime - fileTime) > SEVEN_DAYS_IN_MILLISECONDS;

        } catch (ParseException e) {
            logger.severe("파일 이름에서 시간 정보 파싱 실패: " + fileName);
            return false;
        }
    }
}

 



3. 로그 처리

로그 처리 또한 마찬가지로 핸들러를 설정 및 생성하는 로직과, 오래된 로그 파일을 제거하는 로직으로 구분하였다.


1) 로그 핸들러 생성

프로젝트 경로 내, logs라는 폴더를 생성하여 각 ' 연월/일 ' 별 debug범위의 로그와 error범위의 로그가 별도의 파일로 저장될 수 있도록 설정한 정보를 적용한 로그 핸들러의 생성자이다. 이때 파일이 2개의 종류로 분류되기 때문에 파일 핸들러 생성 로직은 함수화 하여 중복 코드가 작성되지 않도록 하였으며, 생성자 중복 선언의 우려가 있으므로 초기 선언 시 기존 로그 핸들러를 제거하는 함수가 동작될 수 있도록 하였다.

 

상세 로직은 주석을 통해 확인 가능하다.

package app.log;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.*;

public class LoggerConfig {

    /**
     * @apiNote 로그 설정 적용 및 생성
     * @param loggerName logger명
     * @return Logger
     */
    public static Logger createLogger(String loggerName) {
        Logger logger = Logger.getLogger(loggerName);
        removeExistingHandlers(logger);

        try {
            // 현재 날짜 가져오기
            Date now = new Date();
            String yearMonth = new SimpleDateFormat("yyyyMM").format(now); // yyyyMM
            String day = new SimpleDateFormat("dd").format(now);          // dd
            String logBasePath = "logs" + File.separator + yearMonth + File.separator + day + File.separator;

            // 로그 디렉토리 확인 및 생성
            File logDirectory = new File(logBasePath);
            if (!logDirectory.exists() && logDirectory.mkdirs()) {
                System.out.println("로그 디렉토리 생성: " + logDirectory.getAbsolutePath());
            }

            // 로그 설정 초기화
            LogManager.getLogManager().reset();
            logger.setLevel(Level.ALL);

            // 콘솔 핸들러 추가
            ConsoleHandler consoleHandler = new ConsoleHandler();
            consoleHandler.setLevel(Level.ALL);
            consoleHandler.setFormatter(new SimpleFormatter());
            logger.addHandler(consoleHandler);

            // 파일 핸들러 추가
            FileHandler debugFileHandler = createFileHandler(logBasePath + "debug.log", Level.FINE);
            logger.addHandler(debugFileHandler);

            FileHandler errorFileHandler = createFileHandler(logBasePath + "error.log", Level.SEVERE);
            logger.addHandler(errorFileHandler);

        } catch (IOException e) {
            System.err.println("로거 설정 중 오류 발생: " + e.getMessage());
        }

        return logger;
    }

    /**
     * @apiNote 로그핸들러 제거
     * @param logger
     */
    private static void removeExistingHandlers(Logger logger) {
        Handler[] handlers = logger.getHandlers();
        for (Handler handler : handlers) {
            logger.removeHandler(handler);
            handler.close(); // 핸들러 리소스 정리
        }
    }

    /**
     * @apiNote 로그파일 핸들러 생성
     * @param path
     * @param level
     * @return FileHandler
     * @throws IOException
     */
    private static FileHandler createFileHandler(String path, Level level) throws IOException {
        FileHandler fileHandler = new FileHandler(path, true);
        fileHandler.setLevel(level);
        fileHandler.setFormatter(new SimpleFormatter());
        return fileHandler;
    }
}

 



2) 로그 핸들러 삭제

로그 폴더를 삭제하는 메서드와 로그 파일을 삭제하는 메서드를 활용하여 원하는 조건에 맞추어 로그파일과 빈 로그폴더를 제거하는 기능을 구현하였다.

 

상세 내용은 주석을 통해 확인 가능하다.

package app.log;

import app.Application;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Logger;

public class LogCleaner {
    private static final Logger logger = Application.logger;

    /**
     * @apiNote 현시점으로부터 7일 이전 로그 삭제
     * @param logBasePath logs
     */
    public static void deleteOldLogs(String logBasePath) {
        try {
            // 현재 날짜에서 7일 전 계산
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.DAY_OF_YEAR, -7);
            Date sevenDaysAgo = calendar.getTime();

            // 기준 날짜 로그 디렉토리 확인
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            String thresholdDate = dateFormat.format(sevenDaysAgo);

            File logDir = new File(logBasePath);
            if (!logDir.exists() || !logDir.isDirectory()) {
                logger.warning("유효하지 않은 로그 경로: " + logBasePath);
                return;
            }

            // 로그 디렉토리 탐색 및 오래된 파일 삭제
            for (File yearMonthDir : logDir.listFiles()) {
                if (yearMonthDir.isDirectory()) {
                    boolean yearMonthDirEmpty = true; // 해당 년-월 폴더가 비었는지 확인

                    for (File dayDir : yearMonthDir.listFiles()) {
                        if (dayDir.isDirectory()) {
                            String dirName = yearMonthDir.getName() + dayDir.getName(); // yyyyMMdd 형식 생성
                            if (dirName.compareTo(thresholdDate) < 0) {
                                deleteDirectory(dayDir);
                                logger.info("오래된 로그 디렉토리 삭제: " + dayDir.getAbsolutePath());
                            } else {
                                yearMonthDirEmpty = false; // 아직 유효한 로그가 있으면 비어 있지 않음
                            }
                        }
                    }

                    // 년-월 폴더가 비었으면 삭제
                    if (yearMonthDirEmpty && yearMonthDir.listFiles().length == 0) {
                        deleteDirectory(yearMonthDir);
                        logger.info("빈 로그 년-월 디렉토리 삭제: " + yearMonthDir.getAbsolutePath());
                    }
                }
            }
        } catch (Exception e) {
            logger.severe("오래된 로그 삭제 중 오류 발생: " + e.getMessage());
        }
    }

    /**
     * @apiNote 디렉토리 삭제
     * @param directory 로그폴더 경로
     */
    private static void deleteDirectory(File directory) {
        if (directory.isDirectory()) {
            for (File file : directory.listFiles()) {
                deleteDirectory(file);
            }
        }
        if (directory.delete()) {
            logger.fine("삭제 성공: " + directory.getAbsolutePath());
        } else {
            logger.warning("삭제 실패: " + directory.getAbsolutePath());
        }
    }
}

4. 스케줄러 처리

매일 자정, 로그 파일 관리를 위해 스케줄러를 실행하고

이후 원하는 간격마다 api 파라미터를 조작하여 원하는 시간대의 파일을 저장할 수 있도록 하였다. 이때 getPreviousFiveMinuteTime()를 활용한 이유는 레이더 이미지 반환 api가 5분 간격의 이미지만을 반환하며(5분의 딜레이가 있음), 유효하지 않은 tm에 대한 이미지 요청은 가장 최신 파일을 생성하는 것으로 처리되기 때문에 실행시간에서 5분을 뺀 후 5분 단위로 내림하기 위함이다.

 

마찬가지로 더 상세한 내용은 아래 코드 본문 주석을 통해 확인 가능하다.

package app;

import app.image.ImageDownloader;
import app.log.LogCleaner;
import app.log.LoggerConfig;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static app.image.ImageDelete.deleteOldImages;
import static app.image.ImageDownloader.downloadImage;

public class Application {
    public static Logger logger=LoggerConfig.createLogger(Application.class.getName()); //로거(전역 사용)
    private static String basePath = "D:" + File.separator + "WEBDATA" + File.separator + "RADAR" + File.separator; //이미지파일 생성 기본경로
    private static String apiUrl = "http://global.amo.go.kr/radar/cgi-bin/nph-rdr_cmp_img?";

    public static void main(String[] args) {

        logger.info("스케줄러 실행 >>>>>>>>>>");

        // ScheduledExecutorService 생성
        int th=1; //스레드 수
        int interval=2; //실행 주기
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(th);

        // ScheduledExecutorService 동작
        scheduler.scheduleAtFixedRate(() -> {
            String tm = getPreviousFiveMinuteTime();
            // 자정 작업 실행
            if (tm.endsWith("0000")) { // 자정일 경우
                logger.info("자정, 로그 처리 로직 수행 >>>>>>>>>>");
                logger = LoggerConfig.createLogger(Application.class.getName());
                LogCleaner.deleteOldLogs("logs");
            }

            String cmp="HSR";
            String size="1000";
            String imageUrl = apiUrl +
                    "&cmp="+ cmp +
                    "&obs=ECHO" +
                    "&color=C4" +
                    "&ZRa=148" +
                    "&ZRb=1.59" +
                    "&title=1" +
                    "&legend=1" +
                    "&lonlat=0" +
                    "&center=0" +
                    "&topo=0" +
                    "&typ=0" +
                    "&wv=0" +
                    "&aws=01" +
                    "&wt=0" +
                    "&gov=KMA" +
                    "&x1=-10" +
                    "&y1=-10" +
                    "&x2=-10" +
                    "&y2=-10" +
                    "&fir=0" +
                    "&routes=0" +
                    "&qcd=EXT" +
                    "&tm=" + tm +
                    "&map=HR" +
                    "&runway=ICN" +
                    "&xp=477.92602012643" +
                    "&yp=477.92602012643" +
                    "&lat=37.461854" +
                    "&lon=126.440609"+
                    "&zoom=17.0667" +
                    "&size=" + size;

                    String fileName= cmp + "_" + tm + ".jpg";

            // 동적 경로 생성
            String yearMonth = tm.substring(0, 6); // YYYYMM
            String day = tm.substring(6, 8); // DD
            String filePath=basePath + yearMonth + File.separator + day + File.separator;

            try {
                downloadImage(imageUrl, filePath, fileName);
                deleteOldImages(filePath);
            } catch (Exception e) {
                logger.severe("스케줄러 실행 오류: " + e.getMessage());
            }
        }, 0, interval, TimeUnit.MINUTES);

        // 애플리케이션 종료 시 스케줄러 종료 (옵션)
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            logger.info("스케줄러 종료 >>>>>>>>>>");
            scheduler.shutdown();
        }));
    }

    //이미지 조회 가능 시간(5의 배수로 내림처리 - 5분)으로 포맷팅
    public static String getPreviousFiveMinuteTime() {
        // 현재 시간 가져오기
        Calendar now = Calendar.getInstance();

        // 분 단위를 5의 배수로 내림 처리
        int minute = now.get(Calendar.MINUTE);
        int roundedMinute = (minute / 5) * 5;
        now.set(Calendar.MINUTE, roundedMinute);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);

        // 5분을 이전으로 이동
        now.add(Calendar.MINUTE, -5);

        // 포맷팅 (yyyyMMddHHmm)
        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmm");
        return formatter.format(now.getTime());
    }
}


5. 추후 개선 예정

최종적으로 개발된 내용을 검증하다보니, 두가지의 개선이 필요한 부분을 발견하여 아래에 정리하였다.

 

1. 어떤 이미지를 처리하다가 오류가 발생하였는지 확인되지 않는 현상 개선 필요

 

 

2. 오류 발생 대상 이미지, 경로에 존재하더라도 재처리 할 수 있도록 로직 개선 필요

 


 

 

https://github.com/Hamjeonghui/radarImg

 

GitHub - Hamjeonghui/radarImg

Contribute to Hamjeonghui/radarImg development by creating an account on GitHub.

github.com

 

반응형