Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore: 🔧 30x Performance Improvement in PreAuthorize AOP #216

Merged
merged 6 commits into from
Jan 8, 2025

Conversation

psychology50
Copy link
Member

@psychology50 psychology50 commented Jan 8, 2025

작업 이유

  • We were using a custom @PreAuthorize annotation to verify user authentication in the socket module.
  • While it worked well, synchronization was necessary to avoid conflicts caused by StandardEvaluationContext in a concurrent environment.
  • Additionally, I wanted to move away from using Spring AOP, as it felt cumbersome and inefficient.

작업 사항

1️⃣ Before

@MessageMapping(CHAT_MESSAGE_PATH)
@PreAuthorize("#isAuthenticated(#principal) and @chatRoomAccessChecker.hasPermission(#chatRoomId, #principal)")
fun sendMessage(
    @DestinationVariable chatRoomId: Long,
    @Validated payload: ChatMessageDto.Request,
    principal: UserPrincipal,
    @Header("x-message-id") messageId: String?
) { ... }
  • The @PreAuthorize annotation relies on string-based expressions.
  • These expressions are evaluated in a StandardEvaluationContext, but since Spring beans are highly dynamic, this negatively impacts caching performance.

2️⃣ After

@MessageMapping(CHAT_MESSAGE_PATH)
fun sendMessage(
    @DestinationVariable chatRoomId: Long,
    @Validated payload: ChatMessageDto.Request,
    principal: UserPrincipal,
    @Header("x-message-id") messageId: String?
) = authorize(
    principal = principal,
    serviceClass = ChatRoomAccessChecker::class,
    methodName = ChatRoomAccessChecker::hasPermission.name(),
    chatRoomId, principal
) { ... }
  • The PreAuthorizer provides four methods:
    • permitAll() : Allows all requests.
    • authenticate(principal) : Verifies authentication only.
    • authorize(serviceClass, methodName, *args) : Checks authorization only.
    • authorize(principal, serviceClass, methodName, *args) : Performs both authentication and authorization.
  • While the allocation of methodName could be improved, the overall design ensures better type safety.

3️⃣ Benchmark

Test Scenario AOP (μs) Reflection (μs) Performance Improvement
Simple Authentication (Success) 16.04 0.42 38.2x faster
Simple Authorization (Success) 19.90 3.74 5.3x faster
Complex Authorization (Success) 5.51 1.00 5.5x faster
Authentication + Authorization (Success) 23.42 1.86 12.6x faster
Failed Authentication 23.49 6.22 3.8x faster
Failed Authorization 33.85 11.08 3.1x faster
Failed Complex Authorization 20.62 6.08 3.4x faster
Failed Combined Check 30.48 7.56 4.0x faster

Looking at the warm-up performance data:

Batch AOP (μs) Reflection (μs) Notes
1 75.84 304.09 Initial warm-up cost
2 32.00 6.00 Rapid optimization phase
3 30.00 2.00 Near steady state
10 30.00 4.00 Final steady state
  • Performance improved by 3 to 38 times, as shown in the benchmark results.
  • Drawback: The initial warm-up cost is 304μs, which is approximately 4 times higher than AOP’s 75μs.
    • However, warm-up happens only once at application startup, and optimal performance is achieved after 2–3 batch executions, making this drawback negligible.

리뷰어가 중점적으로 확인해야 하는 부분

  • Main Limitation: Only one Manager bean can be used at a time.
  • This change has not yet been applied to production.
    • It will be applied after conducting additional stability tests.

발견한 이슈

val method = serviceClass.memberFunctions.find { it.name == methodName }
    ?: throw NoSuchMethodException("$methodName not found in ${serviceClass.qualifiedName}")
val parameterTypes = method.parameters.drop(1).map { it.type.javaType }

val javaMethod = serviceClass.java.getDeclaredMethod(
    methodName,
    *parameterTypes.map {
        when (it) {
            is Class<*> -> it  // 이미 Class 객체라면 그대로 사용
            is ParameterizedType -> it.rawType as Class<*>  // 제네릭 타입이라면 raw type 사용
            else -> throw IllegalStateException("Unsupported type: $it")
        }
    }.toTypedArray()
)
  • This seemingly trivial code was added due to Kotlin's automatic type inference.
    • When using non-null Long types in Kotlin manager classes, Kotlin automatically converts them to the optimal primitive type (long).
    • However, when using Java reflection, the type is always retrieved as java.lang.Long, causing type mismatch errors.
    • To resolve this, the code dynamically converts the type parameters to match those being used by Kotlin.

@psychology50 psychology50 added the refactoring 리팩토링 작업 label Jan 8, 2025
@psychology50 psychology50 self-assigned this Jan 8, 2025
@psychology50 psychology50 merged commit 5dee6f8 into dev Jan 8, 2025
1 check passed
@psychology50 psychology50 deleted the refactor/spring-aop-to-kotlin-trailing branch January 8, 2025 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactoring 리팩토링 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant