Skip to content

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 인터페이스의 새로운 구현체만 추가하면 됩니다. 인터페이스 기반 설계를 통해 스토리지 전략을 유연하게 변경할 수 있습니다.

TIENIPIA QUALIFIED STANDARD