cross-cutting concerns를 해결하기 위한 Aspect-oriented programming
View: 75
0
0
작성자: 코딩!제이크
카테고리: Scala Language
발행: 2025-07-11
수정
2025-07-11
안녕하세요. 코딩!제이크입니다.
오늘은 Cross-cutting concerns(횡단 관심사)와 이를 해결하기 위한 Aspect-oriented programming에 대해서 알아보겠습니다.
cross-cutting concerns
소프트웨어 개발에서 여러 모듈이나 계층에 공통적으로 영향을 미치지만, 핵심 비즈니스 로직과는 직접적인 관련이 없는 기능들을 의미합니다. 이러한 기능들은 코드 전반에 걸쳐 “가로지르며 (cross-cutting) 나타납니다.
대표적인 예시
- 로깅(logging)
- 보안(auth, access control)
- 트랜잭션 관리
- 오류처리
- 캐싱
- 감사 로깅(auditing)
이러한 기능들은 각 클래스나 메서드에 일일이 삽입하면 중복 코드가 발생하고, 핵심 로직의 가독성이 저하되고, 유지보수의 어려움이 증가하게 됩니다.
이를 해결하기 위해서 AOP(Aspect-Oriented Programming)라는 프로그래밍 기법을 사용합니다.
Aspect-oriented programming (AOP)
AOP는 핵심 비즈니스 로직과 부가 관심사(cross-cutting concerns)를 분리하는 프로그래밍 방식입니다.
로깅, 보안체크, 트랜잭션 처리, 오류처리 등을 핵심 코드에 직접 섞지 않고 별도 모듈로 만들어 처리하기 때문에 유지 보수면에서 매우 유용합니다.
Scala에서 AOP 스타일을 구현하는 방법들
- Stackable Trait Pattern
- Higher-Order Functions Wrapping
- Macros
- Library 사용
각 방식들에 대해서 알아보겠습니다.
- Stackable Trait Pattern
스칼라의 Mixin 기능을 사용해서 구현하는 방식입니다.
trait Logging extends Service:
abstract override def doSomething(): Unit =
println("Before")
super.doSomething()
println("After")
val service = new RealService with Logging
Service trait를 상속해서 추상메서드인 doSomething을 override하고 필요한 로깅 메세지들을 작성해줍니다. super.doSomething은 아직 구현되지 않은 메서드이기 때문에 abstract라는 지시자를 메서드 선언부에 표시해주었습니다. Service의 실제 구현은 RealService에서 Service를 상속 받아서 작성되고 Super.doSomething() 코드로 실제 구현 메서드를 실행하게 됩니다.
- Higher-order Functions (고차함수 래핑)
다음과 같이 구현된 고차함수를 사용해서 반복적인 작업들을 처리할 수 있습니다.
def withLogging[T](block: => T): T =
println("Before")
val result = block
println("After")
result
withLogging {
doSomething()
}
로깅이 필요한 함수를 실행할 때 withLogging에 함수 자체를 인수로 전달해서 작업을 수행하는 방식입니다. 호출부마다 withLogging으로 해당 함수를 감싸야하지만 매우 깔끔한 코드를 구현할 수 있습니다.
- Macros 활용
Scala 3의 inline과 Macros를 활용하는 방식으로 컴파일 타임에, 해당 메서드에 자동으로 코드를 삽입하는 방식입니다.
import scala.quoted.*
inline def logExecution[T](inline expr: T): T = ${
logExecutionImpl('expr)
}
def logExecutionImpl[T: Type](expr: Expr[T])(using Quotes): Expr[T] =
import quotes.reflect.*
'{
val start = System.currentTimeMillis()
println("Starting...")
val result = $expr
val end = System.currentTimeMillis()
println(s"Finished in ${end - start} ms")
result
}
// 사용 예시
def someExpensiveComputation(): Int =
Thread.sleep(500)
42
@main def run(): Unit =
val result = logExecution {
someExpensiveComputation()
}
println(s"Result: $result")
예제에서 inline def logExecution[T](inline expr: T): T
와 def logExecutionImpl[T: Type](expr: Expr[T])(using Quotes): Expr[T]
메서드로 macro를 활용해 logging 기능을 구현해 주었고, main 함수에서 실제 실행하고자 하는 메서드인 someExpensiveComputation()
함수를 logExecution
인라인 메서드에 전달하여 로깅과 함께 처리하도록 구현하였습니다.
macro는 스칼라 3 지원 기능 중에서도 Advanced Topic에 속해서 어렵게 느껴질 수 있습니다. macro를 활용하면 반복을 완전히 제거할 수 있고, compile-time 안정성을 보장하지만 구현 난이도가 높고 복잡한 로직은 디버깅/테스트가 쉽지 않습니다.
- Library 사용
Java AOP Library를 활용하는 방식입니다. AspectJ와 같은 AOP 툴을 사용하는 방식인데 Library를 활용해서 Logging을 처리하기 위한 class를 작성하고 특정 패키지, 클래스, 메서드, 어노테이션을 기준으로 포인트 컷을 지정하면 Compile time 또는 Run time에서 바이트 코드를 조작하여 메서드 전후에 코드가 주입되어 처리됩니다.
class MyService:
def process(): Unit =
println("Doing work...")
import org.aspectj.lang.annotation.{Aspect, Before, After}
@Aspect
class LoggingAspect:
@Before("execution(* MyService.process(..))")
def logBefore(): Unit =
println("Before processing")
@After("execution(* MyService.process(..))")
def logAfter(): Unit =
println("After processing")
지금까지 Aspect-oriented programming (AOP) 방식에 대해서 알아보았습니다. AOP 방식을 활용하면 비즈니스 로직과 관련이 없는 반복적인 작업들을 모듈화하여 코드 중복을 제거하고 유지 보수 및 재사용성을 증가시킬 수 있습니다.
There is no article.
There is no article.
