- 날짜 단위로 백업하기
- 백업 저장소는 AWS S3를 활용하기
- 백업 작업은 배치로 수행하기
DTO
package com.codeit.monew.article.backUp;
import com.codeit.monew.article.entity.Article;
import java.time.LocalDateTime;
public record ArticleBackupDto(
String source,
String sourceUrl,
String articleTitle,
LocalDateTime articlePublishDate,
String articleSummary,
long articleCommentCount,
long articleViewCount,
boolean deleted,
String interestId // 복구할 때 필요함
) {
public static ArticleBackupDto from(Article article) {
// Interest가 Lazy해도 ID접근을 프록시로 가능하게끔
String interestIdStr = (article.getInterest() != null && article.getInterest().getId() != null)
? article.getInterest().getId().toString()
: null;
return new ArticleBackupDto(
article.getSource(),
article.getSourceUrl(),
article.getArticleTitle(),
article.getArticlePublishDate(),
article.getArticleSummary(),
article.getArticleCommentCount(),
article.getArticleViewCount(),
article.isDeleted(),
interestIdStr
);
}
}
AWS
S3Config
package com.codeit.monew.article.backUp.aws;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
@Configuration
public class S3Config {
@Value("${aws.s3.region}")
private String region;
@Bean
public S3Client s3Client() {
return S3Client.builder().region(Region.AP_NORTHEAST_2).build();
}
}
BackupUtility
package com.codeit.monew.article.backUp.aws;
import java.time.LocalDate;
public class BackupUtility {
// S3에 백업파일을 저장할 때 파일 경로(키)를 만들어주는 유틸리티
private BackupUtility() {}
public static String keyFor(LocalDate date) {
String y = "%04d".formatted(date.getYear());
String m = "%02d".formatted(date.getMonthValue());
String d = "%02d".formatted(date.getDayOfMonth());
return "backups/articles/%s/%s/%s/articles-%s-%s-%s.jsonl.gz"
.formatted(y,m,d,y,m,d); // backups/articles/2025/09/16/articles-2025-09-16.jsonl.gz
}
// 위 파일명을 임시 업로드용 파일명으로 바꾸는 메서드
// 임시 키(.part)로 먼저 업로드하고, 업로드 성공하면 최종 키(.json.gz)로 복사 후 임시파일 삭제
public static String tempKeyFor(LocalDate date, String uuid) {
return keyFor(date).replace(".jsonl.gz", "." + uuid + ".part");
}
}
Repository
package com.codeit.monew.article.backUp.repository;
import com.codeit.monew.article.entity.Article;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public interface ArticleBackupRepository extends JpaRepository<Article, UUID> {
@Query("""
select art from Article as art
where art.articlePublishDate >= :startDate and art.articlePublishDate < :endDate
""")
Page<Article> findAllInRange(
@Param("startDate")LocalDateTime startDate,
@Param("endDate")LocalDateTime endDate,
Pageable pageable
);
@Query("""
select art.sourceUrl from Article as art
where art.articlePublishDate >= :startDate and art.articlePublishDate < :endDate
""")
List<String> findAllSourceUrlsInRange(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
}
Service
package com.codeit.monew.article.backUp.service;
import com.codeit.monew.article.backUp.dto.ArticleBackupDto;
import com.codeit.monew.article.backUp.aws.BackupUtility;
import com.codeit.monew.article.backUp.repository.ArticleBackupRepository;
import com.codeit.monew.article.entity.Article;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.zip.GZIPOutputStream;
@Service
@RequiredArgsConstructor
@Slf4j
public class ArticleBackupService {
private static final int PAGE_SIZE = 1000;
private final ArticleBackupRepository articleBackupRepository;
private final S3Client s3;
@Value("${app.backup.bucket}")
String bucket;
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 날짜에 따라 백업하는 메서드
public void articleBackup(LocalDate date) {
LocalDateTime start = date.atStartOfDay();
LocalDateTime end = date.plusDays(1).atStartOfDay();
String backupKey = BackupUtility.keyFor(date);
String tempKey = BackupUtility.tempKeyFor(date, UUID.randomUUID().toString()); // UUID String
Path tmp = null;
try {
tmp = Files.createTempFile("articleBackup",".jsonl.gz");
System.out.println("tmp 경로 : " + tmp.toAbsolutePath());
try (
OutputStream outOs = Files.newOutputStream(tmp, StandardOpenOption.WRITE);
GZIPOutputStream gzipOs = new GZIPOutputStream(outOs);
OutputStreamWriter outOsWriter = new OutputStreamWriter(gzipOs, StandardCharsets.UTF_8)
)
{
Pageable pageable = PageRequest.of(0, PAGE_SIZE, Sort.by("id").ascending()); // import 주의
Page<Article> page;
long total = 0;
do {
page = articleBackupRepository.findAllInRange(start, end, pageable);
for(Article article : page.getContent()) {
ArticleBackupDto articleDto = ArticleBackupDto.from(article);
outOsWriter.write(objectMapper.writeValueAsString(articleDto));
outOsWriter.write('\n');
total++;
}
outOsWriter.flush();
pageable = page.nextPageable();
} while(!page.isLast());
log.info("백업 작성 종료됨, date={}, count={}, file={}", date, total, tmp);
} // 안쪽 try
// S3 임시 키 업로드하기
long size = Files.size(tmp);
PutObjectRequest putObject = PutObjectRequest.builder()
.bucket(bucket)
.key(tempKey)
.contentType("application/x-ndjson")
.contentEncoding("gzip")
.build();
try(InputStream input = Files.newInputStream(tmp)) { // s3에 올리는 로직
s3.putObject(putObject, RequestBody.fromInputStream(input,size));
}
s3.copyObject(c -> c
.sourceBucket(bucket).sourceKey(tempKey)
.destinationBucket(bucket).destinationKey(backupKey));
s3.deleteObject(b -> b.bucket(bucket).key(tempKey));
log.info("백업 업로드됨, s3://{}/{}", bucket, backupKey);
} catch(Exception e) {
log.error("백업 실패함, date = {}", date, e);
throw new RuntimeException(e);
} finally {
if(tmp != null) {
try {
Files.deleteIfExists(tmp);
} catch (IOException ioException) {
}
}
} // 바깥쪽 try
}
}
Scheduler
package com.codeit.monew.article.backUp.scheduler;
import com.codeit.monew.article.backUp.service.ArticleBackupService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
@Component
@RequiredArgsConstructor
@Slf4j
public class BackupScheduler {
private final ArticleBackupService backupService;
// 초, 분, 시, 일, 월, 요일
// 매일 0시, 전날의 데이터를 백업함
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void backupArticle() {
LocalDate target = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(1);
log.info("뉴스기사 백업 시작, {}", target);
backupService.articleBackup(target);
}
}
의존성
implementation 'software.amazon.awssdk:s3:2.31.7' // 아마존 S3 버킷 백업/복구
implementation 'com.fasterxml.jackson.core:jackson-databind' // Jackson
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' // Jackson
implementation 'org.springframework.boot:spring-boot-starter-logging' // 로깅
'Monew' 카테고리의 다른 글
| CD(지속적 배포) 작성 (0) | 2025.09.19 |
|---|---|
| S3에 백업한 뉴스 기사를 '복구'하는 기능 (0) | 2025.09.19 |
| CI(지속적 통합) 작성 (0) | 2025.09.12 |
| build.gradle (0) | 2025.09.04 |
| Jacoco를 활용하기 (테스트 커버리지) (0) | 2025.09.04 |