9.4. 서비스 구현 패턴
9.4.1. StorageService 인터페이스
java
public interface StorageService {
FileResponse upload(MultipartFile file, Long uploadedBy);
FileDownload download(Long fileId);
void delete(Long fileId);
}9.4.2. 구현체 구조
java
@Service
@RequiredArgsConstructor
public class LocalStorageService implements StorageService {
private final StorageProperties properties;
private final FileMetadataRepository fileMetadataRepository;
private final FileValidator fileValidator;
@Override
@Transactional
public FileResponse upload(MultipartFile file, Long uploadedBy) {
fileValidator.validate(file);
String storedFilename = FileNameGenerator.generate(file.getOriginalFilename());
Path targetPath = StoragePath.resolve(properties.uploadPath(), storedFilename);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(file.getInputStream(), targetPath);
} catch (IOException e) {
throw new StorageException("파일 저장에 실패했습니다.", e);
}
FileMetadataRecord record = fileMetadataRepository.insert(
file.getOriginalFilename(),
storedFilename,
targetPath.toString(),
file.getContentType(),
file.getSize(),
uploadedBy
);
return FileResponse.from(record);
}
@Override
@Transactional(readOnly = true)
public FileDownload download(Long fileId) {
FileMetadataRecord metadata = fileMetadataRepository.findById(fileId)
.orElseThrow(() -> new FileNotFoundException(fileId));
Path filePath = Path.of(metadata.getFilePath());
Resource resource = new FileSystemResource(filePath);
if (!resource.exists()) {
throw new FileNotFoundException(fileId);
}
return new FileDownload(
resource,
metadata.getOriginalFilename(),
metadata.getContentType()
);
}
@Override
@Transactional
public void delete(Long fileId) {
FileMetadataRecord metadata = fileMetadataRepository.findById(fileId)
.orElseThrow(() -> new FileNotFoundException(fileId));
try {
Files.deleteIfExists(Path.of(metadata.getFilePath()));
} catch (IOException e) {
log.error("파일 삭제 실패: fileId={}", fileId, e);
}
fileMetadataRepository.deleteById(fileId);
}
}확장
현재는 로컬 파일 시스템을 사용하지만, 추후 클라우드 스토리지(S3 등)로 전환할 경우 StorageService 인터페이스의 새로운 구현체만 추가하면 됩니다. 인터페이스 기반 설계를 통해 스토리지 전략을 유연하게 변경할 수 있습니다.