ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 홈페이지 FileSystem 구조 개선 (DI)
    HelloJkw 개발 2024. 10. 2. 02:03

    내 홈페이지는 총 4가지 파일 시스템을 사용할 수 있다.

    테스트를 위한 Local file system, InMemory file system

    서비스를 위한 Azure file system, Dropbox file system.

    설정 파일 appsettings를 읽어서 어떤 파일 시스템을 쓸지 할지 결정한다.

    각 서비스마다 각자 다른 파일시스템을 설정할 수 있다.

     

    설정은 이렇게 생겼다.

    이번 개선에서 설정 파일을 변경하는 것은 아니다.

    {
      // FileSystem은 한 번만 설정하는 값이다.
      // 연결 정보만 저장한다.
      FileSystem: {
        Dropbox: {
          ClientId: "",
          ClientSecret: ""
        },
        Azure: {
          ConnectionString: ""
        }
      }
      DiaryService: {
        // Path는 각 서비스마다 정의한다.
        // 각 타입마다 각각 지정한다. (경로가 너무 달라서 이렇게 하기로 정했다.)
        Path: {
          Dropbox: {
            DiaryListPath: "/hellojkw/db/diary/diary-info"
          },
          Azure: {
            DiaryListPath: "diary::/diary-info",
          }
        }
      }
    }

     

    문제점1: 종속성 문제

    위에도 써 있지만, 설정을 보고 사용할 파일 시스템을 고르는 if문이 코드에 그대로 있었다.

    이 부분이 홈페이지 서비스와 파일 시스템의 종속성을 높이는 부분이라고 생각한다.

    // FileSystemService 내부
    Dictionary<FileSystemType, IFileSystem> CreateFileSystems()
    {
      var fsDic = new Dictionary<FileSystemType, IFileSystem>();
      if (options.FileSystemType == "Dropbox")
      {
        fsDic.Add(FileSystemType.Dropbox, new DropboxFileSystem());
      }
      else if (options.FileSystemType == "Azure")
      {
        fsDic.Add(FileSystemType.Azure, new AzureFileSystem());
      }
      // add local, in-memory file system
      return fsDic
    }

     

    파일 시스템 타입을 dropbox, azure를 비교하는 코드가 FileSystemService안에 없었으면 좋겠다.

    FileSystemService가 FileSystemType이 4가지 종류라는 것을 몰랐으면 좋겠다.

    FileSystemService의 역할은 필요한 타입의 FileSystem을 제공하는 것일 뿐,

    FileSystem이 몇 종류나 있는지 알 필요는 없다고 생각한다.

    해결

    각 FileSystem은 각자 자신을 등록한다.

    FileSystemService는 등록되어 있는 FileSystem목록을 받아올 뿐이다.

    FileSystem을 요청하면 타입에 맞는 것을 찾아 반환한다.

    만약 AWS S3를 사용하게 된다면 FileSystemType.S3와  program.cs에 AddS3FileSystem함수를 추가하면 된다.

    // Program.cs
    
    builder.Services.AddInMemoryFileSystem();
    builder.Services.AddLocalFileSystem();
    if (fsOption.Dropbox != null)
    {
        builder.Services.AddDropboxFileSystem(fsOption.Dropbox);
    }
    if (fsOption.Azure != null)
    {
        builder.Services.AddAzureFileSystem(fsOption.Azure);
    }
    
    // FileSystemService
    // FileSystemType을 인자로 받고 있지만, 그 종류가 무엇인지는 모른다.
    public interface IFileSystemService
    {
        IFileSystem GetFileSystem(FileSystemSelectOption fileSystemSelectOption, PathMap pathOption);
    }
    
    public class FileSystemService : IFileSystemService
    {
        private readonly IEnumerable<IFileSystemBuilder> _fileSystems;
    
        public FileSystemService(IEnumerable<IFileSystemBuilder> fileSystems)
        {
            _fileSystems = fileSystems;
        }
        
        public IFileSystem GetFileSystem(FileSystemSelectOption fileSystemSelectOption, PathMap pathMap)
        {
            return GetFileSystemBuilder(fileSystemSelectOption.FileSystemType)
                .Build(pathMap);
        }
        
        private IFileSystemBuilder GetFileSystemBuilder(FileSystemType fsType)
        {
            var fileSystem = _fileSystems.FirstOrDefault(fs => fs.FileSystemType == fsType);
            if (fileSystem == null)
            {
                throw new ArgumentException();
            }
            return fileSystem;
        }
    }

     

    문제점2: 각 서비스에서 FileSystem이 아닌 FileSystemService를 DI 하는 문제

    아래 예제를 보면 DiaryService에서 IFileSystem을 받아오지 않고, FileSystemService를 받아오고 있다.

    의미상 DiaryService는 본인이 사용하는 FileSystem에만 관심이 있어야 한다.

    FileSystemService를 굳이 알 필요가 없다.

    (테스트 코드도 길어진다)

    class DiaryService
    {
      readonly IFileSystem _fileSystem;
      
      public DiaryService(
        DiaryOption option,
        FileSystemService fsService
      )
      {
        // IFileSystem을 바로 받아와도 되는데,
        // FileSystemService를 받아와서 fileSystem을 생성하고 있다.
        _fileSystem = fsService.GetFileSystem(option.FileSystemSelect, option.Paths);
      }
    }

     

    해결

    서비스 관련 DI를 등록할 때 IFileSystem을 함께 등록하면 된다.

    Singleton객채에 구분자를 지정할 수 있다. AddKeyedSingleton 함수를 사용한다.

    services.AddKeyedSingleton<IFileSystem>(nameof(DiaryService), (provider, key) =>
    {
        var option = provider.GetRequiredService<DiaryOption>();
        var fileSystemService = provider.GetRequiredService<IFileSystemService>();
        var fileSystem = fileSystemService.GetFileSystem(option.FileSystemSelect, option.Path);
        return fileSystem;
    });
    services.AddKeyedSingleton<IFileSystem>(nameof(DiarySearchService), (provider, key) =>
    {
        var option = provider.GetRequiredService<DiaryOption>();
        var fileSystemService = provider.GetRequiredService<IFileSystemService>();
        var fileSystem = fileSystemService.GetFileSystem(option.SearchEngineFileSystem, option.Path);
        return fileSystem;
    });

     

    public class DiaryService : IDiaryService
    {
        private readonly IFileSystem _fs;
    
        public DiaryService(
            [FromKeyedServices(nameof(DiaryService))] IFileSystem fileSystem)
        {
            _fs = fileSystem;
        }
    }
    
    public class DiarySearchService : IDiarySearchService
    {
        private readonly IFileSystem _fs;
    
        public DiarySearchService(
            [FromKeyedServices(nameof(DiarySearchService))] IFileSystem fileSystem
        )
        {
            _fs = fileSystem;
        }
    }

     

     

    끝!

     

    사실 처음 만들 때 부터 구조가 좋지 않다고 생각했다.

    동작하는데 별 문제가 몇 년 동안 그대로 두다가 이제야 변경하였다...

     

Designed by Tistory.