[Toy Project] DB 백업 및 복구 기능 생성

최지나·2024년 2월 23일
3

개요

  • 이전 포스팅에서 시스템의 실 사용자와 미팅 후, DB 백업/복구 기능의 필요성을 느꼈다!
  • 그래서 crontab을 사용한 스케줄러로 DB를 주기적으로 백업하고, API를 사용해서 원할때 복구하는 기능을 생성하였다
  • 또한 백업 파일이 지나치게 많이 쌓이면 용량을 너무 많이 차지할 수 있기 때문에 일정 기간이 지나면 백업 파일을 삭제하는 스케줄러도 추가로 생성하였다

고민

  • 고민 포인트는 바로 환경이었다! 백업, 복구를 위해서는 dump 뜬 sql 파일을 저장할 경로가 필요했는데, 환경에 따라 경로가 달라야 했기 때문이다. 실제로 서비스가 배포된 환경은 우분투였으나, 로컬 개발 환경은 윈도우였고, 윈도우에 배포하고자 하는 상황이 추후 발생할 수도 있었기에, 양 운영체제에서 모두 동작하는 백업/복구 기능을 개발해야 했다

  • 그래서 현재 os에 따라서 다른 경로를 가져오는 method를 DatabaseUtil에 생성하였다

현재 운영체제 조회
System.getProperty("os.name").toLowerCase().contains("win");

  • DatabaseUtil.java
    public String getFullBackupPath(String winBackupPath, String linuxBackupPath) {
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
        if (isWindows) {
            Path currentDir = Paths.get(System.getProperty("user.dir"));
            if (currentDir.endsWith("backend"))
                currentDir = currentDir.getParent();
            return currentDir.toString() + winBackupPath;
        } else {
            return linuxBackupPath;
        }
    }
  • 그리고 또한 윈도우 (로컬) 환경에서는 docker를 사용해서 DB를 띄웠으나, 운영 서버에서는 직접 postgres를 설치하였기에, DB 백업 시 사용해야 할 명령어가 달랐다 이를 구분하여 기능을 구현하는데에 초점을 맞추었다
String[] winCmd = {
                "cmd.exe", "/c",
                "docker exec {containerName} pg_dump -U " + userName + " -d " + dbName + " -f " + "/tmp/"
                        + backupFileName +
                        " && docker cp containerName}:" + "/tmp/" + backupFileName + " " + fullPath + backupFileName
        };

String[] linuxCmd = {
                "/bin/bash", "-c",
                "sudo -u postgres pg_dump -U " + userName + " -d " + dbName + " -f " + fullPath + backupFileName
        };
  • 복구 시에는 3달치의 sql 파일들 중 원하는 날짜의 버전으로 복구할 수 있도록, sql 파일 이름을 인자로 받아, 해당 파일로 덮어씌우도록 구현하였다
  • 처음에는 가장 최근 파일을 알아서 가져오는 것도 고민을 하였으나, 만약 백업 스케줄러가 작동한 일요일 오전 1시 직후, 일요일 오전 12시 반의 DB 상태로 돌아가고 싶다면, 이를 되돌리기가 어려웠기에, 직접 파일 이름을 입력하는 것이 조금은 귀찮더라도, 원하는 버전을 지정할 수 있는 것이 오히려 관리가 편할 것이라고 생각했다

코드

  • DatabaseBackupScheduler.java
