비대해진 Drive 서비스 리팩토링 및 권한 상속 모델을 적용한 드라이브 자원 공유

Jong Hwan
|2026. 3. 29. 17:03

기존 구조와 문제

초기에는 DriveController에서 단일 DriveService를 주입받아 모든 비즈니스 로직을 처리하는 구조였다.

DriveController
 └── DriveService
      └── DriveEmbeddingService
  
DriveEmbeddingScheduler
 └── DriveEmbeddingService

 

이 구조에서 DriveService는 다음과 같은 다양한 역할을 홀로 비대하게 수행하고 있었다.

  • 폴더의 생명주기 관리 (생성, 이동, 삭제, 수정 등)
  • 파일의 생명주기 관리 (생성, 이동, 삭제, 수정 등)
  • 드라이브 자체의 관리 및 유저 식별
  • AI 연동 질의 시 참조될 파일 및 폴더 관리

기능이 추가될수록 단일 클래스가 비대해졌고, 코드의 가독성과 확장성이 떨어졌다. 각 도메인(파일, 폴더, 드라이브 등)의 경계가 모호해져 유지보수성을 저해함으로써, 미래에 기술 부채로 다가올 것으로 예상되었다.

 

 

1차 리팩토링: Facade 패턴을 통한 책임 분리와 오케스트레이션

DriveController
 └── DriveFacade (흐름 제어 및 트랜잭션 관리)
      ├── DriveFolderService    (폴더 생명주기 관리)
      ├── DriveFileService      (파일 생명주기 관리)
      ├── DriveManageService    (드라이브 생성 및 유저 식별)
      ├── DriveRefService       (질의 참조용 폴더/파일 관리)
      └── DriveEmbeddingService (폴더 임베딩 상태 업데이트)

DriveEmbeddingScheduler
 └── DriveEmbeddingService (임베딩 API 호출 및 상태 관리)
  • 각 서비스는 본연의 비즈니스 로직만 수행하도록 하였으며, DriveFacade는 이들을 주입받아 오케스트레이션 역할을 담당하도록 했다.
  • 특히, 트랜잭션의 범위가 명확해지는 장점이 있었다. Facade 계층의 메서드 진입점에서 트랜잭션을 시작하므로, 여러 서비스 간의 복합적인 상태 변경이 하나의 원자적 단위로 묶이기 때문이다.
  • 다음은 파일/폴더 이동 로직의 예시이다.
/**
 *  파일/폴더들을 특정 폴더로 이동 (Facade Layer)
 */
@Transactional
public void moveItems(String userId, DriveModel.MoveItemsReq req) {
    // 이동에 따른 임베딩 상태 영향 받는 폴더 아이디 리스트
    Set<String> affectedFolderIds = new HashSet<>();

    if (req.getTargetFolderId() != null) {
        affectedFolderIds.add(req.getTargetFolderId());
    }

    // 파일 이동 처리
    driveFileService.moveFiles(req.getFileIds(), req.getTargetFolderId(), affectedFolderIds);

    // 폴더 이동 처리
    driveFolderService.moveFolders(req.getFolderIds(), req.getTargetFolderId(), affectedFolderIds);

    // 이동에 따라 영향 받는 폴더 임베딩 상태 변경
    driveEmbeddingService.updateFolderEmbStatusByFolderId(affectedFolderIds);
}

 

 

 

 

공유 기능이라는 새로운 요구사항: 1:1에서 M:N으로 확장

드라이브 기능이 운영되던 중, 드라이브의 자원(파일/폴더)들은 다른 사용자와 공유될 수 있으며 공유 드라이브 또한 추가된다는 새로운 비즈니스 요구사항이 추가되었다. 이로 인해 기존 아키텍처와 DB 설계에 변화가 필요해졌다.

 

기존:

  • 기존에는 User와 Drive가 1:1 관계였다. 
  • 따라서 드라이브의 PK인 driveCode만으로 이 자원이 누구의 것인지 즉, 접근 권한이 있는지를 판별할 수 있었다.
  • 따라서 DB 쿼리와 인덱스 또한 driveCode에 강하게 결합되어 있었다.

요구사항 추가 후:

  • User와 Drive가 M:N 관계로 변경되어야 했다.
  • 이제 driveCode만으로는 어느 사용자가 해당 자원에 접근 가능한지 구분할 수 없게 되었다.
  • 또한 권한 등급(읽기/쓰기/관리자 등)에 따라 공유받은 자원에 대해 취할 수 있는 행동도 제한해야 했다.

결론적으로, 다형적인 권한 상태를 제어하고 M:N 관계를 해소할 단일 진실 공급원 역할의 별도 매핑 테이블이자 사건 엔티티가 필요해졌다.

 

 

 

2차 리팩토링: Command/Query 분리와 권한 서비스 도입

복잡해진 요구사항(공유, 권한 검증, 임베딩 등)을 하나의 Facade에서 모두 처리하려다 보니, Facade가 너무 많은 것을 알게 되는 God Class가 될 가능성이 있었다.

 

특히 단순히 데이터를 조회하는 로직(Query)과 상태를 변경하는 로직(Command)의 복잡도 차이가 컸다. 이를 해결하기 위해 Facade를 CQS 형태의 설계를 적용했다.

DriveController
 ├── DriveQueryFacade (조회 전용)
 │    ├── DriveManageService
 │    ├── DriveFileService
 │    ├── DriveFolderService
 │    └── DriveRefService
 │
 └── DriveCommandFacade (상태 변경 및 비즈니스 로직 전용)
      ├── DriveManageService
      ├── DriveFileService
      ├── DriveFolderService
      ├── DriveRefService
      ├── DriveEmbeddingService
      └── DrivePermissionService
 
DriveEmbeddingScheduler
 └── DriveEmbeddingService

 

조회 퍼사드와 명령 퍼사드를 분리함으로써 코드의 응집도를 높였고, DrivePermissionService라는 전담 서비스를 추가하여 M:N 환경에서의 공유 권한 검증 로직을 처리하도록 했다.

 

 

 

AOP를 이용한 권한 검증과 권한 상속 모델