9.4. Service Implementation Pattern
9.4.1. StorageService Interface
java
public interface StorageService {
FileResponse upload(MultipartFile file, Long uploadedBy);
FileDownload download(Long fileId);
void delete(Long fileId);
}9.4.2. Implementation Structure
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("Failed to store the file.", 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("Failed to delete file: fileId={}", fileId, e);
}
fileMetadataRepository.deleteById(fileId);
}
}Extension
The current implementation uses the local file system, but when transitioning to cloud storage (e.g., S3) in the future, only a new implementation of the StorageService interface needs to be added. The interface-based design allows the storage strategy to be changed flexibly.