URL 패턴으로 권한을 제어하는 것만으로 충분할까요? 서비스 메서드 자체에 권한을 걸 수는 없을까요?

SecurityFilterChain에서 URL 기반으로 권한을 설정하는 것은 기본이지만, 복잡한 비즈니스 로직에서는 메서드 단위 로 세밀한 권한 제어가 필요합니다. Spring Security의 Method Security 가 이를 해결합니다.

개념 정의

Method Security 는 서비스 계층의 메서드에 직접 권한 검사를 적용하는 기능입니다. AOP를 기반으로 동작하며, 어노테이션으로 선언적 보안을 적용합니다.

설정

Method Security를 사용하려면 설정 클래스에 @EnableMethodSecurity를 추가합니다.

JAVA
@Configuration
@EnableMethodSecurity  // Spring Security 6+
public class MethodSecurityConfig {
}

이 어노테이션 하나로 @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter가 활성화됩니다. Spring Security 5의 @EnableGlobalMethodSecurity는 deprecated되었으므로 사용하지 않습니다.

@PreAuthorize — 메서드 실행 전 권한 검사

가장 많이 사용하는 어노테이션입니다. 메서드가 실행되기 전에 SpEL 표현식으로 권한을 검사합니다.

JAVA
@Service
public class AdminService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        // ADMIN 권한이 없으면 AccessDeniedException 발생
        userRepository.deleteById(userId);
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    public List<UserDto> getAllUsers() {
        return userRepository.findAll().stream()
            .map(UserDto::from)
            .toList();
    }
}

권한이 없으면 메서드 본문이 실행되지 않고 즉시 AccessDeniedException이 발생합니다. 이것이 @PostAuthorize와의 핵심 차이입니다.

SpEL 표현식

표현식설명
hasRole('ADMIN')ROLE_ADMIN 권한 보유 확인
hasAnyRole('ADMIN', 'USER')하나 이상의 역할 보유
hasAuthority('WRITE')특정 권한 보유 (ROLE_ 접두사 없이)
isAuthenticated()인증된 사용자
isAnonymous()익명 사용자
#paramName메서드 파라미터 참조
authentication현재 Authentication 객체
principal현재 Principal 객체

파라미터 참조

SpEL에서 #파라미터명으로 메서드 인자를 참조할 수 있습니다. "본인만 수정 가능" 같은 조건을 표현할 때 유용합니다.

JAVA
// 자신의 정보만 수정 가능
@PreAuthorize("#userId == authentication.principal.id")
public void updateProfile(Long userId, ProfileRequest request) {
    // ...
}

// ADMIN이거나 본인만 접근 가능
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserDto getUser(Long userId) {
    return userRepository.findById(userId)
        .map(UserDto::from)
        .orElseThrow();
}

authentication은 현재 SecurityContext의 Authentication 객체, principalUserDetails 구현체를 가리킵니다.

@PostAuthorize — 메서드 실행 후 권한 검사

메서드를 먼저 실행하고, 반환값을 기반으로 권한을 검사합니다.

JAVA
@PostAuthorize("returnObject.createdBy == authentication.name")
public Document getDocument(Long documentId) {
    return documentRepository.findById(documentId).orElseThrow();
    // 반환된 Document의 createdBy가 현재 사용자가 아니면 AccessDeniedException
}

returnObject는 메서드의 반환값을 참조하는 SpEL 변수입니다.

@PostAuthorize와 부수 효과

@PostAuthorize는 메서드를 ** 먼저 실행 **합니다. 따라서 부수 효과(side effect)가 있는 메서드에 사용하면 위험합니다.

JAVA
// 위험! 메서드가 먼저 실행되므로 삭제가 먼저 수행됨
@PostAuthorize("returnObject.owner == authentication.name")
public Document deleteDocument(Long id) {
    // 삭제 후에 권한 검사 → 이미 삭제됨!
    return documentRepository.delete(id);
}

삭제, 수정 같은 변경 작업에는 반드시 @PreAuthorize를 사용해야 합니다. @PostAuthorize는 조회 메서드에서 "본인이 작성한 문서만 반환"처럼 반환값 기반 검증에만 적합합니다.

@Secured

간단한 역할 기반 검사에 사용합니다. SpEL을 지원하지 않습니다.

JAVA
@Secured("ROLE_ADMIN")
public void adminOnly() { ... }

@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { ... }

