동적 권한 관리
작년 한 프로젝트에서 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