private final ExecutorService executor = Executors.newCachedThreadPool();  // 새로운 스레드풀 생성

 @Scheduled(cron = "0 1 * * * 0") // Every Sunday at 1AM
    public void backupDatabase() {
        String backupFileName = "db_backup_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".sql";
        String dbName = databaseUtil.getDbName(dbUrl);
        String fullPath = databaseUtil.getFullBackupPath(winBackupPath, linuxBackupPath);
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");

        String[] winCmd = {
                "cmd.exe", "/c",
                "docker exec madeg_postgres pg_dump -U " + userName + " -d " + dbName + " -f " + "/tmp/"
                        + backupFileName +
                        " && docker cp madeg_postgres:" + "/tmp/" + backupFileName + " " + fullPath + backupFileName
        };

        String[] linuxCmd = {
                "/bin/bash", "-c",
                "sudo -u postgres pg_dump -U " + userName + " -d " + dbName + " -f " + fullPath + backupFileName
        };

        String[] cmd = isWindows ? winCmd : linuxCmd;
        ProcessBuilder pb = new ProcessBuilder(cmd);
        try {
            Process process = pb.start();
            boolean finished = process.waitFor(60, TimeUnit.SECONDS);
            if (finished && process.exitValue() == 0) {
                log.info("Database backup created successfully at " + fullPath + backupFileName);
            } else {
                log.error("Error occurred during database backup or timeout reached.");
            }
        } catch (InterruptedException | IOException e) {
            log.error("Exception occurred during backup process", e);
        } finally {
            executor.shutdownNow();
        }
    }



    @Scheduled(cron = "0 2 * * * 0") // Every Sunday at 2AM
    public void deleteOldBackups() {
        String fullPath = databaseUtil.getFullBackupPath(winBackupPath, linuxBackupPath);
        LocalDate threeMonthAgo = LocalDate.now().minusMonths(3);

        try (Stream<Path> files = Files.walk(Paths.get(fullPath))) {
            files.filter(Files::isRegularFile)
                    .filter(path -> path.toString().endsWith(".sql"))
                    .forEach(path -> {
                        String fileName = path.getFileName().toString();
                        String datePart = fileName.replace("db_backup_", "").replace(".sql", "");
                        try {
                            LocalDate fileDate = LocalDate.parse(datePart, DateTimeFormatter.ofPattern("yyyyMMdd"));
                            if (fileDate.isBefore(threeMonthAgo)) {
                                File file = path.toFile();
                                if (file.delete()) {
                                    log.info("Deleted old backup file: " + file.getPath());
                                } else {
                                    log.error("Failed to delete file: " + file.getPath());
                                }
                            }
                        } catch (Exception e) {
                            log.error("Error deleting file: " + path, e);
                        }
                    });
        } catch (IOException e) {
            log.error("Error accessing backup directory: " + fullPath, e);
        }
    }
  • RestoreService.java
    • API input : backupFileName
 public void restoreDatabase(String backupFileName) {
        String dbName = databaseUtil.getDbName(dbUrl);
        String fullPath = databaseUtil.getFullBackupPath(winBackupPath, linuxBackupPath);
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");

        String[] winCmd = new String[] { "cmd.exe", "/c", "docker cp " + fullPath + backupFileName
                + " madeg_postgres:/tmp/" + backupFileName + " && " + "docker exec -i madeg_postgres psql -U "
                + userName + " -d " + dbName + " -f /tmp/"
                + backupFileName };

        String[] linuxCmd = new String[] { "/bin/bash", "-c",
                "sudo -u postgres psql -U " + userName + " -d " + dbName + " -f " + fullPath
                        + backupFileName };

        String[] cmd = isWindows ? winCmd : linuxCmd;
        ProcessBuilder pb = new ProcessBuilder(cmd);
        try {
            Process process = pb.start();
            boolean finish = process.waitFor(60, TimeUnit.SECONDS);

            if (finish && process.exitValue() == 0) {
                log.info("Database restored successfully from " + fullPath);
            } else {
                log.error("Error occurred during database restoration or timeout reached.");
            }
        } catch (InterruptedException | IOException e) {
            log.error("Exception occurred during restore process", e);
        }
    }

후기

  • DB 백업 복구 기능을 만들고 나니 사용자가 사용중이라는 것에 대한 부담이 조금은 줄어들었던 것 같다!
  • 물론 1차 버전이 완벽히 마무리되기도 전에 사용자가 생길 줄은 몰랐으나,,, 앞으로는 설계 단계에서 백업/복구 기능도 고려해야겠다고 다짐했다
  • 이슈에서 언급한 것처럼 서버 자체가 다운될 상황에 대비해서 sql 파일을 이메일 등의 방식으로 전송해서 더 안전하게 데이터를 관리하는 방법도 고려해 봐야겠다
  • 이슈를 하나 해결하면 또 하나가 생기는 것 같다 😆😆

profile
의견 나누는 것을 좋아합니다 ლ(・ヮ・ლ)

1개의 댓글

comment-user-thumbnail
2024년 7월 29일

백업 관련 공부하다가 블로그를 알게 되었습니다. 잘 보고 가요 :-)

답글 달기