동적 권한 관리

동적 권한 관리

작년 한 프로젝트에서 Admin이 직접 리소스별 접근 권한을 제어할 수 있는 기능을 구현하며 얻은 인사이트를 나누고자 합니다. 서비스 중단 없는 권한 갱신부터 DB 설계까지, 실제 적용 사례를 통해 동적 권한 관리를 어떻게 효율적으로 구현했는지 단계별로 알아보겠습니다.

도메인 설계 - 동적 권한 관리를 위한 DB 구조

먼저, 동적 권한 관리를 적용하기 위한 Menu 도메인은 다음과 같이 구성되어 있습니다.

public class Menu {
    private String id;
    private String menuContext;                // ex) admin:project:button
    private List<String> permittedRoleCodes;   // 해당 메뉴에 접근 가능한 권한
}

menuContext를 통해 서비스의 계층 구조를 명확히 정의했습니다. 또한 permittedRoleCodes에 담긴 권한 정보를 활용해 런타임 시점에도 유연하게 인가(Authorization)를 수행할 수 있는 기반을 마련했습니다.

백엔드 - Spring AOP를 활용한 동적 권한 검증 로직 구현

커스텀 어노테이션(@PermissionCheckRequired)을 정의하고, 이를 처리할 Aspect를 구성했습니다. 이 구조의 핵심은 비즈니스 로직과 권한 검증 로직의 완전한 분리입니다. PermissionCheckRequiredAspect는 단순히 어노테이션의 값을 추출하는 역할만 수행하고, 실제 권한 체크 로직은 외부에서 주입받은 permissionConsumer가 담당하게 하여 결합도를 낮추고 유지보수성을 높였습니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheckRequired {
    String menuContext();
}
@Aspect
@RequiredArgsConstructor
public class PermissionCheckRequiredAspect {
    @Around("@annotation({packageName}.PermissionCheckRequired)")
    public Object checkPermission(ProceddingJoinPoint point) throws Throwable {
        // 어노테이션 값 추출 로직
    }
}
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@RequiredArgsConstructor
public class PermissionConfig {
    private final ObjectProvider<Object> beanProvider;
    private final UserSeek userSeek;

    @Bean
    public PermissionCheckRequiredAspect permissionCheckRequiredAspect() {

        return new PermissionCheckRequiredAspect(menuContext -> {
         // 권한 체크 로직
    }
}

이제 권한 제어가 필요한 각 API 메서드에 이 어노테이션을 선언하기만 하면 됩니다. 덕분에 복잡한 권한 체크 코드를 서비스 로직에 반복해서 작성할 필요가 없어졌습니다.

@PostMapping("/sync-users/command")
@PermissionCheckRequired(menuContext = "admin:user:sync")
public CommandResponse syncUsers(@RequestBody SyncUsersCommand command) {
    // 비즈니스 코드
}

프론트엔드 - 컴포넌트를 활용한 메뉴 등록 및 권한 제어

프론트엔드에서는 사용자에게 허용된 기능만 노출하여 혼란을 줄이고 보안 사고를 미연에 방지해야 합니다. 이를 위해 권한 여부에 따라 렌더링을 제어하는 공통 컴포넌트(WithPermission)를 활용했습니다.

export const WithPermission = ({
                                 children,
                                 menuContext
                               }: {
  children: ReactNode;
  menuContext: string;
}) => {
  const RenderByRoles = () => {
    // 권한 체크 로직
  return 
    <RenderByRoles/>
  );
}

이렇게 만든 컴포넌트는 페이지 전체를 감싸거나 특정 버튼을 감싸는 형태로 사용됩니다. 백엔드 어노테이션에 설정한 menuContext와 동일한 값을 사용하여 일관성을 유지합니다.

<WithPermission menuContext='admin:menu'>
  <MenuPageView/>
</WithPermission>

이외에도 정적 분석 스크립트를 활용하여 프로젝트를 분석해 메뉴 정보를 자동으로 등록하거나 업데이트할 수도 있습니다.

function extractWithPermissionProps(filePaths: string[]): MenuCdo[] {
  for (const filePath of filePaths) {
    const content = fs.readFileSync(filePath, 'utf-8');

    const regex = /<WithPermission...>/g; // 메뉴 정보 추출 

    let match;
    while ((match = regex.exec(content)) !== null) {
      // 추출한 메뉴 등록
    }
  }
}

마치며 - 유연한 권한 관리가 가져온 이점

지금까지 DB 설계부터 백엔드 AOP, 그리고 프론트엔드의 정적 분석을 통한 자동 등록까지 동적 권한 관리 시스템의 전 과정을 살펴보았습니다. 과거에는 메뉴 접근 제어가 변경 될 때마다 코드 수정과 재배포가 필수였습니다. 하지만 이제는 실시간 권한 반영이 가능해져 즉각적으로 대응할 수 있게 되었습니다.

Hustle Paul