@EnableMethodSecurity(securedEnabled = true)로 활성화해야 합니다.

@PreFilter / @PostFilter

컬렉션의 요소를 필터링합니다.

JAVA
// 입력 리스트에서 현재 사용자 소유의 항목만 필터링
@PreFilter("filterObject.owner == authentication.name")
public void deleteDocuments(List<Document> documents) {
    documentRepository.deleteAll(documents);
}

// 반환 리스트에서 현재 사용자 소유의 항목만 필터링
@PostFilter("filterObject.owner == authentication.name")
public List<Document> getAllDocuments() {
    return documentRepository.findAll();
}

filterObject는 컬렉션의 각 요소를 참조합니다.

커스텀 권한 로직

SpEL에서 스프링 빈을 참조하여 복잡한 권한 검사를 수행할 수 있습니다.

JAVA
// 커스텀 권한 검증 빈
@Component("authz")
public class AuthorizationChecker {

    private final TeamMemberRepository teamMemberRepository;

    public boolean isTeamMember(Long teamId, Long userId) {
        return teamMemberRepository.existsByTeamIdAndUserId(teamId, userId);
    }

    public boolean isDocumentOwner(Long documentId, String username) {
        return documentRepository.findById(documentId)
            .map(doc -> doc.getCreatedBy().equals(username))
            .orElse(false);
    }
}
JAVA
// SpEL에서 커스텀 빈 참조
@PreAuthorize("@authz.isTeamMember(#teamId, authentication.principal.id)")
public void addMemberToTeam(Long teamId, MemberRequest request) {
    // ...
}

@PreAuthorize("@authz.isDocumentOwner(#documentId, authentication.name) " +
    "or hasRole('ADMIN')")
public void editDocument(Long documentId, DocumentRequest request) {
    // ...
}

커스텀 메타 어노테이션

자주 사용하는 권한 검사를 메타 어노테이션으로 만들 수 있습니다.

JAVA
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface AdminOnly {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public @interface AdminOrSelf {
}
JAVA
@Service
public class UserService {

    @AdminOnly
    public void deleteUser(Long userId) { ... }

    @AdminOrSelf
    public UserDto getUser(Long userId) { ... }
}

주의할 점

같은 클래스 내부 호출에서 @PreAuthorize가 무시되는 문제

Method Security는 AOP 프록시 기반으로 동작합니다. 같은 클래스 내에서 this.method()로 호출하면 프록시를 거치지 않기 때문에 @PreAuthorize가 적용되지 않습니다.

JAVA
@Service
public class DocumentService {
    public void process(Long docId) {
        this.deleteDocument(docId);  // ← 프록시를 거치지 않음! 권한 검사 X
    }

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteDocument(Long docId) { ... }
}

이것은 @Transactional의 self-invocation 문제와 동일한 원리입니다. 해결하려면 클래스를 분리하거나 AopContext.currentProxy()를 사용해야 합니다.

hasRole과 hasAuthority의 ROLE_ 접두사 혼동

hasRole('ADMIN')은 내부적으로 ROLE_ADMIN 권한을 확인합니다. DB에 권한을 ADMIN으로 저장했다면 hasAuthority('ADMIN')을 사용해야 합니다. hasRole('ROLE_ADMIN')으로 쓰면 ROLE_ROLE_ADMIN을 찾게 되어 항상 권한 거부가 발생합니다.

@PreAuthorize 표현식에서 SpEL 오류가 500으로 반환되는 문제

SpEL 표현식에 오타가 있으면(#userld 대신 #userId 등) 런타임에 SpelEvaluationException이 발생하여 500 Internal Server Error가 반환됩니다. 컴파일 타임에 검증되지 않으므로 ** 테스트가 필수 **입니다.

정리

항목설명
활성화@EnableMethodSecurity (Spring Security 6+)
@PreAuthorize메서드 실행 ** 전** 권한 검사 — 변경 작업에 적합
@PostAuthorize메서드 실행 ** 후** 반환값 기반 검사 — 조회 작업에 적합
SpEL 참조#파라미터, authentication, principal, returnObject
커스텀 빈@빈이름.메서드() 형태로 복잡한 권한 로직 위임
메타 어노테이션@AdminOnly 같은 커스텀 어노테이션으로 재사용
동작 원리AOP 프록시 기반 — 내부 호출 시 권한 검사 미적용
댓글 로딩 중...