Aspect-oriented programming 방식 중 Stackable Trait Pattern 구현 예제

View: 19 0 0
작성자: 코딩!제이크
카테고리: Scala Language
발행: 2026-03-13 수정 2026-03-13

다음과 같은 JSON 데이터가 있습니다.

[
  {  
      "firstName": "Ivan",
      "lastName": "Nikolov",
      "age": 26
  },
  {
     "firstName": "John",
     "lastName": "Smith",
     "age": 55
  },
  {  
      "firstName": "Maria",
      "lastName": "Cooper",
      "age": 19
  }
]

이 JSON code를 Scala 객체로 Parsing 하기 위해 Person이라는 case class를 만듭니다.

case class Person(firstName: String, lastName: String, age: Int)

JSON을 parsing하기 위해 json4s라는 parser를 build.sbt에 dependency로 추가합니다.

libraryDependencies += "org.json4s" %% "json4s-jackson" % "4.0.7"

json4s를 사용해서 다음과 같이 완성된 코드를 작성하였습니다.

import org.json4s.*
import org.json4s.jackson.JsonMethods.*
import scala.util.Using

trait DataReader:
  def readData(): List[Person]
  def readDataInefficiently(): List[Person]

class DataReaderImpl extends DataReader:
  given Formats = DefaultFormats
  private def readUntimed(): List[Person] =
    Using.resource(
      Option(getClass.getResourceAsStream("/users.json"))
        .getOrElse(throw new RuntimeException("users.json not found"))
      ) { is =>
        parse(StreamInput(is)).extract[List[Person]]
      }

  override def readData(): List[Person] = readUntimed()

  override def readDataInefficiently(): List[Person] =
    (1 to 10000).foreach { _ =>
      readUntimed()
    }
    readUntimed()

여기서 DataReader trait는 interface 역할을 하고 구현은 매우 간단합니다.

object DataReaderExample:
  def main(args: Array[String]): Unit = 
    val dataReader = new DataReaderImpl
    System.out.println(s"I just read the following data efficiently: ${dataReader.readData()}")
    System.out.println(s"I just read the following data inefficiently: ${dataReader.readDataInefficiently()}")

자, 이제 우리는 이 간단한 Application이 어떻게 수행되는지, 실행에는 어느 정도의 시간이 소요되는지 알고 싶습니다.
지난 글에서 언급했었던 cross-cutting concerns가 발생한 건데 이를 구현하기 위해 먼저 AOP을 적용하지 않고 원하는 기능을 작성해 보겠습니다.

import org.json4s.*
import org.json4s.jackson.JsonMethods.*
import scala.util.Using

class DataReaderImpl extends DataReader:
  given Formats = DefaultFormats

  private def readUntimed(): List[Person] =
    Using.resource(
      Option(getClass.getResourceAsStream("/users.json"))
        .getOrElse(throw new RuntimeException("users.json not found"))
      ) { is =>
        parse(StreamInput(is)).extract[List[Person]]
      }

  override def readData(): List[Person] = 
    val startMillis = System.currentTimeMillis()
    val result = readUntimed()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readData took ${time} milliseconds.")
    result

  override def readDataInefficiently(): List[Person] =
    val startMillis = System.currentTimeMillis()
    (1 to 10000).foreach { _ =>
      readUntimed()
    }
    val result = readUntimed()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readDataInefficiently took ${time} milliseconds.")
    result

AOP 적용 없이 DataReaderImpl을 리팩토링해서 method 내부에 timing 기능을 추가했습니다.
이 코드에서는 기존의 functionality와 부가적인 정보를 보여주는 코드가 섞여 있어서 가독성이 좋지 않고, method 재사용성을 떨어뜨릴 수 있습니다.

다음 코드는 AOP 방식으로 Stackable Trait Pattern을 구현한 코드입니다.

trait LoggingDataReader extends DataReader:
  abstract override def readData(): List[Person] =
    val startMillis = System.currentTimeMillis()
    val result = super.readData()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readData took ${time} milliseconds.")
    result

  abstract override def readDataInefficiently(): List[Person] =
    val startMillis = System.currentTimeMillis()
    val result = super.readDataInefficiently()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readDataInefficiently took ${time} milliseconds.")
    result

object DataReaderAOPExample:
  def main(args: Array[String]): Unit =
    val dataReader = new DataReaderImpl with LoggingDataReader
    System.out.println(s"I just read the following data efficiently: ${dataReader.readData()}")
    System.out.println(s"I just read the following data Inefficiently: ${dataReader.readDataInefficiently()}")

DataReader를 상속받아 trait LoggingDataReader를 새로 만들고 readData()와 readDataInefficiently() 메서드를 override 하여 timing 기능을 구현하였습니다.
abstract override 키워드는 super.readData(), super.readDataInefficiently()와 같이 super 호출에 의존하므로 독립적으로는 완성되지 않은 stackable modification이라는 것을 컴파일러에게 알려줍니다. main 문에서는 기존에 작성했던 순수한 기능만 가지고 있는 DataReaderImpl에 LoggingDataReader를 mixin하여 본연의 기능을 훼손하지 않는 상태로 자연스럽게 로깅 기능을 추가하였습니다.

이와 같이 Scala의 Stackable Trait Pattern을 사용하면 AOP(Aspect-oriented programming) 방식으로 다른 코드와 섞이지 않으면서도 유지보수가 가능한 코드 구현이 가능해지고, 또한 Scala의 mixin과 linearization 지원을 통해 같은 방식으로 로깅 외의 다른 부가 기능들도 조합하여 확장 할 수 있습니다.

본 내용은 Ivan Nikolov의 Scala Design Patterns Second Edition의 Chapter 5 Aspect-Oriented Programming and Components의 내용을 바탕으로 Scala3 기반 코드 수정 및 보강이 이루어졌고, 보충 설명이 추가 되었습니다.

comments 0