diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit
deleted file mode 100755
index 3d451ccc..00000000
--- a/.github/hooks/pre-commit
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-# .git/hooks/pre-commit
-
-echo "๐จ Gradle ๋น๋ ๊ฒ์ฆโฆ"
-
-if ! ./gradlew clean build --no-daemon; then
- echo "โ ๋น๋ ์คํจโ์ปค๋ฐ ์ค๋จ"
- exit 1
-fi
-
-echo "โ
๋น๋ ์ฑ๊ณต"
diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push
new file mode 100755
index 00000000..8308eb18
--- /dev/null
+++ b/.github/hooks/pre-push
@@ -0,0 +1,11 @@
+#!/bin/bash
+# .git/hooks/pre-push
+
+echo "๐จ Gradle ๋น๋ ๊ฒ์ฆ (push ์ ์คํ)โฆ"
+
+if ! ./gradlew clean build --no-daemon; then
+ echo "โ ๋น๋ ์คํจโํธ์ ์ค๋จ"
+ exit 1
+fi
+
+echo "โ
๋น๋ ์ฑ๊ณตโํธ์ ์งํ"
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index 1fb4dc95..4eef9734 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -12,6 +12,12 @@ object Dependencies {
//jexl
const val APACHE_COMMONS_JEXL = "org.apache.commons:commons-jexl3:${DependencyVersions.APACHE_COMMONS_JEXL_VERSION}"
+ //kotlinx serialization
+ const val KOTLINX_SERIALIZATION_JSON = "org.jetbrains.kotlinx:kotlinx-serialization-json:${DependencyVersions.KOTLINX_SERIALIZATION_VERSION}"
+
+ //kotlinx coroutines
+ const val KOTLINX_COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${DependencyVersions.KOTLINX_COROUTINES_VERSION}"
+
//junit
const val JUNIT = "org.jetbrains.kotlin:kotlin-test-junit5"
const val JUNIT_PLATFORM_LAUNCHER = "org.junit.platform:junit-platform-launcher"
diff --git a/buildSrc/src/main/kotlin/DependencyVersions.kt b/buildSrc/src/main/kotlin/DependencyVersions.kt
index 738177f8..a229be14 100644
--- a/buildSrc/src/main/kotlin/DependencyVersions.kt
+++ b/buildSrc/src/main/kotlin/DependencyVersions.kt
@@ -1,4 +1,10 @@
object DependencyVersions {
// JEXL
const val APACHE_COMMONS_JEXL_VERSION = "3.5.0"
+
+ // Kotlinx Serialization
+ const val KOTLINX_SERIALIZATION_VERSION = "1.6.3"
+
+ // Kotlinx Coroutines
+ const val KOTLINX_COROUTINES_VERSION = "1.8.1"
}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/src/main/kotlin/Plugins.kt
index 96482103..3532b5ef 100644
--- a/buildSrc/src/main/kotlin/Plugins.kt
+++ b/buildSrc/src/main/kotlin/Plugins.kt
@@ -2,6 +2,8 @@ object Plugins {
const val KOTLIN_JVM = "jvm"
const val JETBRAINS_KOTLIN_JVM = "org.jetbrains.kotlin.jvm"
const val KOTLIN_SPRING = "plugin.spring"
+ const val KOTLIN_SERIALIZATION = "plugin.serialization"
+ const val JETBRAINS_KOTLIN_SERIALIZATION = "org.jetbrains.kotlin.plugin.serialization"
const val SPRING_BOOT = "org.springframework.boot"
const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management"
const val KTLINT = "org.jlleitschuh.gradle.ktlint"
diff --git a/casper-application-domain/build.gradle.kts b/casper-application-domain/build.gradle.kts
index 7c81cef0..87c58d05 100644
--- a/casper-application-domain/build.gradle.kts
+++ b/casper-application-domain/build.gradle.kts
@@ -1,10 +1,14 @@
plugins {
kotlin(Plugins.KOTLIN_JVM) version PluginVersions.KOTLIN_VERSION
+ kotlin(Plugins.KOTLIN_SERIALIZATION) version PluginVersions.KOTLIN_VERSION
}
version = Projects.APPLICATION_DOMAIN_VERSION
dependencies {
+ implementation(Dependencies.KOTLINX_SERIALIZATION_JSON)
+ implementation(Dependencies.KOTLINX_COROUTINES_CORE)
+
testImplementation(Dependencies.JUNIT)
testRuntimeOnly(Dependencies.JUNIT_PLATFORM_LAUNCHER)
testImplementation(Dependencies.KOTLIN_TEST)
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt
new file mode 100644
index 00000000..4c6b7f70
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt
@@ -0,0 +1,524 @@
+package hs.kr.entrydsm.domain.ast.aggregates
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.entities.IfNode
+import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode
+import hs.kr.entrydsm.domain.ast.entities.NumberNode
+import hs.kr.entrydsm.domain.ast.entities.BooleanNode
+import hs.kr.entrydsm.domain.ast.entities.VariableNode
+import hs.kr.entrydsm.domain.ast.events.DomainEvents
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.services.TreeTraverser
+import hs.kr.entrydsm.domain.ast.services.TreeOptimizer
+import hs.kr.entrydsm.domain.ast.factories.ASTNodeFactory
+import hs.kr.entrydsm.domain.ast.specifications.ASTValiditySpec
+import hs.kr.entrydsm.domain.ast.specifications.NodeStructureSpec
+import hs.kr.entrydsm.domain.ast.values.NodeSize
+import hs.kr.entrydsm.domain.ast.values.TreeDepth
+import hs.kr.entrydsm.domain.ast.values.OptimizationLevel
+import hs.kr.entrydsm.domain.ast.values.ASTValidationResult
+import hs.kr.entrydsm.domain.ast.values.ASTOptimizationResult
+import hs.kr.entrydsm.domain.ast.values.TreeStatistics
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.aggregates.Aggregate
+import java.time.LocalDateTime
+import java.util.*
+
+/**
+ * ํํ์ AST๋ฅผ ๊ด๋ฆฌํ๋ ์ ๊ทธ๋ฆฌ๊ฒ์ดํธ์
๋๋ค.
+ *
+ * AST ํธ๋ฆฌ์ ์์ฑ, ์์ , ๊ฒ์ฆ, ์ต์ ํ ๋ฑ์ ๋น์ฆ๋์ค ๋ก์ง์
+ * ์บก์ํํ๊ณ ์ผ๊ด์ฑ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Aggregate(context = "ast")
+class ExpressionAST private constructor(
+ val id: String,
+ private var root: ASTNode,
+ private val traverser: TreeTraverser = TreeTraverser(),
+ private val optimizer: TreeOptimizer = TreeOptimizer(),
+ private val factory: ASTNodeFactory = ASTNodeFactory(),
+ private val validitySpec: ASTValiditySpec = ASTValiditySpec(),
+ private val structureSpec: NodeStructureSpec = NodeStructureSpec(),
+ private val createdAt: LocalDateTime = LocalDateTime.now(),
+ private var lastModifiedAt: LocalDateTime = LocalDateTime.now(),
+ private var optimizationLevel: OptimizationLevel = OptimizationLevel.NONE,
+ private var isValidated: Boolean = false,
+ private var validationResult: ASTValidationResult? = null
+) {
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ
+ private val domainEvents = mutableListOf()
+
+ /**
+ * ๋ฃจํธ ๋
ธ๋๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getRoot(): ASTNode = root
+
+
+ /**
+ * ์์ฑ ์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getCreatedAt(): LocalDateTime = createdAt
+
+ /**
+ * ๋ง์ง๋ง ์์ ์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getLastModifiedAt(): LocalDateTime = lastModifiedAt
+
+ /**
+ * ์ต์ ํ ๋ ๋ฒจ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getOptimizationLevel(): OptimizationLevel = optimizationLevel
+
+ /**
+ * ๊ฒ์ฆ ์ฌ๋ถ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun isValidated(): Boolean = isValidated
+
+ /**
+ * ๊ฒ์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getValidationResult(): ASTValidationResult? = validationResult
+
+ /**
+ * ํธ๋ฆฌ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getSize(): NodeSize = NodeSize.of(root.getSize())
+
+ /**
+ * ํธ๋ฆฌ ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getDepth(): TreeDepth = TreeDepth.of(root.getDepth())
+
+ /**
+ * ๋ณ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getVariables(): Set = root.getVariables()
+
+ /**
+ * ๋ฃจํธ ๋
ธ๋๋ฅผ ์ค์ ํฉ๋๋ค.
+ */
+ fun setRoot(newRoot: ASTNode) {
+ if(!validitySpec.isSatisfiedBy(newRoot)) {
+ val reason = validitySpec.getWhyNotSatisfied(newRoot)
+ throw ASTException.invalidRootNode(reason)
+ }
+
+ val oldRoot = this.root
+ this.root = newRoot
+ this.lastModifiedAt = LocalDateTime.now()
+ this.isValidated = false
+ this.validationResult = null
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐ์
+ addDomainEvent(mapOf(
+ "eventType" to DomainEvents.AST_MODIFIED,
+ "aggregateId" to id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "oldRoot" to oldRoot.toString(),
+ "newRoot" to newRoot.toString(),
+ "modifiedAt" to LocalDateTime.now().toString()
+ )
+ ))
+ }
+
+ /**
+ * AST๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
+ */
+ fun validate(): ASTValidationResult {
+ val violations = mutableListOf()
+
+ // ์ ํจ์ฑ ๊ฒ์ฆ
+ if (!validitySpec.isSatisfiedBy(root)) {
+ val reason = validitySpec.getWhyNotSatisfied(root)
+ throw ASTException.invalidRootNode(reason)
+ }
+
+ // ๊ตฌ์กฐ ๊ฒ์ฆ
+ if (!structureSpec.isSatisfiedBy(root)) {
+ val reason = structureSpec.getWhyNotSatisfied(root)
+ throw ASTException.invalidNodeStructure(root.toString(), reason)
+ }
+
+ // ํฌ๊ธฐ ์ ํ ๊ฒ์ฆ
+ if (getSize().isAtLimit()) {
+ throw ASTException.sizeLimitExceeded()
+ }
+
+ // ๊น์ด ์ ํ ๊ฒ์ฆ
+ if (getDepth().isAtLimit()) {
+ throw ASTException.depthLimitExceeded()
+ }
+
+ val result = ASTValidationResult(
+ isValid = violations.isEmpty(),
+ violations = violations,
+ validatedAt = LocalDateTime.now(),
+ astId = id
+ )
+
+ this.isValidated = true
+ this.validationResult = result
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐ์
+ addDomainEvent(mapOf(
+ "eventType" to DomainEvents.AST_VALIDATED,
+ "aggregateId" to id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "isValid" to result.isValid,
+ "violations" to result.violations,
+ "validatedAt" to result.validatedAt.toString()
+ )
+ ))
+
+ return result
+ }
+
+ /**
+ * AST๋ฅผ ์ต์ ํํฉ๋๋ค.
+ */
+ fun optimize(level: OptimizationLevel = OptimizationLevel.FULL): ASTOptimizationResult {
+ val originalSize = getSize()
+ val originalDepth = getDepth()
+ val originalRoot = this.root
+
+ // ์ต์ ํ ์ํ
+ val optimizedRoot = when (level) {
+ OptimizationLevel.NONE -> this.root
+ OptimizationLevel.BASIC -> performBasicOptimization(this.root)
+ OptimizationLevel.FULL -> optimizer.optimize(this.root)
+ }
+
+ // ๋ฃจํธ ์
๋ฐ์ดํธ
+ this.root = optimizedRoot
+ this.optimizationLevel = level
+ this.lastModifiedAt = LocalDateTime.now()
+ this.isValidated = false
+ this.validationResult = null
+
+ val optimizedSize = getSize()
+ val optimizedDepth = getDepth()
+
+ val result = ASTOptimizationResult(
+ originalSize = originalSize,
+ optimizedSize = optimizedSize,
+ originalDepth = originalDepth,
+ optimizedDepth = optimizedDepth,
+ level = level,
+ optimizedAt = LocalDateTime.now(),
+ astId = id
+ )
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐ์
+ addDomainEvent(mapOf(
+ "eventType" to DomainEvents.AST_OPTIMIZED,
+ "aggregateId" to id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "originalRoot" to originalRoot.toString(),
+ "optimizedRoot" to optimizedRoot.toString(),
+ "originalSize" to originalSize.value,
+ "optimizedSize" to optimizedSize.value,
+ "level" to level.name,
+ "optimizedAt" to LocalDateTime.now().toString()
+ )
+ ))
+
+ return result
+ }
+
+ /**
+ * ๊ธฐ๋ณธ ์ต์ ํ๋ฅผ ์ํํฉ๋๋ค.
+ *
+ * ๋ชจ๋ ์์ ๋
ธ๋๋ฅผ ์ฌ๊ท์ ์ผ๋ก ์ต์ ํํ ํ ํ์ฌ ๋
ธ๋์ ์ต์ ํ๋ฅผ ์ํํฉ๋๋ค.
+ */
+ private fun performBasicOptimization(node: ASTNode): ASTNode {
+ // ๋จผ์ ๋ชจ๋ ์์ ๋
ธ๋๋ฅผ ์ฌ๊ท์ ์ผ๋ก ์ต์ ํ
+ val optimizedNode = when (node) {
+ is BinaryOpNode -> BinaryOpNode(
+ left = performBasicOptimization(node.left),
+ operator = node.operator,
+ right = performBasicOptimization(node.right)
+ )
+ is UnaryOpNode -> UnaryOpNode(
+ operator = node.operator,
+ operand = performBasicOptimization(node.operand)
+ )
+ is FunctionCallNode -> FunctionCallNode(
+ name = node.name,
+ args = node.args.map { performBasicOptimization(it) }
+ )
+ is IfNode -> IfNode(
+ condition = performBasicOptimization(node.condition),
+ trueValue = performBasicOptimization(node.trueValue),
+ falseValue = performBasicOptimization(node.falseValue)
+ )
+ is ArgumentsNode -> ArgumentsNode(
+ arguments = node.arguments.map { performBasicOptimization(it) }
+ )
+ // ๋ฆฌํ ๋
ธ๋๋ค์ ๊ทธ๋๋ก ๋ฐํ
+ is NumberNode, is BooleanNode, is VariableNode -> node
+ else -> node
+ }
+
+ // ์์ ๋
ธ๋ ์ต์ ํ ํ ํ์ฌ ๋
ธ๋์ ์ต์ ํ ์ํ
+ return when (optimizedNode) {
+ is IfNode -> optimizedNode.optimize()
+ is UnaryOpNode -> optimizedNode.simplify()
+ is BinaryOpNode -> optimizedNode.simplify()
+ else -> optimizedNode
+ }
+ }
+
+ /**
+ * ํน์ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ */
+ fun findNode(condition: (ASTNode) -> Boolean): ASTNode? {
+ return traverser.depthFirstSearch(root, condition)
+ }
+
+ /**
+ * ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๋ชจ๋ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ */
+ fun findAllNodes(condition: (ASTNode) -> Boolean): List {
+ return traverser.findAll(root, condition)
+ }
+
+ /**
+ * ํน์ ํ์
์ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ */
+ fun findNodesByType(nodeClass: Class): List {
+ return traverser.findByType(root, nodeClass)
+ }
+
+ /**
+ * ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ */
+ fun accept(visitor: ASTVisitor) {
+ traverser.preOrderTraversal(root, visitor)
+ }
+
+ /**
+ * ํธ๋ฆฌ ํต๊ณ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun getStatistics(): TreeStatistics {
+ val stats = traverser.calculateStatistics(root)
+ return TreeStatistics(
+ nodeCount = stats.nodeCount,
+ leafCount = stats.leafCount,
+ maxDepth = stats.maxDepth,
+ averageDepth = stats.averageDepth,
+ nodeTypeCounts = stats.nodeTypeCounts,
+ variables = getVariables(),
+ astId = id,
+ calculatedAt = LocalDateTime.now()
+ )
+ }
+
+ /**
+ * ์๋ธํธ๋ฆฌ๋ฅผ ๊ต์ฒดํฉ๋๋ค.
+ */
+ fun replaceSubtree(target: ASTNode, replacement: ASTNode) {
+ if (!validitySpec.isSatisfiedBy(replacement)) {
+ val reason = validitySpec.getWhyNotSatisfied(replacement)
+ throw ASTException.invalidReplacementNode(reason, replacement.toString())
+ }
+
+ val newRoot = replaceSubtreeHelper(root, target, replacement)
+
+ if (newRoot != root) {
+ setRoot(newRoot)
+ addDomainEvent(mapOf(
+ "eventType" to DomainEvents.SUBTREE_REPLACED,
+ "aggregateId" to id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "target" to target.toString(),
+ "replacement" to replacement.toString(),
+ "replacedAt" to LocalDateTime.now().toString()
+ )
+ ))
+ }
+ }
+
+ /**
+ * ์๋ธํธ๋ฆฌ ๊ต์ฒด ํฌํผ ํจ์
+ */
+ private fun replaceSubtreeHelper(current: ASTNode, target: ASTNode, replacement: ASTNode): ASTNode {
+ if (current == target) {
+ return replacement
+ }
+
+ return when (current) {
+ is BinaryOpNode -> {
+ val newLeft = replaceSubtreeHelper(current.left, target, replacement)
+ val newRight = replaceSubtreeHelper(current.right, target, replacement)
+ if (newLeft != current.left || newRight != current.right) {
+ factory.createBinaryOp(newLeft, current.operator, newRight)
+ } else {
+ current
+ }
+ }
+ is UnaryOpNode -> {
+ val newOperand = replaceSubtreeHelper(current.operand, target, replacement)
+ if (newOperand != current.operand) {
+ factory.createUnaryOp(current.operator, newOperand)
+ } else {
+ current
+ }
+ }
+ is FunctionCallNode -> {
+ val newArgs = current.args.map { replaceSubtreeHelper(it, target, replacement) }
+ if (newArgs != current.args) {
+ factory.createFunctionCall(current.name, newArgs)
+ } else {
+ current
+ }
+ }
+ is IfNode -> {
+ val newCondition = replaceSubtreeHelper(current.condition, target, replacement)
+ val newTrueValue = replaceSubtreeHelper(current.trueValue, target, replacement)
+ val newFalseValue = replaceSubtreeHelper(current.falseValue, target, replacement)
+ if (newCondition != current.condition || newTrueValue != current.trueValue || newFalseValue != current.falseValue) {
+ factory.createIf(newCondition, newTrueValue, newFalseValue)
+ } else {
+ current
+ }
+ }
+ is ArgumentsNode -> {
+ val newArgs = current.arguments.map { replaceSubtreeHelper(it, target, replacement) }
+ if (newArgs != current.arguments) {
+ factory.createArguments(newArgs)
+ } else {
+ current
+ }
+ }
+ else -> current
+ }
+ }
+
+ /**
+ * ๋๋ฉ์ธ ์ด๋ฒคํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+ */
+ private fun addDomainEvent(event: Any) {
+ domainEvents.add(event)
+ }
+
+ /**
+ * ๋๋ฉ์ธ ์ด๋ฒคํธ๋ค์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getDomainEvents(): List = domainEvents.toList()
+
+ /**
+ * ๋๋ฉ์ธ ์ด๋ฒคํธ๋ค์ ํด๋ฆฌ์ดํฉ๋๋ค.
+ */
+ fun clearDomainEvents() {
+ domainEvents.clear()
+ }
+
+ /**
+ * ๋ณต์ฌ๋ณธ์ ์์ฑํฉ๋๋ค.
+ */
+ fun copy(): ExpressionAST {
+ return ExpressionAST(
+ id = UUID.randomUUID().toString(),
+ root = root,
+ createdAt = LocalDateTime.now(),
+ lastModifiedAt = LocalDateTime.now(),
+ optimizationLevel = OptimizationLevel.NONE,
+ isValidated = false,
+ validationResult = null
+ )
+ }
+
+ /**
+ * ๋ฌธ์์ด ํํ์ ๋ฐํํฉ๋๋ค.
+ */
+ override fun toString(): String {
+ return "ExpressionAST(id='$id', size=${getSize().value}, depth=${getDepth().value})"
+ }
+
+ /**
+ * ๊ฐ์ ๊ฐ์ฒด์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ExpressionAST) return false
+ return id == other.id
+ }
+
+ /**
+ * ํด์ ์ฝ๋๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+
+ companion object {
+ /**
+ * ์๋ก์ด ExpressionAST๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ fun create(root: ASTNode): ExpressionAST {
+ val ast = ExpressionAST(
+ id = UUID.randomUUID().toString(),
+ root = root,
+ createdAt = LocalDateTime.now(),
+ lastModifiedAt = LocalDateTime.now(),
+ optimizationLevel = OptimizationLevel.NONE,
+ isValidated = false,
+ validationResult = null
+ )
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐ์
+ ast.addDomainEvent(mapOf(
+ "eventType" to DomainEvents.AST_CREATED,
+ "aggregateId" to ast.id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "root" to root.toString(),
+ "createdAt" to LocalDateTime.now().toString()
+ )
+ ))
+
+ return ast
+ }
+
+ /**
+ * ID๋ก ExpressionAST๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ fun createWithId(id: String, root: ASTNode): ExpressionAST {
+ val ast = ExpressionAST(
+ id = id,
+ root = root,
+ createdAt = LocalDateTime.now(),
+ lastModifiedAt = LocalDateTime.now(),
+ optimizationLevel = OptimizationLevel.NONE,
+ isValidated = false,
+ validationResult = null
+ )
+
+ // ๋๋ฉ์ธ ์ด๋ฒคํธ ๋ฐ์
+ ast.addDomainEvent(mapOf(
+ "eventType" to DomainEvents.AST_CREATED,
+ "aggregateId" to id,
+ "aggregateType" to DomainEvents.EXPRESSION_AST,
+ "payload" to mapOf(
+ "root" to root.toString(),
+ "createdAt" to LocalDateTime.now().toString()
+ )
+ ))
+
+ return ast
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt
new file mode 100644
index 00000000..934bb275
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt
@@ -0,0 +1,197 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.global.interfaces.EntityMarker
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ์ถ์ ๊ตฌ๋ฌธ ํธ๋ฆฌ(AST)์ ๋ชจ๋ ๋
ธ๋์ ๋ํ ๊ธฐ๋ณธ sealed ํด๋์ค์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์ ๋ชจ๋ ๊ตฌ๋ฌธ ์์๋ฅผ ๋ํ๋ด๋ AST ๋
ธ๋๋ค์ ๊ธฐ๋ณธ ์ธํฐํ์ด์ค๋ก,
+ * Visitor ํจํด์ ์ง์ํ์ฌ ๋ค์ํ ์ฐ์ฐ(ํ๊ฐ, ์ถ๋ ฅ, ๋ณํ ๋ฑ)์ ์ํํ ์ ์์ต๋๋ค.
+ * ๋ชจ๋ AST ๋
ธ๋๋ ์ด ํด๋์ค๋ฅผ ์์๋ฐ์์ผ ํ๋ฉฐ, ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋ฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+sealed class ASTNode : EntityMarker {
+
+ private val id: String = java.util.UUID.randomUUID().toString()
+
+ override fun getDomainContext(): String = AST
+
+ override fun getIdentifier(): String = id
+
+ /**
+ * ์ด AST ๋
ธ๋์ ํฌํจ๋ ๋ชจ๋ ๋ณ์ ์ด๋ฆ์ ๋ฐํํฉ๋๋ค.
+ *
+ * ํํ์์์ ์ฌ์ฉ๋๋ ๋ณ์๋ค์ ์ถ์ถํ์ฌ ๋ณ์ ์์กด์ฑ ๋ถ์์ด๋
+ * ๋ณ์ ๊ฐ ๊ฒ์ฆ์ ํ์ฉํ ์ ์์ต๋๋ค.
+ *
+ * @return ๋ณ์ ์ด๋ฆ์ ์งํฉ
+ */
+ abstract fun getVariables(): Set
+
+ /**
+ * AST ๋
ธ๋์ ํ์
์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋
ธ๋ ํ์
๋ฌธ์์ด
+ */
+ fun getNodeType(): String = this::class.simpleName ?: UNKNOWN_NODE
+
+ /**
+ * AST ๋
ธ๋๊ฐ ๋ฆฌํฐ๋ด ๊ฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ฆฌํฐ๋ด์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isLiteral(): Boolean = false
+
+ /**
+ * AST ๋
ธ๋๊ฐ ๋ฆฌํ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ * ๋ฆฌํ ๋
ธ๋๋ ์์ ๋
ธ๋๊ฐ ์๋ ๋
ธ๋์
๋๋ค.
+ *
+ * @return ๋ฆฌํ ๋
ธ๋์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isLeaf(): Boolean = getChildren().isEmpty()
+
+ /**
+ * ์ด ๋
ธ๋์ ๋ชจ๋ ์์ ๋
ธ๋๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์์ ๋
ธ๋๋ค์ ๋ฆฌ์คํธ
+ */
+ abstract fun getChildren(): List
+
+ /**
+ * AST ๋
ธ๋๊ฐ ๋ณ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ณ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isVariable(): Boolean = false
+
+ /**
+ * AST ๋
ธ๋๊ฐ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isOperator(): Boolean = false
+
+ /**
+ * AST ๋
ธ๋๊ฐ ํจ์ ํธ์ถ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ํจ์ ํธ์ถ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isFunctionCall(): Boolean = false
+
+ /**
+ * AST ๋
ธ๋๊ฐ ์กฐ๊ฑด๋ฌธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์กฐ๊ฑด๋ฌธ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ open fun isConditional(): Boolean = false
+
+ /**
+ * AST ๋
ธ๋์ ๊น์ด๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @return ๋
ธ๋์ ์ต๋ ๊น์ด
+ */
+ abstract fun getDepth(): Int
+
+ /**
+ * AST ๋
ธ๋์ ์ด ๋
ธ๋ ๊ฐ์๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @return ํ์ ๋
ธ๋๋ฅผ ํฌํจํ ์ด ๋
ธ๋ ๊ฐ์
+ */
+ abstract fun getNodeCount(): Int
+
+ /**
+ * AST ๋
ธ๋์ ํฌ๊ธฐ(์ด ๋
ธ๋ ๊ฐ์)๋ฅผ ๋ฐํํฉ๋๋ค.
+ * getNodeCount()์ ๋์ผํ์ง๋ง ๋ค๋ฅธ ์ปจํ
์คํธ์์ ์ฌ์ฉ๋ฉ๋๋ค.
+ *
+ * @return ํ์ ๋
ธ๋๋ฅผ ํฌํจํ ์ด ๋
ธ๋ ๊ฐ์
+ */
+ fun getSize(): Int = getNodeCount()
+
+ /**
+ * AST ๋
ธ๋๋ฅผ ๋ณต์ ํฉ๋๋ค.
+ *
+ * @return ๋ณต์ ๋ AST ๋
ธ๋
+ */
+ abstract fun copy(): ASTNode
+
+ /**
+ * AST ๋
ธ๋์ ๊ตฌ์กฐ๋ฅผ ํธ๋ฆฌ ํํ๋ก ์ถ๋ ฅํฉ๋๋ค.
+ *
+ * @param indent ๋ค์ฌ์ฐ๊ธฐ ๋ ๋ฒจ
+ * @return ํธ๋ฆฌ ๊ตฌ์กฐ ๋ฌธ์์ด
+ */
+ open fun toTreeString(indent: Int = 0): String {
+ val spaces = " ".repeat(indent)
+ return "$spaces${getNodeType()}: $this"
+ }
+
+ /**
+ * AST ๋
ธ๋๋ฅผ ๊ดํธ ์์ด ๊ฐ๋จํ ํํ๋ก ์ถ๋ ฅํฉ๋๋ค.
+ *
+ * @return ๊ฐ๋จํ ๋ฌธ์์ด ํํ
+ */
+ abstract fun toSimpleString(): String
+
+ /**
+ * Visitor ํจํด์ accept ๋ฉ์๋๋ฅผ ๊ตฌํํฉ๋๋ค.
+ *
+ * @param visitor ๋ฐฉ๋ฌธ์ ๊ฐ์ฒด
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ abstract fun accept(visitor: ASTVisitor): T
+
+ /**
+ * ๋ AST ๋
ธ๋๊ฐ ๊ตฌ์กฐ์ ์ผ๋ก ๋์ผํ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ AST ๋
ธ๋
+ * @return ๊ตฌ์กฐ์ ์ผ๋ก ๋์ผํ๋ฉด true, ์๋๋ฉด false
+ */
+ abstract fun isStructurallyEqual(other: ASTNode): Boolean
+
+ /**
+ * AST ๋
ธ๋์ ์ ํจ์ฑ์ ๊ฒ์ฆํฉ๋๋ค.
+ * ๊ธฐ๋ณธ ๊ตฌํ์์๋ true๋ฅผ ๋ฐํํ๋ฉฐ, ํ์์ ๋ฐ๋ผ ํ์ ํด๋์ค์์ ์ฌ์ ์ํ ์ ์์ต๋๋ค.
+ *
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ open fun validate(): Boolean = true
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ์ ์ค์ฒฉ ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ * ๊ธฐ๋ณธ๊ฐ์ 0์ด๋ฉฐ, ์กฐ๊ฑด๋ฌธ ๋
ธ๋์์ ์ฌ์ ์๋ฉ๋๋ค.
+ *
+ * @return ์ค์ฒฉ ๊น์ด
+ */
+ open fun getNestingDepth(): Int = 0
+
+ companion object {
+ /**
+ * AST ๋
ธ๋ ํ์
์ ํ์ธํ๋ ์ ํธ๋ฆฌํฐ ๋ฉ์๋์
๋๋ค.
+ *
+ * @param node ํ์ธํ ๋
ธ๋
+ * @return ๋
ธ๋ ํ์
์ ๋ณด
+ */
+ fun getNodeInfo(node: ASTNode): Map = mapOf(
+ "type" to node.getNodeType(),
+ "isLiteral" to node.isLiteral(),
+ "isVariable" to node.isVariable(),
+ "isOperator" to node.isOperator(),
+ "isFunctionCall" to node.isFunctionCall(),
+ "isConditional" to node.isConditional(),
+ "depth" to node.getDepth(),
+ "nodeCount" to node.getNodeCount(),
+ "variables" to node.getVariables()
+ )
+
+ const val AST = "ast"
+ const val UNKNOWN_NODE = "UnknownNode"
+ }
+}
+
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt
new file mode 100644
index 00000000..6fbff0a1
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt
@@ -0,0 +1,351 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ํจ์ ์ธ์ ๋ชฉ๋ก์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ํ์ฑ ๊ณผ์ ์์ ํจ์ ์ธ์๋ค์ ๊ทธ๋ฃนํํ๊ณ ๊ด๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ฉฐ,
+ * ์ต์ข
์ ์ผ๋ก๋ FunctionCallNode์ ํตํฉ๋ฉ๋๋ค.
+ *
+ * @property arguments ์ธ์ ๋ชฉ๋ก
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class ArgumentsNode(
+ val arguments: List
+) : ASTNode() {
+
+ init {
+ if (arguments.size > MAX_ARGUMENTS) {
+ throw ASTException.argumentCountExceeded()
+ }
+ }
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitArguments(this)
+
+ override fun getVariables(): Set = arguments.flatMap { it.getVariables() }.toSet()
+
+ override fun getChildren(): List = arguments
+
+ override fun getDepth(): Int = 1 + (arguments.maxOfOrNull { it.getDepth() } ?: 0)
+
+ override fun getNodeCount(): Int = 1 + arguments.sumOf { it.getNodeCount() }
+
+ override fun copy(): ASTNode = ArgumentsNode(arguments.map { it.copy() })
+
+ override fun toSimpleString(): String = arguments.joinToString(", ") { it.toSimpleString() }
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean {
+ return other is ArgumentsNode &&
+ this.arguments.size == other.arguments.size &&
+ this.arguments.zip(other.arguments).all { (thisArg, otherArg) ->
+ thisArg.isStructurallyEqual(otherArg)
+ }
+ }
+
+ /**
+ * ์ธ์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ธ์ ๊ฐ์
+ */
+ fun getArgumentCount(): Int = arguments.size
+
+ /**
+ * ์ธ์๊ฐ ๋น์ด์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ธ์๊ฐ ๋น์ด์์ผ๋ฉด true
+ */
+ fun isEmpty(): Boolean = arguments.isEmpty()
+
+ /**
+ * ์ธ์๊ฐ ํ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ธ์๊ฐ ํ๋์ด๋ฉด true
+ */
+ fun isSingle(): Boolean = arguments.size == 1
+
+ /**
+ * ์ธ์๊ฐ ์ฌ๋ฌ ๊ฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ธ์๊ฐ ์ฌ๋ฌ ๊ฐ์ด๋ฉด true
+ */
+ fun isMultiple(): Boolean = arguments.size > 1
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param index ์ธ์ ์ธ๋ฑ์ค
+ * @return ํด๋น ์ธ๋ฑ์ค์ ์ธ์, ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด null
+ */
+ fun getArgument(index: Int): ASTNode? {
+ return if (index in 0 until arguments.size) arguments[index] else null
+ }
+
+ /**
+ * ์ฒซ ๋ฒ์งธ ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฒซ ๋ฒ์งธ ์ธ์, ์์ผ๋ฉด null
+ */
+ fun getFirst(): ASTNode? = arguments.firstOrNull()
+
+ /**
+ * ๋ง์ง๋ง ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ง์ง๋ง ์ธ์, ์์ผ๋ฉด null
+ */
+ fun getLast(): ASTNode? = arguments.lastOrNull()
+
+ /**
+ * ์ธ์๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+ *
+ * @param argument ์ถ๊ฐํ ์ธ์
+ * @return ์๋ก์ด ArgumentsNode
+ */
+ fun addArgument(argument: ASTNode): ArgumentsNode {
+ return ArgumentsNode(arguments + argument)
+ }
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ์ธ์๋ฅผ ์ฝ์
ํฉ๋๋ค.
+ *
+ * @param index ์ฝ์
ํ ์ธ๋ฑ์ค
+ * @param argument ์ฝ์
ํ ์ธ์
+ * @return ์๋ก์ด ArgumentsNode
+ */
+ fun insertArgument(index: Int, argument: ASTNode): ArgumentsNode {
+ if (index !in 0..arguments.size) {
+ throw ASTException.indexOutOfRange()
+ }
+ val newArguments = arguments.toMutableList()
+ newArguments.add(index, argument)
+ return ArgumentsNode(newArguments)
+ }
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ์ธ์๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
+ *
+ * @param index ์ ๊ฑฐํ ์ธ์์ ์ธ๋ฑ์ค
+ * @return ์๋ก์ด ArgumentsNode
+ */
+ fun removeArgument(index: Int): ArgumentsNode {
+ if (index !in 0..arguments.size) {
+ throw ASTException.indexOutOfRange()
+ }
+ return ArgumentsNode(arguments.filterIndexed { i, _ -> i != index })
+ }
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ์ธ์๋ฅผ ๊ต์ฒดํฉ๋๋ค.
+ *
+ * @param index ๊ต์ฒดํ ์ธ์์ ์ธ๋ฑ์ค
+ * @param newArgument ์๋ก์ด ์ธ์
+ * @return ์๋ก์ด ArgumentsNode
+ */
+ fun replaceArgument(index: Int, newArgument: ASTNode): ArgumentsNode {
+ if (index !in 0..arguments.size) {
+ throw ASTException.indexOutOfRange()
+ }
+ val newArguments = arguments.toMutableList()
+ newArguments[index] = newArgument
+ return ArgumentsNode(newArguments)
+ }
+
+ /**
+ * ๋ชจ๋ ์ธ์๊ฐ ๋ฆฌํฐ๋ด์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ชจ๋ ์ธ์๊ฐ ๋ฆฌํฐ๋ด์ด๋ฉด true
+ */
+ fun areAllLiterals(): Boolean = arguments.all { it.isLiteral() }
+
+ /**
+ * ๋ชจ๋ ์ธ์๊ฐ ์ซ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ชจ๋ ์ธ์๊ฐ ์ซ์์ด๋ฉด true
+ */
+ fun areAllNumbers(): Boolean = arguments.all { it is NumberNode }
+
+ /**
+ * ๋ชจ๋ ์ธ์๊ฐ ๋ถ๋ฆฌ์ธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ชจ๋ ์ธ์๊ฐ ๋ถ๋ฆฌ์ธ์ด๋ฉด true
+ */
+ fun areAllBooleans(): Boolean = arguments.all { it is BooleanNode }
+
+ /**
+ * ๋ชจ๋ ์ธ์๊ฐ ๋ณ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ชจ๋ ์ธ์๊ฐ ๋ณ์์ด๋ฉด true
+ */
+ fun areAllVariables(): Boolean = arguments.all { it is VariableNode }
+
+ /**
+ * ํน์ ํ์
์ ์ธ์๋ง ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param nodeType ํ์ธํ ๋
ธ๋ ํ์
+ * @return ํด๋น ํ์
์ ์ธ์๋ง ์์ผ๋ฉด true
+ */
+ fun hasOnlyType(nodeType: String): Boolean = arguments.all { it.getNodeType() == nodeType }
+
+ /**
+ * ํน์ ํ์
์ ์ธ์๊ฐ ํฌํจ๋์ด ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param nodeType ํ์ธํ ๋
ธ๋ ํ์
+ * @return ํด๋น ํ์
์ ์ธ์๊ฐ ์์ผ๋ฉด true
+ */
+ fun hasType(nodeType: String): Boolean = arguments.any { it.getNodeType() == nodeType }
+
+ /**
+ * ์ธ์ ํ์
๋ถํฌ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ธ์ ํ์
๋ณ ๊ฐ์
+ */
+ fun getTypeDistribution(): Map {
+ return arguments.groupingBy { it.getNodeType() }.eachCount()
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก์ ๋ฆฌ์คํธ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ธ์ ๋ฆฌ์คํธ
+ */
+ fun toList(): List = arguments.toList()
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก์ ์ญ์์ผ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ญ์ ArgumentsNode
+ */
+ fun reverse(): ArgumentsNode = ArgumentsNode(arguments.reversed())
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก์ ์ ๋ ฌํฉ๋๋ค.
+ *
+ * @param comparator ๋น๊ต ํจ์
+ * @return ์ ๋ ฌ๋ ArgumentsNode
+ */
+ fun sort(comparator: Comparator): ArgumentsNode {
+ return ArgumentsNode(arguments.sortedWith(comparator))
+ }
+
+ /**
+ * ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ธ์๋ค์ ํํฐ๋งํฉ๋๋ค.
+ *
+ * @param predicate ์กฐ๊ฑด ํจ์
+ * @return ํํฐ๋ง๋ ArgumentsNode
+ */
+ fun filter(predicate: (ASTNode) -> Boolean): ArgumentsNode {
+ return ArgumentsNode(arguments.filter(predicate))
+ }
+
+ /**
+ * ์ธ์๋ค์ ๋ณํํฉ๋๋ค.
+ *
+ * @param transform ๋ณํ ํจ์
+ * @return ๋ณํ๋ ArgumentsNode
+ */
+ fun map(transform: (ASTNode) -> ASTNode): ArgumentsNode {
+ return ArgumentsNode(arguments.map(transform))
+ }
+
+ /**
+ * ์ธ์ ๋
ธ๋์ ์์ธ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ธ์ ์ ๋ณด ๋งต
+ */
+ fun getArgumentsInfo(): Map {
+ return mapOf(
+ "count" to getArgumentCount(),
+ "isEmpty" to isEmpty(),
+ "isSingle" to isSingle(),
+ "isMultiple" to isMultiple(),
+ "areAllLiterals" to areAllLiterals(),
+ "areAllNumbers" to areAllNumbers(),
+ "areAllBooleans" to areAllBooleans(),
+ "areAllVariables" to areAllVariables(),
+ "typeDistribution" to getTypeDistribution(),
+ "argumentTypes" to arguments.map { it.getNodeType() },
+ "variables" to getVariables(),
+ "totalSize" to getNodeCount(),
+ "maxDepth" to (arguments.maxOfOrNull { it.getDepth() } ?: 0)
+ )
+ }
+
+ override fun toString(): String = arguments.joinToString(", ", "[", "]")
+
+ companion object {
+ private const val MAX_ARGUMENTS = 100
+
+ /**
+ * ๋น ์ธ์ ๋ชฉ๋ก์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๋น ArgumentsNode
+ */
+ fun empty(): ArgumentsNode = ArgumentsNode(emptyList())
+
+ /**
+ * ๋จ์ผ ์ธ์๋ก ArgumentsNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param argument ์ธ์
+ * @return ArgumentsNode ์ธ์คํด์ค
+ */
+ fun single(argument: ASTNode): ArgumentsNode = ArgumentsNode(listOf(argument))
+
+ /**
+ * ์ฌ๋ฌ ์ธ์๋ก ArgumentsNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param arguments ์ธ์๋ค
+ * @return ArgumentsNode ์ธ์คํด์ค
+ */
+ fun of(vararg arguments: ASTNode): ArgumentsNode = ArgumentsNode(arguments.toList())
+
+ /**
+ * ๋ฆฌ์คํธ๋ก๋ถํฐ ArgumentsNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param arguments ์ธ์ ๋ฆฌ์คํธ
+ * @return ArgumentsNode ์ธ์คํด์ค
+ */
+ fun from(arguments: List): ArgumentsNode = ArgumentsNode(arguments)
+
+ /**
+ * ์ธ์ ๋
ธ๋ ๋ชฉ๋ก์ ํต๊ณ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param nodes ArgumentsNode ๋ชฉ๋ก
+ * @return ํต๊ณ ์ ๋ณด
+ */
+ fun calculateStatistics(nodes: List): Map {
+ if (nodes.isEmpty()) {
+ return mapOf(
+ "count" to 0,
+ "isEmpty" to true
+ )
+ }
+
+ val argumentCounts = nodes.map { it.getArgumentCount() }
+ val averageArgCount = argumentCounts.average()
+ val maxArgCount = argumentCounts.maxOrNull() ?: 0
+ val minArgCount = argumentCounts.minOrNull() ?: 0
+ val emptyCount = nodes.count { it.isEmpty() }
+ val singleCount = nodes.count { it.isSingle() }
+ val multipleCount = nodes.count { it.isMultiple() }
+
+ return mapOf(
+ "count" to nodes.size,
+ "averageArgCount" to averageArgCount,
+ "maxArgCount" to maxArgCount,
+ "minArgCount" to minArgCount,
+ "emptyCount" to emptyCount,
+ "singleCount" to singleCount,
+ "multipleCount" to multipleCount,
+ "isEmpty" to false,
+ "argCountRange" to (maxArgCount - minArgCount)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt
new file mode 100644
index 00000000..bffb6ea1
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt
@@ -0,0 +1,443 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+import kotlin.math.pow
+
+/**
+ * ์ดํญ ์ฐ์ฐ(์: ๋ง์
, ๋บ์
, ๋น๊ต)์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ์ดํญ ์ฐ์ฐ์๋ฅผ ํํํ๋ฉฐ, ์ข์ธก ํผ์ฐ์ฐ์,
+ * ์ฐ์ฐ์, ์ฐ์ธก ํผ์ฐ์ฐ์๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ์ฐ์ ์ฐ์ฐ, ๋น๊ต ์ฐ์ฐ, ๋
ผ๋ฆฌ ์ฐ์ฐ ๋ฑ์
+ * ๋ชจ๋ ์ง์ํ๋ฉฐ, ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋์ด ์์ ํ ์ฐ์ฐ ํธ๋ฆฌ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
+ *
+ * @property left ์ข์ธก ํผ์ฐ์ฐ์ AST ๋
ธ๋
+ * @property operator ์ฐ์ฐ์ ๋ฌธ์์ด (์: "+", "-", "==")
+ * @property right ์ฐ์ธก ํผ์ฐ์ฐ์ AST ๋
ธ๋
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class BinaryOpNode(
+ val left: ASTNode,
+ val operator: String,
+ val right: ASTNode
+) : ASTNode() {
+
+ init {
+ if (operator.isBlank()) {
+ throw ASTException.operatorEmpty()
+ }
+
+ if (!isSupportedOperator(operator)) {
+ throw ASTException.unsupportedOperator()
+ }
+ }
+
+ override fun getVariables(): Set = left.getVariables() + right.getVariables()
+
+ override fun getChildren(): List = listOf(left, right)
+
+ override fun isOperator(): Boolean = true
+
+ override fun getDepth(): Int = maxOf(left.getDepth(), right.getDepth()) + 1
+
+ override fun getNodeCount(): Int = left.getNodeCount() + right.getNodeCount() + 1
+
+ override fun copy(): BinaryOpNode = BinaryOpNode(left.copy(), operator, right.copy())
+
+ override fun toSimpleString(): String = "($left $operator $right)"
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitBinaryOp(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean {
+ if (other !is BinaryOpNode || this.operator != other.operator) return false
+
+ // ๊ธฐ๋ณธ์ ์ธ ์์๋๋ก ๋น๊ต
+ val standardMatch = this.left.isStructurallyEqual(other.left) &&
+ this.right.isStructurallyEqual(other.right)
+
+ // ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ ์ฐ์ฐ์์ ๊ฒฝ์ฐ ์์๋ฅผ ๋ฐ๊ฟ์๋ ๋น๊ต
+ val commutativeMatch = if (isCommutativeOperator(operator)) {
+ this.left.isStructurallyEqual(other.right) &&
+ this.right.isStructurallyEqual(other.left)
+ } else false
+
+ return standardMatch || commutativeMatch
+ }
+
+ /**
+ * ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param op ํ์ธํ ์ฐ์ฐ์
+ * @return ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ฉด true
+ */
+ private fun isCommutativeOperator(op: String): Boolean = when (op) {
+ "+", "*", "==", "!=", "&&", "||" -> true
+ else -> false
+ }
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ง์๋๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param op ํ์ธํ ์ฐ์ฐ์
+ * @return ์ง์๋๋ฉด true, ์๋๋ฉด false
+ */
+ private fun isSupportedOperator(op: String): Boolean = op in SUPPORTED_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ฐ์ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐ์ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isArithmeticOperator(): Boolean = operator in ARITHMETIC_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๋น๊ต ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋น๊ต ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isComparisonOperator(): Boolean = operator in COMPARISON_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๋
ผ๋ฆฌ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋
ผ๋ฆฌ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isLogicalOperator(): Boolean = operator in LOGICAL_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isCommutative(): Boolean = operator in COMMUTATIVE_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๊ฒฐํฉ๋ฒ์น์ด ์ฑ๋ฆฝํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ฒฐํฉ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isAssociative(): Boolean = operator in ASSOCIATIVE_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์์ ์ฐ์ ์์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฐ์ ์์ (๋์์๋ก ๋จผ์ ๊ณ์ฐ)
+ */
+ fun getPrecedence(): Int = OPERATOR_PRECEDENCE[operator] ?: 0
+
+ /**
+ * ์ฐ์ฐ์์ ๊ฒฐํฉ์ฑ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๊ฒฐํฉ์ฑ (LEFT ๋๋ RIGHT)
+ */
+ fun getAssociativity(): Associativity = OPERATOR_ASSOCIATIVITY[operator] ?: Associativity.LEFT
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ข๊ฒฐํฉ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ข๊ฒฐํฉ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isLeftAssociative(): Boolean = getAssociativity() == Associativity.LEFT
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ฐ๊ฒฐํฉ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐ๊ฒฐํฉ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isRightAssociative(): Boolean = getAssociativity() == Associativity.RIGHT
+
+ /**
+ * ์ฐ์ฐ์์ ํ์
์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฐ์ฐ์ ํ์
+ */
+ fun getOperatorType(): OperatorType = when {
+ isArithmeticOperator() -> OperatorType.ARITHMETIC
+ isComparisonOperator() -> OperatorType.COMPARISON
+ isLogicalOperator() -> OperatorType.LOGICAL
+ else -> OperatorType.UNKNOWN
+ }
+
+ /**
+ * ์ข์ธก ํผ์ฐ์ฐ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด BinaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newLeft ์๋ก์ด ์ข์ธก ํผ์ฐ์ฐ์
+ * @return ์๋ก์ด BinaryOpNode
+ */
+ fun withLeft(newLeft: ASTNode): BinaryOpNode = BinaryOpNode(newLeft, operator, right)
+
+ /**
+ * ์ฐ์ธก ํผ์ฐ์ฐ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด BinaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newRight ์๋ก์ด ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return ์๋ก์ด BinaryOpNode
+ */
+ fun withRight(newRight: ASTNode): BinaryOpNode = BinaryOpNode(left, operator, newRight)
+
+ /**
+ * ์ฐ์ฐ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด BinaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newOperator ์๋ก์ด ์ฐ์ฐ์
+ * @return ์๋ก์ด BinaryOpNode
+ */
+ fun withOperator(newOperator: String): BinaryOpNode = BinaryOpNode(left, newOperator, right)
+
+ /**
+ * ๊ตํ๋ฒ์น์ ์ ์ฉํ ์๋ก์ด BinaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํผ์ฐ์ฐ์๊ฐ ๊ตํ๋ ์๋ก์ด BinaryOpNode
+ * @throws IllegalStateException ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ์ง ์๋ ์ฐ์ฐ์์ธ ๊ฒฝ์ฐ
+ */
+ fun commute(): BinaryOpNode {
+ if (!isCommutative()) {
+ throw ASTException.operatorNotCommutative(operator)
+ }
+ return BinaryOpNode(right, operator, left)
+ }
+
+
+ /**
+ * BinaryOpNode๋ฅผ ๋จ์ํํฉ๋๋ค.
+ *
+ * ๋ค์๊ณผ ๊ฐ์ ์ต์ ํ๋ฅผ ์ํํฉ๋๋ค:
+ * - ์ฐ์ ์ฐ์ฐ: 0๊ณผ์ ๋ง์
/๋บ์
, 1๊ณผ์ ๊ณฑ์
/๋๋์
, 0๊ณผ์ ๊ณฑ์
+ * - ๋
ผ๋ฆฌ ์ฐ์ฐ: true/false์์ ๋
ผ๋ฆฌ ์ฐ์ฐ
+ * - ์์ ๊ณ์ฐ: ๋ ์์์ ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฏธ๋ฆฌ ๊ณ์ฐ
+ * - ๋์ผํ ํผ์ฐ์ฐ์์ ์ฐ์ฐ ์ฒ๋ฆฌ
+ *
+ * @return ๋จ์ํ๋ AST ๋
ธ๋
+ */
+ fun simplify(): ASTNode {
+ // ์์ชฝ ํผ์ฐ์ฐ์๊ฐ NumberNode์ธ ๊ฒฝ์ฐ ์์ ๊ณ์ฐ
+ if (left is NumberNode && right is NumberNode) {
+ return when (operator) {
+ "+" -> NumberNode(left.value + right.value)
+ "-" -> NumberNode(left.value - right.value)
+ "*" -> NumberNode(left.value * right.value)
+ "/" -> {
+ if (right.isZero()) return this // 0์ผ๋ก ๋๋๊ธฐ๋ ๋จ์ํํ์ง ์์
+ NumberNode(left.value / right.value)
+ }
+ "%" -> {
+ if (right.isZero()) return this
+ NumberNode(left.value % right.value)
+ }
+ "^" -> NumberNode(left.value.pow(right.value))
+ "==" -> NumberNode(if (left.value == right.value) 1.0 else 0.0)
+ "!=" -> NumberNode(if (left.value != right.value) 1.0 else 0.0)
+ "<" -> NumberNode(if (left.value < right.value) 1.0 else 0.0)
+ "<=" -> NumberNode(if (left.value <= right.value) 1.0 else 0.0)
+ ">" -> NumberNode(if (left.value > right.value) 1.0 else 0.0)
+ ">=" -> NumberNode(if (left.value >= right.value) 1.0 else 0.0)
+ "&&" -> NumberNode(if (!left.isZero() && !right.isZero()) 1.0 else 0.0)
+ "||" -> NumberNode(if (!left.isZero() || !right.isZero()) 1.0 else 0.0)
+ else -> this
+ }
+ }
+
+ // ์ฐ์ ์ฐ์ฐ ์ต์ ํ
+ if (left is NumberNode || right is NumberNode) {
+ when (operator) {
+ "+" -> {
+ if (left is NumberNode && left.isZero()) return right
+ if (right is NumberNode && right.isZero()) return left
+ }
+ "-" -> {
+ if (right is NumberNode && right.isZero()) return left
+ if (left is NumberNode && left.isZero()) return UnaryOpNode("-", right)
+ }
+ "*" -> {
+ if ((left is NumberNode && left.isZero()) || (right is NumberNode && right.isZero())) {
+ return NumberNode.ZERO
+ }
+ if (left is NumberNode && left.value == 1.0) return right
+ if (right is NumberNode && right.value == 1.0) return left
+ if (left is NumberNode && left.value == -1.0) return UnaryOpNode("-", right)
+ if (right is NumberNode && right.value == -1.0) return UnaryOpNode("-", left)
+ }
+ "/" -> {
+ if (left is NumberNode && left.isZero()) return NumberNode.ZERO
+ if (right is NumberNode && right.value == 1.0) return left
+ if (right is NumberNode && right.value == -1.0) return UnaryOpNode("-", left)
+ }
+ "^" -> {
+ if (right is NumberNode && right.isZero()) return NumberNode.ONE
+ if (right is NumberNode && right.value == 1.0) return left
+ if (left is NumberNode && left.value == 1.0) return NumberNode.ONE
+ if (left is NumberNode && left.isZero()) return NumberNode.ZERO
+ }
+ }
+ }
+
+ // ๋
ผ๋ฆฌ ์ฐ์ฐ ์ต์ ํ
+ when (operator) {
+ "&&" -> {
+ if (left is NumberNode && left.isZero()) return NumberNode.ZERO
+ if (right is NumberNode && right.isZero()) return NumberNode.ZERO
+ if (left is NumberNode && !left.isZero()) return right
+ if (right is NumberNode && !right.isZero()) return left
+ }
+ "||" -> {
+ if (left is NumberNode && !left.isZero()) return NumberNode.ONE
+ if (right is NumberNode && !right.isZero()) return NumberNode.ONE
+ if (left is NumberNode && left.isZero()) return right
+ if (right is NumberNode && right.isZero()) return left
+ }
+ }
+
+ // ๋์ผํ ํผ์ฐ์ฐ์ ์ฒ๋ฆฌ
+ if (left.isStructurallyEqual(right)) {
+ when (operator) {
+ "-" -> return NumberNode.ZERO
+ "/" -> return NumberNode.ONE
+ "%" -> return NumberNode.ZERO
+ "==" -> return NumberNode.ONE
+ "!=" -> return NumberNode.ZERO
+ "<=" -> return NumberNode.ONE
+ ">=" -> return NumberNode.ONE
+ "<" -> return NumberNode.ZERO
+ ">" -> return NumberNode.ZERO
+ }
+ }
+
+ return this
+ }
+
+ /**
+ * ๊ดํธ ์์ด ์ฐ์ฐ์ ์ฐ์ ์์์ ๋ฐ๋ผ ๋ฌธ์์ด์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ์ฐ์ ์์๋ฅผ ๊ณ ๋ คํ ๋ฌธ์์ด
+ */
+ fun toStringWithPrecedence(): String {
+ val leftStr = if (left is BinaryOpNode && left.getPrecedence() < getPrecedence()) {
+ "(${left.toStringWithPrecedence()})"
+ } else {
+ left.toSimpleString()
+ }
+
+ val rightStr = if (right is BinaryOpNode &&
+ (right.getPrecedence() < getPrecedence() ||
+ (right.getPrecedence() == getPrecedence() && getAssociativity() == Associativity.LEFT))) {
+ "(${right.toStringWithPrecedence()})"
+ } else {
+ right.toSimpleString()
+ }
+
+ return "$leftStr $operator $rightStr"
+ }
+
+ override fun toString(): String = toStringWithPrecedence()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return buildString {
+ appendLine("${spaces}BinaryOpNode: $operator")
+ appendLine("${spaces} left:")
+ appendLine(left.toTreeString(indent + 2))
+ appendLine("${spaces} right:")
+ append(right.toTreeString(indent + 2))
+ }
+ }
+
+ /**
+ * ์ฐ์ฐ์ ๊ฒฐํฉ์ฑ์ ๋ํ๋ด๋ ์ด๊ฑฐํ์
๋๋ค.
+ */
+ enum class Associativity {
+ LEFT, RIGHT
+ }
+
+ /**
+ * ์ฐ์ฐ์ ํ์
์ ๋ํ๋ด๋ ์ด๊ฑฐํ์
๋๋ค.
+ */
+ enum class OperatorType {
+ ARITHMETIC, COMPARISON, LOGICAL, UNKNOWN
+ }
+
+ companion object {
+ /**
+ * ์ง์๋๋ ๋ชจ๋ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val SUPPORTED_OPERATORS = setOf(
+ "+", "-", "*", "/", "^", "%",
+ "==", "!=", "<", "<=", ">", ">=",
+ "&&", "||"
+ )
+
+ /**
+ * ์ฐ์ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val ARITHMETIC_OPERATORS = setOf("+", "-", "*", "/", "^", "%")
+
+ /**
+ * ๋น๊ต ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val COMPARISON_OPERATORS = setOf("==", "!=", "<", "<=", ">", ">=")
+
+ /**
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val LOGICAL_OPERATORS = setOf("&&", "||")
+
+ /**
+ * ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val COMMUTATIVE_OPERATORS = setOf("+", "*", "==", "!=", "&&", "||")
+
+ /**
+ * ๊ฒฐํฉ๋ฒ์น์ด ์ฑ๋ฆฝํ๋ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val ASSOCIATIVE_OPERATORS = setOf("+", "*", "&&", "||")
+
+ /**
+ * ์ฐ์ฐ์ ์ฐ์ ์์ ๋งต์
๋๋ค.
+ */
+ private val OPERATOR_PRECEDENCE = mapOf(
+ "||" to 1,
+ "&&" to 2,
+ "==" to 3, "!=" to 3,
+ "<" to 4, "<=" to 4, ">" to 4, ">=" to 4,
+ "+" to 5, "-" to 5,
+ "*" to 6, "/" to 6, "%" to 6,
+ "^" to 7
+ )
+
+ /**
+ * ์ฐ์ฐ์ ๊ฒฐํฉ์ฑ ๋งต์
๋๋ค.
+ */
+ private val OPERATOR_ASSOCIATIVITY = mapOf(
+ "||" to Associativity.LEFT,
+ "&&" to Associativity.LEFT,
+ "==" to Associativity.LEFT, "!=" to Associativity.LEFT,
+ "<" to Associativity.LEFT, "<=" to Associativity.LEFT, ">" to Associativity.LEFT, ">=" to Associativity.LEFT,
+ "+" to Associativity.LEFT, "-" to Associativity.LEFT,
+ "*" to Associativity.LEFT, "/" to Associativity.LEFT, "%" to Associativity.LEFT,
+ "^" to Associativity.RIGHT
+ )
+
+ /**
+ * ์ง์๋๋ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ง์๋๋ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getSupportedOperators(): Set = SUPPORTED_OPERATORS.toSet()
+
+ /**
+ * ํน์ ํ์
์ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @param type ์ฐ์ฐ์ ํ์
+ * @return ํด๋น ํ์
์ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getOperatorsByType(type: OperatorType): Set = when (type) {
+ OperatorType.ARITHMETIC -> ARITHMETIC_OPERATORS
+ OperatorType.COMPARISON -> COMPARISON_OPERATORS
+ OperatorType.LOGICAL -> LOGICAL_OPERATORS
+ OperatorType.UNKNOWN -> emptySet()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt
new file mode 100644
index 00000000..8825f35b
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt
@@ -0,0 +1,180 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ๋ถ๋ฆฐ ๊ฐ(true, false)์ ํํํ๋ฉฐ,
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ๊ณผ ์กฐ๊ฑด๋ฌธ์์ ์ฌ์ฉ๋ฉ๋๋ค. ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋์ด
+ * ์์ ํ ๊ฐ ์ ๋ฌ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @property value ๋
ธ๋์ ๋ถ๋ฆฐ ๊ฐ (true ๋๋ false)
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class BooleanNode(val value: Boolean) : ASTNode() {
+
+ override fun getVariables(): Set = emptySet()
+
+ override fun getChildren(): List = emptyList()
+
+ override fun isLiteral(): Boolean = true
+
+ override fun getDepth(): Int = 1
+
+ override fun getNodeCount(): Int = 1
+
+ override fun copy(): BooleanNode = this
+
+ override fun toSimpleString(): String = value.toString()
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitBoolean(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is BooleanNode && this.value == other.value
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ด ์ฐธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐธ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isTrue(): Boolean = value
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ด ๊ฑฐ์ง์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ฑฐ์ง์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isFalse(): Boolean = !value
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ ๋ฐ์ ์ํต๋๋ค.
+ *
+ * @return ๋ฐ์ ๋ ๊ฐ์ ๊ฐ์ง ์๋ก์ด BooleanNode
+ */
+ fun not(): BooleanNode = BooleanNode(!value)
+
+ /**
+ * ๋ค๋ฅธ BooleanNode์ ๋
ผ๋ฆฌ๊ณฑ(AND) ์ฐ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param other AND ์ฐ์ฐํ BooleanNode
+ * @return AND ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ง ์๋ก์ด BooleanNode
+ */
+ infix fun and(other: BooleanNode): BooleanNode = BooleanNode(value && other.value)
+
+ /**
+ * ๋ค๋ฅธ BooleanNode์ ๋
ผ๋ฆฌํฉ(OR) ์ฐ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param other OR ์ฐ์ฐํ BooleanNode
+ * @return OR ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ง ์๋ก์ด BooleanNode
+ */
+ infix fun or(other: BooleanNode): BooleanNode = BooleanNode(value || other.value)
+
+ /**
+ * ๋ค๋ฅธ BooleanNode์ ๋ฐฐํ์ ๋
ผ๋ฆฌํฉ(XOR) ์ฐ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param other XOR ์ฐ์ฐํ BooleanNode
+ * @return XOR ์ฐ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ง ์๋ก์ด BooleanNode
+ */
+ infix fun xor(other: BooleanNode): BooleanNode = BooleanNode(value xor other.value)
+
+ /**
+ * ๋ค๋ฅธ BooleanNode์ ๋์น์ฑ์ ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ BooleanNode
+ * @return ๊ฐ์ผ๋ฉด true, ๋ค๋ฅด๋ฉด false
+ */
+ fun isEqualTo(other: BooleanNode): Boolean = value == other.value
+
+ /**
+ * ๋ค๋ฅธ BooleanNode์ ๋น๋์น์ฑ์ ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ BooleanNode
+ * @return ๋ค๋ฅด๋ฉด true, ๊ฐ์ผ๋ฉด false
+ */
+ fun isNotEqualTo(other: BooleanNode): Boolean = value != other.value
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ ์ ์๋ก ๋ณํํฉ๋๋ค.
+ *
+ * @return true๋ฉด 1, false๋ฉด 0
+ */
+ fun toInt(): Int = if (value) 1 else 0
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ Double๋ก ๋ณํํฉ๋๋ค.
+ *
+ * @return true๋ฉด 1.0, false๋ฉด 0.0
+ */
+ fun toDouble(): Double = if (value) 1.0 else 0.0
+
+ /**
+ * ๋ถ๋ฆฐ ๊ฐ์ ์ซ์ ๋
ธ๋๋ก ๋ณํํฉ๋๋ค.
+ *
+ * @return true๋ฉด NumberNode(1.0), false๋ฉด NumberNode(0.0)
+ */
+ fun toNumberNode(): NumberNode = NumberNode(toDouble())
+
+ override fun toString(): String = value.toString()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return "${spaces}BooleanNode: $value"
+ }
+
+ companion object {
+ /**
+ * TRUE๋ฅผ ๋ํ๋ด๋ BooleanNode์
๋๋ค.
+ */
+ val TRUE = BooleanNode(true)
+
+ /**
+ * FALSE๋ฅผ ๋ํ๋ด๋ BooleanNode์
๋๋ค.
+ */
+ val FALSE = BooleanNode(false)
+
+ /**
+ * Boolean ๊ฐ์ผ๋ก BooleanNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value Boolean ๊ฐ
+ * @return BooleanNode ์ธ์คํด์ค
+ */
+ fun of(value: Boolean): BooleanNode = if (value) TRUE else FALSE
+
+ /**
+ * ๋ฌธ์์ด๋ก๋ถํฐ BooleanNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๋ถ๋ฆฐ ๋ฌธ์์ด ("true", "false", ๋์๋ฌธ์ ๋ฌด๊ด)
+ * @return BooleanNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์๋ชป๋ ๋ถ๋ฆฐ ๋ฌธ์์ด์ธ ๊ฒฝ์ฐ
+ */
+ fun parse(value: String): BooleanNode = when (value.lowercase()) {
+ "true" -> TRUE
+ "false" -> FALSE
+ else -> throw ASTException.invalidBooleanValue(value)
+ }
+
+ /**
+ * ์ซ์๋ก๋ถํฐ BooleanNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๊ฐ (0์ด๋ฉด false, ๊ทธ ์ธ๋ true)
+ * @return BooleanNode ์ธ์คํด์ค
+ */
+ fun fromNumber(value: Double): BooleanNode = of(value != 0.0)
+
+ /**
+ * NumberNode๋ก๋ถํฐ BooleanNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param numberNode NumberNode (0์ด๋ฉด false, ๊ทธ ์ธ๋ true)
+ * @return BooleanNode ์ธ์คํด์ค
+ */
+ fun fromNumberNode(numberNode: NumberNode): BooleanNode = of(!numberNode.isZero())
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt
new file mode 100644
index 00000000..f2f01599
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt
@@ -0,0 +1,343 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ํจ์ ํธ์ถ์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ํจ์ ํธ์ถ์ ํํํ๋ฉฐ, ํจ์๋ช
๊ณผ ์ธ์ ๋ชฉ๋ก์ผ๋ก
+ * ๊ตฌ์ฑ๋ฉ๋๋ค. ์ํ ํจ์(sin, cos, sqrt ๋ฑ)์ ์ฌ์ฉ์ ์ ์ ํจ์๋ฅผ ๋ชจ๋ ์ง์ํ๋ฉฐ,
+ * ๊ฐ๋ณ ์ธ์์ ์ ํ์ ์ธ์๋ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋ฉ๋๋ค.
+ *
+ * @property name ํธ์ถํ ํจ์์ ์ด๋ฆ
+ * @property args ํจ์์ ์ ๋ฌ๋ ์ธ์ ๋ชฉ๋ก (AST ๋
ธ๋)
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class FunctionCallNode(
+ val name: String,
+ val args: List
+) : ASTNode() {
+
+ init {
+ if (name.isBlank()) {
+ throw ASTException.functionNameEmpty()
+ }
+ if (!isValidFunctionName(name)) {
+ throw ASTException.invalidFunctionName(name)
+ }
+ if (args.size > MAX_ARGUMENTS) {
+ throw ASTException.argumentCountExceeded()
+ }
+ }
+
+ override fun getVariables(): Set = args.flatMap { it.getVariables() }.toSet()
+
+ override fun getChildren(): List = args
+
+ override fun isFunctionCall(): Boolean = true
+
+ override fun getDepth(): Int = (args.maxOfOrNull { it.getDepth() } ?: 0) + 1
+
+ override fun getNodeCount(): Int = args.sumOf { it.getNodeCount() } + 1
+
+ override fun copy(): FunctionCallNode = FunctionCallNode(name, args.map { it.copy() })
+
+ override fun toSimpleString(): String = "$name(${args.joinToString(", ") { it.toSimpleString() }})"
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitFunctionCall(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is FunctionCallNode &&
+ this.name == other.name &&
+ this.args.size == other.args.size &&
+ this.args.zip(other.args).all { (a, b) -> a.isStructurallyEqual(b) }
+
+ /**
+ * ํจ์๋ช
์ด ์ ํจํ ์๋ณ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param functionName ํ์ธํ ํจ์๋ช
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ private fun isValidFunctionName(functionName: String): Boolean {
+ if (functionName.isEmpty()) return false
+
+ // ์ฒซ ๋ฌธ์๋ ์๋ฌธ์ ๋๋ ๋ฐ์ค์ด์ด์ผ ํจ
+ if (!functionName.first().isLetter() && functionName.first() != '_') return false
+
+ // ๋๋จธ์ง ๋ฌธ์๋ ์๋ฌธ์, ์ซ์, ๋ฐ์ค์ด์ด์ผ ํจ
+ return functionName.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ /**
+ * ์ธ์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ธ์ ๊ฐ์
+ */
+ fun getArgumentCount(): Int = args.size
+
+ /**
+ * ์ธ์๊ฐ ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ธ์๊ฐ ์์ผ๋ฉด true, ์์ผ๋ฉด false
+ */
+ fun hasNoArguments(): Boolean = args.isEmpty()
+
+ /**
+ * ๋จ์ผ ์ธ์๋ฅผ ๊ฐ์ง๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋จ์ผ ์ธ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasSingleArgument(): Boolean = args.size == 1
+
+ /**
+ * ๋ค์ค ์ธ์๋ฅผ ๊ฐ์ง๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ค์ค ์ธ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasMultipleArguments(): Boolean = args.size > 1
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param index ์ธ์ ์ธ๋ฑ์ค
+ * @return ํด๋น ์ธ๋ฑ์ค์ ์ธ์
+ * @throws IndexOutOfBoundsException ์ธ๋ฑ์ค๊ฐ ๋ฒ์๋ฅผ ๋ฒ์ด๋ ๊ฒฝ์ฐ
+ */
+ fun getArgument(index: Int): ASTNode {
+ if (index !in args.indices) {
+ throw ASTException.indexOutOfRange()
+ }
+ return args[index]
+ }
+
+ /**
+ * ์ฒซ ๋ฒ์งธ ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฒซ ๋ฒ์งธ ์ธ์
+ * @throws IllegalStateException ์ธ์๊ฐ ์๋ ๊ฒฝ์ฐ
+ */
+ fun getFirstArgument(): ASTNode {
+ if (args.isEmpty()) {
+ throw ASTException.argumentsEmpty()
+ }
+ return args[0]
+ }
+
+ /**
+ * ๋ง์ง๋ง ์ธ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ง์ง๋ง ์ธ์
+ * @throws IllegalStateException ์ธ์๊ฐ ์๋ ๊ฒฝ์ฐ
+ */
+ fun getLastArgument(): ASTNode {
+ if (args.isEmpty()) {
+ throw ASTException.argumentsEmpty()
+ }
+ return args.last()
+ }
+
+ /**
+ * ํจ์๊ฐ ์ํ ํจ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ํ ํจ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isMathFunction(): Boolean = name.lowercase() in MATH_FUNCTIONS
+
+ /**
+ * ํจ์๊ฐ ์ง๊ณ ํจ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ง๊ณ ํจ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isAggregateFunction(): Boolean = name.lowercase() in AGGREGATE_FUNCTIONS
+
+ /**
+ * ํจ์๊ฐ ๋ฌธ์์ด ํจ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ฌธ์์ด ํจ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isStringFunction(): Boolean = name.lowercase() in STRING_FUNCTIONS
+
+ /**
+ * ํจ์๊ฐ ์ฌ์ฉ์ ์ ์ ํจ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฌ์ฉ์ ์ ์ ํจ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isUserDefinedFunction(): Boolean = !isMathFunction() && !isAggregateFunction() && !isStringFunction()
+
+ /**
+ * ํจ์์ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํจ์ ์นดํ
๊ณ ๋ฆฌ
+ */
+ fun getFunctionCategory(): FunctionCategory = when {
+ isMathFunction() -> FunctionCategory.MATH
+ isAggregateFunction() -> FunctionCategory.AGGREGATE
+ isStringFunction() -> FunctionCategory.STRING
+ else -> FunctionCategory.USER_DEFINED
+ }
+
+ /**
+ * ํน์ ์ธ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด FunctionCallNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param index ๊ต์ฒดํ ์ธ์์ ์ธ๋ฑ์ค
+ * @param newArgument ์๋ก์ด ์ธ์
+ * @return ์๋ก์ด FunctionCallNode
+ */
+ fun withArgument(index: Int, newArgument: ASTNode): FunctionCallNode {
+ if (index !in args.indices) {
+ throw ASTException.indexOutOfRange()
+ }
+ val newArgs = args.toMutableList()
+ newArgs[index] = newArgument
+ return FunctionCallNode(name, newArgs)
+ }
+
+ /**
+ * ์ธ์๋ฅผ ์ถ๊ฐํ ์๋ก์ด FunctionCallNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newArgument ์ถ๊ฐํ ์ธ์
+ * @return ์๋ก์ด FunctionCallNode
+ */
+ fun withAddedArgument(newArgument: ASTNode): FunctionCallNode {
+ if (args.size > MAX_ARGUMENTS) {
+ throw ASTException.argumentCountExceeded()
+ }
+ return FunctionCallNode(name, args + newArgument)
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก์ ๊ต์ฒดํ ์๋ก์ด FunctionCallNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newArgs ์๋ก์ด ์ธ์ ๋ชฉ๋ก
+ * @return ์๋ก์ด FunctionCallNode
+ */
+ fun withArguments(newArgs: List): FunctionCallNode = FunctionCallNode(name, newArgs)
+
+ /**
+ * ํจ์๋ช
์ ๊ต์ฒดํ ์๋ก์ด FunctionCallNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newName ์๋ก์ด ํจ์๋ช
+ * @return ์๋ก์ด FunctionCallNode
+ */
+ fun withName(newName: String): FunctionCallNode = FunctionCallNode(newName, args)
+
+ override fun toString(): String = toSimpleString()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return buildString {
+ appendLine("${spaces}FunctionCallNode: $name")
+ if (args.isNotEmpty()) {
+ appendLine("${spaces} arguments:")
+ args.forEachIndexed { index, arg ->
+ appendLine("${spaces} [$index]:")
+ if (index < args.size - 1) {
+ appendLine(arg.toTreeString(indent + 3))
+ } else {
+ append(arg.toTreeString(indent + 3))
+ }
+ }
+ } else {
+ append("${spaces} arguments: (none)")
+ }
+ }
+ }
+
+ /**
+ * ํจ์ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ๋ํ๋ด๋ ์ด๊ฑฐํ์
๋๋ค.
+ */
+ enum class FunctionCategory {
+ MATH, AGGREGATE, STRING, USER_DEFINED
+ }
+
+ companion object {
+ /**
+ * ์ต๋ ์ธ์ ๊ฐ์์
๋๋ค.
+ */
+ const val MAX_ARGUMENTS = 10
+
+ /**
+ * ์ํ ํจ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val MATH_FUNCTIONS = setOf(
+ "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
+ "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
+ "exp", "log", "log10", "log2", "ln",
+ "sqrt", "cbrt", "pow", "abs", "sign",
+ "floor", "ceil", "round", "trunc",
+ "min", "max", "clamp"
+ )
+
+ /**
+ * ์ง๊ณ ํจ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val AGGREGATE_FUNCTIONS = setOf(
+ "sum", "avg", "mean", "median", "mode",
+ "count", "distinct", "variance", "stddev"
+ )
+
+ /**
+ * ๋ฌธ์์ด ํจ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val STRING_FUNCTIONS = setOf(
+ "length", "upper", "lower", "trim", "substring",
+ "replace", "contains", "startswith", "endswith"
+ )
+
+ /**
+ * ์ง์๋๋ ๋ชจ๋ ํจ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ง์๋๋ ํจ์ ์งํฉ
+ */
+ fun getSupportedFunctions(): Set =
+ MATH_FUNCTIONS + AGGREGATE_FUNCTIONS + STRING_FUNCTIONS
+
+ /**
+ * ํน์ ์นดํ
๊ณ ๋ฆฌ์ ํจ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @param category ํจ์ ์นดํ
๊ณ ๋ฆฌ
+ * @return ํด๋น ์นดํ
๊ณ ๋ฆฌ์ ํจ์ ์งํฉ
+ */
+ fun getFunctionsByCategory(category: FunctionCategory): Set = when (category) {
+ FunctionCategory.MATH -> MATH_FUNCTIONS
+ FunctionCategory.AGGREGATE -> AGGREGATE_FUNCTIONS
+ FunctionCategory.STRING -> STRING_FUNCTIONS
+ FunctionCategory.USER_DEFINED -> emptySet()
+ }
+
+ /**
+ * ์ธ์ ์๋ ํจ์ ํธ์ถ์ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @return FunctionCallNode ์ธ์คํด์ค
+ */
+ fun withoutArguments(name: String): FunctionCallNode = FunctionCallNode(name, emptyList())
+
+ /**
+ * ๋จ์ผ ์ธ์ ํจ์ ํธ์ถ์ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param argument ์ธ์
+ * @return FunctionCallNode ์ธ์คํด์ค
+ */
+ fun withSingleArgument(name: String, argument: ASTNode): FunctionCallNode =
+ FunctionCallNode(name, listOf(argument))
+
+ /**
+ * ๋ค์ค ์ธ์ ํจ์ ํธ์ถ์ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param arguments ์ธ์๋ค
+ * @return FunctionCallNode ์ธ์คํด์ค
+ */
+ fun withArguments(name: String, vararg arguments: ASTNode): FunctionCallNode =
+ FunctionCallNode(name, arguments.toList())
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt
new file mode 100644
index 00000000..546f1db2
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt
@@ -0,0 +1,340 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ์กฐ๊ฑด๋ฌธ (IF)์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ์ผํญ ์กฐ๊ฑด ์ฐ์ฐ์๋ฅผ ํํํ๋ฉฐ, ์กฐ๊ฑด์, ์ฐธ ๊ฐ, ๊ฑฐ์ง ๊ฐ์ผ๋ก
+ * ๊ตฌ์ฑ๋ฉ๋๋ค. IF(condition, trueValue, falseValue) ํํ๋ก ์ฌ์ฉ๋๋ฉฐ,
+ * ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฐ์ ๋ฐํํ๋ ์กฐ๊ฑด๋ถ ํํ์์ ๊ตฌํํฉ๋๋ค.
+ *
+ * @property condition ์กฐ๊ฑด์ AST ๋
ธ๋
+ * @property trueValue ์กฐ๊ฑด์ด ์ฐธ์ผ ๋ ํ๊ฐ๋ AST ๋
ธ๋
+ * @property falseValue ์กฐ๊ฑด์ด ๊ฑฐ์ง์ผ ๋ ํ๊ฐ๋ AST ๋
ธ๋
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class IfNode(
+ val condition: ASTNode,
+ val trueValue: ASTNode,
+ val falseValue: ASTNode
+) : ASTNode() {
+
+ override fun getVariables(): Set =
+ condition.getVariables() + trueValue.getVariables() + falseValue.getVariables()
+
+ override fun getChildren(): List = listOf(condition, trueValue, falseValue)
+
+ override fun isConditional(): Boolean = true
+
+ override fun getDepth(): Int = maxOf(condition.getDepth(), trueValue.getDepth(), falseValue.getDepth()) + 1
+
+ override fun getNodeCount(): Int = condition.getNodeCount() + trueValue.getNodeCount() + falseValue.getNodeCount() + 1
+
+ override fun copy(): IfNode = IfNode(condition.copy(), trueValue.copy(), falseValue.copy())
+
+ override fun toSimpleString(): String = "$IF(${condition.toSimpleString()}, ${trueValue.toSimpleString()}, ${falseValue.toSimpleString()})"
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitIf(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is IfNode &&
+ this.condition.isStructurallyEqual(other.condition) &&
+ this.trueValue.isStructurallyEqual(other.trueValue) &&
+ this.falseValue.isStructurallyEqual(other.falseValue)
+
+ /**
+ * ์กฐ๊ฑด์์ด ์์ ๊ฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์กฐ๊ฑด์์ด ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasConstantCondition(): Boolean = condition.isLiteral()
+
+ /**
+ * ์กฐ๊ฑด์์ด ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasBooleanCondition(): Boolean = condition is BooleanNode
+
+ /**
+ * ์กฐ๊ฑด์์ด ์ซ์ ๋ฆฌํฐ๋ด์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ซ์ ๋ฆฌํฐ๋ด์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasNumberCondition(): Boolean = condition is NumberNode
+
+ /**
+ * ์ฐธ ๊ฐ๊ณผ ๊ฑฐ์ง ๊ฐ์ด ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ฐ์ผ๋ฉด true, ๋ค๋ฅด๋ฉด false
+ */
+ fun hasSameValues(): Boolean = trueValue.isStructurallyEqual(falseValue)
+
+ /**
+ * ์ฐธ ๊ฐ์ด ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐธ ๊ฐ์ด ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasConstantTrueValue(): Boolean = trueValue.isLiteral()
+
+ /**
+ * ๊ฑฐ์ง ๊ฐ์ด ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ฑฐ์ง ๊ฐ์ด ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasConstantFalseValue(): Boolean = falseValue.isLiteral()
+
+ /**
+ * ๋ชจ๋ ๊ฐ์ด ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋ชจ๋ ๊ฐ์ด ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasAllConstantValues(): Boolean = hasConstantCondition() && hasConstantTrueValue() && hasConstantFalseValue()
+
+ /**
+ * ์ค์ฒฉ๋ IF ๋ฌธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ค์ฒฉ๋ IF ๋ฌธ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isNestedIf(): Boolean = trueValue is IfNode || falseValue is IfNode
+
+ /**
+ * ์ฐธ ๊ฐ์ด IF ๋ฌธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐธ ๊ฐ์ด IF ๋ฌธ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasTrueValueAsIf(): Boolean = trueValue is IfNode
+
+ /**
+ * ๊ฑฐ์ง ๊ฐ์ด IF ๋ฌธ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๊ฑฐ์ง ๊ฐ์ด IF ๋ฌธ์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasFalseValueAsIf(): Boolean = falseValue is IfNode
+
+ /**
+ * ์กฐ๊ฑด์์ ๊ต์ฒดํ ์๋ก์ด IfNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newCondition ์๋ก์ด ์กฐ๊ฑด์
+ * @return ์๋ก์ด IfNode
+ */
+ fun withCondition(newCondition: ASTNode): IfNode = IfNode(newCondition, trueValue, falseValue)
+
+ /**
+ * ์ฐธ ๊ฐ์ ๊ต์ฒดํ ์๋ก์ด IfNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newTrueValue ์๋ก์ด ์ฐธ ๊ฐ
+ * @return ์๋ก์ด IfNode
+ */
+ fun withTrueValue(newTrueValue: ASTNode): IfNode = IfNode(condition, newTrueValue, falseValue)
+
+ /**
+ * ๊ฑฐ์ง ๊ฐ์ ๊ต์ฒดํ ์๋ก์ด IfNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newFalseValue ์๋ก์ด ๊ฑฐ์ง ๊ฐ
+ * @return ์๋ก์ด IfNode
+ */
+ fun withFalseValue(newFalseValue: ASTNode): IfNode = IfNode(condition, trueValue, newFalseValue)
+
+ /**
+ * ๋ชจ๋ ๊ตฌ์ฑ ์์๋ฅผ ๊ต์ฒดํ ์๋ก์ด IfNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newCondition ์๋ก์ด ์กฐ๊ฑด์
+ * @param newTrueValue ์๋ก์ด ์ฐธ ๊ฐ
+ * @param newFalseValue ์๋ก์ด ๊ฑฐ์ง ๊ฐ
+ * @return ์๋ก์ด IfNode
+ */
+ fun withAll(newCondition: ASTNode, newTrueValue: ASTNode, newFalseValue: ASTNode): IfNode =
+ IfNode(newCondition, newTrueValue, newFalseValue)
+
+ /**
+ * ๋จ์ํํ ์ ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ ๊ฐ๋ฅํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun canSimplify(): Boolean = when {
+ hasSameValues() -> true
+ hasBooleanCondition() -> true
+ hasNumberCondition() && (condition as NumberNode).let { it.isZero() || !it.isZero() } -> true
+ else -> false
+ }
+
+ /**
+ * IF ๋
ธ๋๋ฅผ ๋จ์ํํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ๋ AST ๋
ธ๋
+ * @throws IllegalStateException ๋จ์ํํ ์ ์๋ ๊ฒฝ์ฐ
+ */
+ fun simplify(): ASTNode {
+ if (!canSimplify()) {
+ throw ASTException.ifNotSimplifiable()
+ }
+ return when {
+ hasSameValues() -> trueValue
+ hasBooleanCondition() -> {
+ val boolCondition = condition as BooleanNode
+ if (boolCondition.value) trueValue else falseValue
+ }
+ hasNumberCondition() -> {
+ val numCondition = condition as NumberNode
+ if (!numCondition.isZero()) trueValue else falseValue
+ }
+ else -> throw ASTException.simplificationUnexpectedCase()
+ }
+ }
+
+ /**
+ * IF ๋
ธ๋๋ฅผ ์ต์ ํํฉ๋๋ค.
+ *
+ * @return ์ต์ ํ๋ AST ๋
ธ๋
+ */
+ fun optimize(): ASTNode {
+ return when {
+ canSimplify() -> simplify()
+ else -> this
+ }
+ }
+
+ /**
+ * ์กฐ๊ฑด์ ๋ฐ์ ์ํจ ์๋ก์ด IfNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์กฐ๊ฑด์ด ๋ฐ์ ๋๊ณ ์ฐธ/๊ฑฐ์ง ๊ฐ์ด ๋ฐ๋ ์๋ก์ด IfNode
+ */
+ fun negate(): IfNode {
+ val negatedCondition = when (condition) {
+ is BooleanNode -> condition.not()
+ is UnaryOpNode -> if (condition.isLogicalNot()) condition.operand else UnaryOpNode.logicalNot(condition)
+ else -> UnaryOpNode.logicalNot(condition)
+ }
+ return IfNode(negatedCondition, falseValue, trueValue)
+ }
+
+ /**
+ * ์ค์ฒฉ ๊น์ด๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @return ์ค์ฒฉ IF ๋ฌธ์ ์ต๋ ๊น์ด
+ */
+ override fun getNestingDepth(): Int {
+ val trueDepth = if (trueValue is IfNode) trueValue.getNestingDepth() + 1 else 1
+ val falseDepth = if (falseValue is IfNode) falseValue.getNestingDepth() + 1 else 1
+ return maxOf(trueDepth, falseDepth)
+ }
+
+ /**
+ * ์ผํญ ์ฐ์ฐ์ ํํ์ ๋ฌธ์์ด์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return "condition ? trueValue : falseValue" ํํ์ ๋ฌธ์์ด
+ */
+ fun toTernaryString(): String =
+ "${condition.toSimpleString()} ? ${trueValue.toSimpleString()} : ${falseValue.toSimpleString()}"
+
+ override fun toString(): String = toSimpleString()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return buildString {
+ appendLine("${spaces}$IF_NODE")
+ appendLine("${spaces} $CONDITION")
+ appendLine(condition.toTreeString(indent + 2))
+ appendLine("${spaces} $TRUE_VALUE")
+ appendLine(trueValue.toTreeString(indent + 2))
+ appendLine("${spaces} $FALSE_VALUE")
+ append(falseValue.toTreeString(indent + 2))
+ }
+ }
+
+ companion object {
+
+ const val IF = "IF"
+ const val IF_NODE = "IfNode:"
+ const val CONDITION = "condition:"
+ const val TRUE_VALUE = "trueValue:"
+ const val FALSE_VALUE = "falseValue:"
+ const val LOGICAL_AND = "&&"
+
+ /**
+ * ๋ถ๋ฆฐ ์กฐ๊ฑด์ผ๋ก IF ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param condition ๋ถ๋ฆฐ ์กฐ๊ฑด
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ * @return IfNode ์ธ์คํด์ค
+ */
+ fun withBooleanCondition(condition: Boolean, trueValue: ASTNode, falseValue: ASTNode): IfNode =
+ IfNode(BooleanNode.of(condition), trueValue, falseValue)
+
+ /**
+ * ์ซ์ ์กฐ๊ฑด์ผ๋ก IF ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param condition ์ซ์ ์กฐ๊ฑด (0์ด๋ฉด ๊ฑฐ์ง, ๊ทธ ์ธ๋ ์ฐธ)
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ * @return IfNode ์ธ์คํด์ค
+ */
+ fun withNumberCondition(condition: Double, trueValue: ASTNode, falseValue: ASTNode): IfNode =
+ IfNode(NumberNode.of(condition), trueValue, falseValue)
+
+ /**
+ * ๋จ์ ๋ถ๋ฆฐ ๋ถ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param condition ์กฐ๊ฑด์
+ * @param trueValue ์ฐธ์ผ ๋ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง์ผ ๋ ๊ฐ
+ * @return IfNode ์ธ์คํด์ค
+ */
+ fun createBranch(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode =
+ IfNode(condition, trueValue, falseValue)
+
+ /**
+ * ์ค์ฒฉ IF ๋ฌธ์ ํ๋ฉดํํฉ๋๋ค.
+ *
+ * @param ifNode ํ๋ฉดํํ IF ๋
ธ๋
+ * @return ํ๋ฉดํ๋ IF ์กฐ๊ฑด๋ค์ ๋ฆฌ์คํธ
+ */
+ fun flatten(ifNode: IfNode): List> {
+ val result = mutableListOf>()
+
+ fun collect(node: IfNode, currentConditions: List) {
+ val trueConditions = currentConditions + node.condition
+ val falseConditions = currentConditions + UnaryOpNode.logicalNot(node.condition)
+
+ when (node.trueValue) {
+ is IfNode -> collect(node.trueValue, trueConditions)
+ else -> result.add(createCompoundCondition(trueConditions) to node.trueValue)
+ }
+
+ when (node.falseValue) {
+ is IfNode -> collect(node.falseValue, falseConditions)
+ else -> result.add(createCompoundCondition(falseConditions) to node.falseValue)
+ }
+ }
+
+ collect(ifNode, emptyList())
+ return result
+ }
+
+ /**
+ * ์กฐ๊ฑด ๋ฆฌ์คํธ๋ฅผ ํ๋์ ๋ณตํฉ ์กฐ๊ฑด์ผ๋ก ๊ฒฐํฉํฉ๋๋ค.
+ *
+ * @param conditions ๊ฒฐํฉํ ์กฐ๊ฑด๋ค
+ * @return ๊ฒฐํฉ๋ ์กฐ๊ฑด
+ */
+ private fun createCompoundCondition(conditions: List): ASTNode {
+ return when {
+ conditions.isEmpty() -> BooleanNode.TRUE
+ conditions.size == 1 -> conditions.first()
+ else -> conditions.reduce { acc, condition ->
+ BinaryOpNode(acc, LOGICAL_AND, condition)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt
new file mode 100644
index 00000000..1dc3d3c7
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt
@@ -0,0 +1,254 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ์ซ์ ๋ฆฌํฐ๋ด์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ์ซ์ ๊ฐ(์ ์, ์ค์)์ ํํํ๋ฉฐ,
+ * Double ํ์
์ผ๋ก ๊ฐ์ ์ ์ฅํ์ฌ ์ ๋ฐํ ๊ณ์ฐ์ ์ง์ํฉ๋๋ค.
+ * ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋์ด ์์ ํ ๊ฐ ์ ๋ฌ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @property value ๋
ธ๋์ ์ซ์ ๊ฐ
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class NumberNode(val value: Double) : ASTNode() {
+
+ init {
+ if (!value.isFinite()) {
+ throw ASTException.numberNotFinite(value)
+ }
+ }
+
+ override fun getVariables(): Set = emptySet()
+
+ override fun getChildren(): List = emptyList()
+
+ override fun isLiteral(): Boolean = true
+
+ override fun getDepth(): Int = 1
+
+ override fun getNodeCount(): Int = 1
+
+ override fun copy(): NumberNode = NumberNode(value)
+
+ override fun toSimpleString(): String = when {
+ value == value.toLong().toDouble() -> value.toLong().toString()
+ else -> value.toString()
+ }
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitNumber(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is NumberNode && areNumbersEqual(this.value, other.value)
+
+ /**
+ * ๋ double ๊ฐ์ด ์๋ฏธ์ ์ผ๋ก ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ * ๋ถ๋์์์ ์ ๋ฐ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ก์ค๋ก ๊ธฐ๋ฐ ๋น๊ต๋ฅผ ์ฌ์ฉํฉ๋๋ค.
+ *
+ * @param a ์ฒซ ๋ฒ์งธ ๊ฐ
+ * @param b ๋ ๋ฒ์งธ ๊ฐ
+ * @return ๊ฐ์ด ๊ฐ์ผ๋ฉด true
+ */
+ private fun areNumbersEqual(a: Double, b: Double): Boolean {
+ // NaN๊ณผ ๋ฌดํ๋๋ ์ ํํ ๊ฐ์์ผ ํจ
+ if (a.isNaN() && b.isNaN()) return true
+ if (a.isInfinite() && b.isInfinite()) return a == b
+ if (a.isNaN() || b.isNaN()) return false
+ if (a.isInfinite() || b.isInfinite()) return false
+
+ // 0์ ๊ฐ๊น์ด ๊ฐ๋ค์ ์ ๋ ์ฐจ์ด๋ก ๋น๊ต
+ if (kotlin.math.abs(a) < EPSILON && kotlin.math.abs(b) < EPSILON) {
+ return kotlin.math.abs(a - b) < EPSILON
+ }
+
+ // ์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ๋ ์๋ ์ค์ฐจ๋ก ๋น๊ต
+ val diff = kotlin.math.abs(a - b)
+ val maxAbs = kotlin.math.max(kotlin.math.abs(a), kotlin.math.abs(b))
+ return diff <= EPSILON * maxAbs
+ }
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์ ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isInteger(): Boolean = value == value.toLong().toDouble()
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isPositive(): Boolean = value > 0.0
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isNegative(): Boolean = value < 0.0
+
+ /**
+ * ์ซ์ ๊ฐ์ด 0์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return 0์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isZero(): Boolean = value == 0.0
+
+ /**
+ * ์ซ์ ๊ฐ์ ์ ์๋ก ๋ณํํฉ๋๋ค.
+ *
+ * @return ์ ์ ๊ฐ
+ * @throws IllegalStateException ์ ์๊ฐ ์๋ ๊ฒฝ์ฐ
+ */
+ fun toInt(): Int {
+ if (!isInteger()) {
+ throw ASTException.notIntegerForInt(value)
+ }
+ return value.toInt()
+ }
+
+ /**
+ * ์ซ์ ๊ฐ์ Long์ผ๋ก ๋ณํํฉ๋๋ค.
+ *
+ * @return Long ๊ฐ
+ * @throws IllegalStateException ์ ์๊ฐ ์๋ ๊ฒฝ์ฐ
+ */
+ fun toLong(): Long {
+ if (!isInteger()) {
+ throw ASTException.notIntegerForLong(value)
+ }
+ return value.toLong()
+ }
+
+ /**
+ * ์ซ์์ ์ ๋๊ฐ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ ๋๊ฐ์ ๊ฐ์ง ์๋ก์ด NumberNode
+ */
+ fun abs(): NumberNode = NumberNode(kotlin.math.abs(value))
+
+ /**
+ * ์ซ์์ ๋ถํธ๋ฅผ ๋ฐ์ ํ ๊ฐ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ถํธ๊ฐ ๋ฐ์ ๋ ์๋ก์ด NumberNode
+ */
+ fun negate(): NumberNode = NumberNode(-value)
+
+ /**
+ * ๋ค๋ฅธ NumberNode์ ๋ํฉ๋๋ค.
+ *
+ * @param other ๋ํ NumberNode
+ * @return ํฉ์ ๊ฐ์ง ์๋ก์ด NumberNode
+ */
+ operator fun plus(other: NumberNode): NumberNode = NumberNode(value + other.value)
+
+ /**
+ * ๋ค๋ฅธ NumberNode์ ๋บ๋๋ค.
+ *
+ * @param other ๋บ NumberNode
+ * @return ์ฐจ๋ฅผ ๊ฐ์ง ์๋ก์ด NumberNode
+ */
+ operator fun minus(other: NumberNode): NumberNode = NumberNode(value - other.value)
+
+ /**
+ * ๋ค๋ฅธ NumberNode์ ๊ณฑํฉ๋๋ค.
+ *
+ * @param other ๊ณฑํ NumberNode
+ * @return ๊ณฑ์ ๊ฐ์ง ์๋ก์ด NumberNode
+ */
+ operator fun times(other: NumberNode): NumberNode = NumberNode(value * other.value)
+
+ /**
+ * ๋ค๋ฅธ NumberNode๋ก ๋๋๋๋ค.
+ *
+ * @param other ๋๋ NumberNode
+ * @return ๋ชซ์ ๊ฐ์ง ์๋ก์ด NumberNode
+ */
+ operator fun div(other: NumberNode): NumberNode {
+ if (other.isZero()) {
+ throw ASTException.divisionByZero()
+ }
+ return NumberNode(value / other.value)
+ }
+
+ /**
+ * ๋ค๋ฅธ NumberNode์ ํฌ๊ธฐ๋ฅผ ๋น๊ตํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ NumberNode
+ * @return ๋น๊ต ๊ฒฐ๊ณผ (-1, 0, 1)
+ */
+ operator fun compareTo(other: NumberNode): Int = value.compareTo(other.value)
+
+ override fun toString(): String = toSimpleString()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return "${spaces}$NUMBER_NODE: $value"
+ }
+
+ companion object {
+
+ const val NUMBER_NODE = "NumberNode:"
+ /**
+ * ๋ถ๋์์์ ๋น๊ต๋ฅผ ์ํ ์ก์ค๋ก ๊ฐ
+ */
+ private const val EPSILON = 1e-10
+
+ /**
+ * 0์ ๋ํ๋ด๋ NumberNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ val ZERO = NumberNode(0.0)
+
+ /**
+ * 1์ ๋ํ๋ด๋ NumberNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ val ONE = NumberNode(1.0)
+
+ /**
+ * -1์ ๋ํ๋ด๋ NumberNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ val MINUS_ONE = NumberNode(-1.0)
+
+ /**
+ * ์ ์ ๊ฐ์ผ๋ก NumberNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ ์ ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun of(value: Int): NumberNode = NumberNode(value.toDouble())
+
+ /**
+ * Long ๊ฐ์ผ๋ก NumberNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value Long ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun of(value: Long): NumberNode = NumberNode(value.toDouble())
+
+ /**
+ * Double ๊ฐ์ผ๋ก NumberNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value Double ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun of(value: Double): NumberNode = NumberNode(value)
+
+ /**
+ * ๋ฌธ์์ด๋ก๋ถํฐ NumberNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๋ฌธ์์ด
+ * @return NumberNode ์ธ์คํด์ค
+ * @throws NumberFormatException ์๋ชป๋ ์ซ์ ํ์์ธ ๊ฒฝ์ฐ
+ */
+ fun parse(value: String): NumberNode = NumberNode(value.toDouble())
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt
new file mode 100644
index 00000000..f2a34824
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt
@@ -0,0 +1,296 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ๋จํญ ์ฐ์ฐ(์: ์์, ๋
ผ๋ฆฌ ๋ถ์ )์ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ๋จํญ ์ฐ์ฐ์๋ฅผ ํํํ๋ฉฐ, ์ฐ์ฐ์์
+ * ํผ์ฐ์ฐ์๋ก ๊ตฌ์ฑ๋ฉ๋๋ค. ์์ ์ฐ์ฐ์(-), ๋
ผ๋ฆฌ ๋ถ์ ์ฐ์ฐ์(!) ๋ฑ์
+ * ์ง์ํ๋ฉฐ, ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋์ด ์์ ํ ์ฐ์ฐ ํธ๋ฆฌ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
+ *
+ * @property operator ์ฐ์ฐ์ ๋ฌธ์์ด (์: "-", "!")
+ * @property operand ํผ์ฐ์ฐ์ AST ๋
ธ๋
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class UnaryOpNode(
+ val operator: String,
+ val operand: ASTNode
+) : ASTNode() {
+
+ init {
+ if (operator.isBlank()) {
+ throw ASTException.operatorEmpty()
+ }
+
+ if (!isSupportedOperator(operator)) {
+ throw ASTException.unsupportedUnaryOperator(operator)
+ }
+ }
+
+ override fun getVariables(): Set = operand.getVariables()
+
+ override fun getChildren(): List = listOf(operand)
+
+ override fun isOperator(): Boolean = true
+
+ override fun getDepth(): Int = operand.getDepth() + 1
+
+ override fun getNodeCount(): Int = operand.getNodeCount() + 1
+
+ override fun copy(): UnaryOpNode = UnaryOpNode(operator, operand.copy())
+
+ override fun toSimpleString(): String = "$operator$operand"
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitUnaryOp(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is UnaryOpNode &&
+ this.operator == other.operator &&
+ this.operand.isStructurallyEqual(other.operand)
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ง์๋๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param op ํ์ธํ ์ฐ์ฐ์
+ * @return ์ง์๋๋ฉด true, ์๋๋ฉด false
+ */
+ private fun isSupportedOperator(op: String): Boolean = op in SUPPORTED_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์ฐ์ ๋จํญ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์ฐ์ ๋จํญ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isArithmeticOperator(): Boolean = operator in ARITHMETIC_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๋
ผ๋ฆฌ ๋จํญ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋
ผ๋ฆฌ ๋จํญ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isLogicalOperator(): Boolean = operator in LOGICAL_OPERATORS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์์ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์์ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isNegation(): Boolean = operator == MINUS
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ๋
ผ๋ฆฌ ๋ถ์ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋
ผ๋ฆฌ ๋ถ์ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isLogicalNot(): Boolean = operator == EXCLAMATION
+
+ /**
+ * ์ฐ์ฐ์๊ฐ ์์ ํ์ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ์์ ํ์ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isPositive(): Boolean = operator == PLUS
+
+ /**
+ * ์ฐ์ฐ์์ ์ฐ์ ์์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฐ์ ์์ (๋์์๋ก ๋จผ์ ๊ณ์ฐ)
+ */
+ fun getPrecedence(): Int = OPERATOR_PRECEDENCE[operator] ?: 0
+
+ /**
+ * ์ฐ์ฐ์์ ํ์
์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฐ์ฐ์ ํ์
+ */
+ fun getOperatorType(): OperatorType = when {
+ isArithmeticOperator() -> OperatorType.ARITHMETIC
+ isLogicalOperator() -> OperatorType.LOGICAL
+ else -> OperatorType.UNKNOWN
+ }
+
+ /**
+ * ํผ์ฐ์ฐ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด UnaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newOperand ์๋ก์ด ํผ์ฐ์ฐ์
+ * @return ์๋ก์ด UnaryOpNode
+ */
+ fun withOperand(newOperand: ASTNode): UnaryOpNode = UnaryOpNode(operator, newOperand)
+
+ /**
+ * ์ฐ์ฐ์๋ฅผ ๊ต์ฒดํ ์๋ก์ด UnaryOpNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param newOperator ์๋ก์ด ์ฐ์ฐ์
+ * @return ์๋ก์ด UnaryOpNode
+ */
+ fun withOperator(newOperator: String): UnaryOpNode = UnaryOpNode(newOperator, operand)
+
+ /**
+ * ์ด์ค ์์(-(-x))๋ฅผ ๋จ์ํํ ์ ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ ๊ฐ๋ฅํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun canSimplifyDoubleNegation(): Boolean =
+ isNegation() && operand is UnaryOpNode && operand.isNegation()
+
+ /**
+ * ์ด์ค ๋ถ์ (!(!x))๋ฅผ ๋จ์ํํ ์ ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ ๊ฐ๋ฅํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun canSimplifyDoubleNegationLogical(): Boolean =
+ isLogicalNot() && operand is UnaryOpNode && operand.isLogicalNot()
+
+ /**
+ * ์ด์ค ์์๋ฅผ ๋จ์ํํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ๋ AST ๋
ธ๋
+ * @throws IllegalStateException ๋จ์ํํ ์ ์๋ ๊ฒฝ์ฐ
+ */
+ fun simplifyDoubleNegation(): ASTNode {
+ if (!canSimplifyDoubleNegation()) {
+ throw ASTException.doubleNegationNotSimplifiable()
+ }
+ return (operand as UnaryOpNode).operand
+ }
+
+ /**
+ * ์ด์ค ๋
ผ๋ฆฌ ๋ถ์ ์ ๋จ์ํํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ๋ AST ๋
ธ๋
+ * @throws IllegalStateException ๋จ์ํํ ์ ์๋ ๊ฒฝ์ฐ
+ */
+ fun simplifyDoubleLogicalNegation(): ASTNode {
+ if (!canSimplifyDoubleNegationLogical()) {
+ throw ASTException.doubleLogicalNegationNotSimplifiable()
+ }
+ return (operand as UnaryOpNode).operand
+ }
+
+ /**
+ * UnaryOpNode๋ฅผ ๋จ์ํํฉ๋๋ค.
+ *
+ * @return ๋จ์ํ๋ AST ๋
ธ๋
+ */
+ fun simplify(): ASTNode {
+ return when {
+ canSimplifyDoubleNegation() -> simplifyDoubleNegation()
+ canSimplifyDoubleNegationLogical() -> simplifyDoubleLogicalNegation()
+ else -> this
+ }
+ }
+
+ /**
+ * ๊ดํธ๋ฅผ ํฌํจํ ๋ฌธ์์ด ํํ์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ดํธ๊ฐ ํฌํจ๋ ๋ฌธ์์ด
+ */
+ fun toStringWithParentheses(): String {
+ val operandStr = when {
+ operand is BinaryOpNode -> "(${operand.toSimpleString()})"
+ operand is UnaryOpNode && operand.getPrecedence() <= getPrecedence() -> "(${operand.toSimpleString()})"
+ else -> operand.toSimpleString()
+ }
+ return "$operator$operandStr"
+ }
+
+ override fun toString(): String = toStringWithParentheses()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return buildString {
+ appendLine("${spaces}UnaryOpNode: $operator")
+ appendLine("${spaces} operand:")
+ append(operand.toTreeString(indent + 2))
+ }
+ }
+
+ /**
+ * ์ฐ์ฐ์ ํ์
์ ๋ํ๋ด๋ ์ด๊ฑฐํ์
๋๋ค.
+ */
+ enum class OperatorType {
+ ARITHMETIC, LOGICAL, UNKNOWN
+ }
+
+ companion object {
+
+ const val MINUS = "-"
+ const val PLUS = "+"
+ const val EXCLAMATION = "!"
+
+ /**
+ * ์ง์๋๋ ๋ชจ๋ ๋จํญ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val SUPPORTED_OPERATORS = setOf("-", "!", "+")
+
+ /**
+ * ์ฐ์ ๋จํญ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val ARITHMETIC_OPERATORS = setOf("-", "+")
+
+ /**
+ * ๋
ผ๋ฆฌ ๋จํญ ์ฐ์ฐ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val LOGICAL_OPERATORS = setOf("!")
+
+ /**
+ * ์ฐ์ฐ์ ์ฐ์ ์์ ๋งต์
๋๋ค.
+ */
+ private val OPERATOR_PRECEDENCE = mapOf(
+ "!" to 8, // ์ต๊ณ ์ฐ์ ์์
+ "-" to 8, // ๋จํญ ๋ง์ด๋์ค
+ "+" to 8 // ๋จํญ ํ๋ฌ์ค
+ )
+
+ /**
+ * ์ง์๋๋ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ง์๋๋ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getSupportedOperators(): Set = SUPPORTED_OPERATORS.toSet()
+
+ /**
+ * ํน์ ํ์
์ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @param type ์ฐ์ฐ์ ํ์
+ * @return ํด๋น ํ์
์ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getOperatorsByType(type: OperatorType): Set = when (type) {
+ OperatorType.ARITHMETIC -> ARITHMETIC_OPERATORS
+ OperatorType.LOGICAL -> LOGICAL_OPERATORS
+ OperatorType.UNKNOWN -> emptySet()
+ }
+
+ /**
+ * ์์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return ์์ UnaryOpNode
+ */
+ fun negate(operand: ASTNode): UnaryOpNode = UnaryOpNode("-", operand)
+
+ /**
+ * ๋
ผ๋ฆฌ ๋ถ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return ๋
ผ๋ฆฌ ๋ถ์ UnaryOpNode
+ */
+ fun logicalNot(operand: ASTNode): UnaryOpNode = UnaryOpNode("!", operand)
+
+ /**
+ * ์์ ํ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return ์์ ํ์ UnaryOpNode
+ */
+ fun positive(operand: ASTNode): UnaryOpNode = UnaryOpNode("+", operand)
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt
new file mode 100644
index 00000000..6f244439
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt
@@ -0,0 +1,221 @@
+package hs.kr.entrydsm.domain.ast.entities
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.global.annotation.entities.Entity
+
+/**
+ * ๋ณ์๋ฅผ ๋ํ๋ด๋ AST ๋
ธ๋์
๋๋ค.
+ *
+ * ๊ณ์ฐ๊ธฐ ์ธ์ด์์ ์ฌ์ฉ๋๋ ๋ณ์๋ฅผ ํํํ๋ฉฐ, ์ค๊ดํธ๋ก ๋ฌถ์ธ ํํ({variable})๋ก
+ * ์
๋ ฅ๋ฉ๋๋ค. ๋ณ์๋ช
์ ์๋ณ์ ๊ท์น์ ๋ฐ๋ผ์ผ ํ๋ฉฐ, ํ๊ฐ ์ ๋ณ์ ๊ฐ์ผ๋ก ๋์ฒด๋ฉ๋๋ค.
+ * ๋ถ๋ณ ๊ฐ์ฒด๋ก ์ค๊ณ๋์ด ์์ ํ ๋ณ์ ์ฐธ์กฐ๋ฅผ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @property name ๋ณ์์ ์ด๋ฆ
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast")
+data class VariableNode(val name: String) : ASTNode() {
+
+ init {
+ if (name.isBlank()) {
+ throw ASTException.variableNameEmpty()
+ }
+
+ if (!isValidVariableName(name)) {
+ throw ASTException.invalidVariableName(name)
+ } }
+
+ override fun getVariables(): Set = setOf(name)
+
+ override fun getChildren(): List = emptyList()
+
+ override fun isVariable(): Boolean = true
+
+ override fun getDepth(): Int = 1
+
+ override fun getNodeCount(): Int = 1
+
+ override fun copy(): VariableNode = VariableNode(name)
+
+ override fun toSimpleString(): String = name
+
+ override fun accept(visitor: ASTVisitor): T = visitor.visitVariable(this)
+
+ override fun isStructurallyEqual(other: ASTNode): Boolean =
+ other is VariableNode && this.name == other.name
+
+ /**
+ * ๋ณ์๋ช
์ด ์ ํจํ ์๋ณ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param variableName ํ์ธํ ๋ณ์๋ช
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ private fun isValidVariableName(variableName: String): Boolean = isValidName(variableName)
+
+ /**
+ * ๋ณ์๋ช
์ด ํค์๋์ ์ถฉ๋ํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @return ํค์๋์ ์ถฉ๋ํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isKeywordConflict(): Boolean = name.uppercase() in RESERVED_KEYWORDS
+
+ /**
+ * ๋ณ์๋ช
์ด ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ง ์๊ณ ๋ค๋ฅธ ๋ณ์์ ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ ๋ณ์๋ช
+ * @return ๋์๋ฌธ์ ๋ฌด๊ดํ๊ฒ ๊ฐ์ผ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isSameVariableIgnoreCase(other: String): Boolean =
+ name.equals(other, ignoreCase = true)
+
+ /**
+ * ๋ณ์๋ช
์ด ๋ค๋ฅธ VariableNode์ ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ VariableNode
+ * @return ๊ฐ์ผ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isSameVariable(other: VariableNode): Boolean = name == other.name
+
+ /**
+ * ๋ณ์๋ช
์ด ๋ค๋ฅธ VariableNode์ ๋์๋ฌธ์ ๋ฌด๊ดํ๊ฒ ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param other ๋น๊ตํ VariableNode
+ * @return ๋์๋ฌธ์ ๋ฌด๊ดํ๊ฒ ๊ฐ์ผ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isSameVariableIgnoreCase(other: VariableNode): Boolean =
+ isSameVariableIgnoreCase(other.name)
+
+ /**
+ * ๋ณ์๋ช
์ ๊ธธ์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ณ์๋ช
๊ธธ์ด
+ */
+ fun getNameLength(): Int = name.length
+
+ /**
+ * ๋ณ์๋ช
์ด ํน์ ์ ๋์ฌ๋ก ์์ํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param prefix ํ์ธํ ์ ๋์ฌ
+ * @return ์ ๋์ฌ๋ก ์์ํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasPrefix(prefix: String): Boolean = name.startsWith(prefix)
+
+ /**
+ * ๋ณ์๋ช
์ด ํน์ ์ ๋ฏธ์ฌ๋ก ๋๋๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param suffix ํ์ธํ ์ ๋ฏธ์ฌ
+ * @return ์ ๋ฏธ์ฌ๋ก ๋๋๋ฉด true, ์๋๋ฉด false
+ */
+ fun hasSuffix(suffix: String): Boolean = name.endsWith(suffix)
+
+ /**
+ * ๋ณ์๋ช
์ ํน์ ๋ฌธ์์ด์ด ํฌํจ๋์ด ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param substring ํ์ธํ ๋ถ๋ถ ๋ฌธ์์ด
+ * @return ํฌํจ๋์ด ์์ผ๋ฉด true, ์๋๋ฉด false
+ */
+ fun contains(substring: String): Boolean = name.contains(substring)
+
+ /**
+ * ๋ณ์๋ช
์ ์๋ฌธ์๋ก ๋ณํํ ์๋ก์ด VariableNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์๋ฌธ์ ๋ณ์๋ช
์ ๊ฐ์ง ์๋ก์ด VariableNode
+ */
+ fun toLowerCase(): VariableNode = VariableNode(name.lowercase())
+
+ /**
+ * ๋ณ์๋ช
์ ๋๋ฌธ์๋ก ๋ณํํ ์๋ก์ด VariableNode๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋๋ฌธ์ ๋ณ์๋ช
์ ๊ฐ์ง ์๋ก์ด VariableNode
+ */
+ fun toUpperCase(): VariableNode = VariableNode(name.uppercase())
+
+ /**
+ * ๋ณ์๋ฅผ ์ค๊ดํธ ํํ๋ก ํํํฉ๋๋ค.
+ *
+ * @return "{๋ณ์๋ช
}" ํํ์ ๋ฌธ์์ด
+ */
+ fun toBracketedString(): String = "{$name}"
+
+ override fun toString(): String = toBracketedString()
+
+ override fun toTreeString(indent: Int): String {
+ val spaces = " ".repeat(indent)
+ return "${spaces}VariableNode: {$name}"
+ }
+
+ companion object {
+ /**
+ * ์์ฝ๋ ํค์๋ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val RESERVED_KEYWORDS = setOf(
+ "IF", "TRUE", "FALSE", "AND", "OR", "NOT"
+ )
+
+ /**
+ * ๋ณ์๋ช
์ด ์ ํจํ์ง ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ๊ฒ์ฆํ ๋ณ์๋ช
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isValidName(name: String): Boolean {
+ if (name.isBlank()) return false
+ if (!name.first().isLetter() && name.first() != '_') return false
+ return name.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ /**
+ * ์์ ํ ๋ฐฉ์์ผ๋ก VariableNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ๋ณ์๋ช
+ * @return VariableNode ์ธ์คํด์ค ๋๋ null (์ ํจํ์ง ์์ ๊ฒฝ์ฐ)
+ */
+ fun createSafe(name: String): VariableNode? = try {
+ if (isValidName(name)) VariableNode(name) else null
+ } catch (e: IllegalArgumentException) {
+ // ์ ํจํ์ง ์์ ๋ณ์๋ช
์ผ๋ก ์ธํ ์์๋ ์์ธ๋ ์กฐ์ฉํ ์ฒ๋ฆฌ
+ null
+ } catch (e: Exception) {
+ // ์์์น ๋ชปํ ์์ธ๋ ๊ฐ๋จํ๊ฒ ๋ก๊น
ํ๊ณ null ๋ฐํ
+ System.err.println("์์์น ๋ชปํ ์ค๋ฅ ๋ฐ์: name='$name', error=${e.javaClass.simpleName}: ${e.message}")
+ null
+ }
+
+ /**
+ * ์ฌ๋ฌ ๋ณ์๋ช
์ผ๋ก๋ถํฐ VariableNode ๋ฆฌ์คํธ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param names ๋ณ์๋ช
๋ฆฌ์คํธ
+ * @return ์ ํจํ VariableNode ๋ฆฌ์คํธ
+ */
+ fun createFromNames(names: List): List =
+ names.mapNotNull { createSafe(it) }
+
+ /**
+ * ๋ฌธ์์ด์์ ๋ณ์๋ช
์ ์ถ์ถํ์ฌ VariableNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param bracketedString "{๋ณ์๋ช
}" ํํ์ ๋ฌธ์์ด
+ * @return VariableNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์๋ชป๋ ํ์์ธ ๊ฒฝ์ฐ
+ */
+ fun fromBracketedString(bracketedString: String): VariableNode {
+ if (!(bracketedString.startsWith("{") && bracketedString.endsWith("}"))) {
+ throw ASTException.variableNotBracketed(bracketedString)
+ }
+
+ val variableName = bracketedString.substring(1, bracketedString.length - 1)
+ return VariableNode(variableName)
+ }
+
+ /**
+ * ์์ฝ๋ ํค์๋ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์์ฝ๋ ํค์๋ ์งํฉ
+ */
+ fun getReservedKeywords(): Set = RESERVED_KEYWORDS.toSet()
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt
new file mode 100644
index 00000000..b2a86258
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt
@@ -0,0 +1,28 @@
+package hs.kr.entrydsm.domain.ast.events
+
+/**
+ * AST ๋๋ฉ์ธ์์ ๋ฐํ/๊ตฌ๋
์ ์ฌ์ฉํ๋ ์ด๋ฒคํธ ํค ์์ ๋ชจ์.
+ *
+ * ๋ฉ์์ง ๋ธ๋ก์ปค, ์ด๋ฒคํธ ๋ฒ์ค, ๋ก๊น
๋ฑ์์ **์ผ๊ด๋ ์ด๋ฒคํธ ์ด๋ฆ**์ผ๋ก ํ์ฉํ์ธ์.
+ * (๊ถ์ฅ ๊ณตํต ํ์ด๋ก๋: `aggregateId`, `occurredAt`, `actor`, `metadata`)
+ */
+object DomainEvents {
+
+ /** AST(์ถ์ ๊ตฌ๋ฌธ ํธ๋ฆฌ)๊ฐ ์ต์ด ์์ฑ๋์์ ๋. */
+ const val AST_CREATED = "AST_CREATED"
+
+ /** AST๊ฐ ๋ณ๊ฒฝ๋์์ ๋(๋
ธ๋ ์ถ๊ฐ/์ญ์ /๊ฐฑ์ ๋ฑ). */
+ const val AST_MODIFIED = "AST_MODIFIED"
+
+ /** AST์ ๋ถ๋ถ ํธ๋ฆฌ๊ฐ ๋ค๋ฅธ ํธ๋ฆฌ๋ก ๊ต์ฒด๋์์ ๋. */
+ const val SUBTREE_REPLACED = "SUBTREE_REPLACED"
+
+ /** ExpressionAST ๋๋ฉ์ธ์ฉ ํ ํฝ/์นดํ
๊ณ ๋ฆฌ ์๋ณ์. */
+ const val EXPRESSION_AST = "ExpressionAST"
+
+ /** AST ์ต์ ํ(์์ ํด๋ฉ, ๋ถํ์ ๋
ธ๋ ์ ๊ฑฐ ๋ฑ) ์๋ฃ ์. */
+ const val AST_OPTIMIZED = "AST_OPTIMIZED"
+
+ /** AST ์ ํจ์ฑ ๊ฒ์ฆ์ ํต๊ณผํ์ ๋. */
+ const val AST_VALIDATED = "AST_VALIDATED"
+}
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt
new file mode 100644
index 00000000..9e7568fe
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt
@@ -0,0 +1,1207 @@
+package hs.kr.entrydsm.domain.ast.exceptions
+
+import hs.kr.entrydsm.global.exception.ErrorCode
+import hs.kr.entrydsm.global.exception.DomainException
+
+/**
+ * AST(Abstract Syntax Tree) ๋๋ฉ์ธ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * AST ๊ตฌ์ถ, ๋
ธ๋ ํ์
๊ฒ์ฆ, ํธ๋ฆฌ ๊ตฌ์กฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฑ์
+ * ์ถ์ ๊ตฌ๋ฌธ ํธ๋ฆฌ ๊ด๋ จ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
+ *
+ * @property nodeType ์ค๋ฅ์ ๊ด๋ จ๋ ๋
ธ๋ ํ์
(์ ํ์ฌํญ)
+ * @property nodeName ์ค๋ฅ๊ฐ ๋ฐ์ํ ๋
ธ๋ ์ด๋ฆ (์ ํ์ฌํญ)
+ * @property expectedType ์์๋ ๋
ธ๋ ํ์
(์ ํ์ฌํญ)
+ * @property actualType ์ค์ ๋
ธ๋ ํ์
(์ ํ์ฌํญ)
+ * @property reason ์ฌ์ (์ ํ์ฌํญ)
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+class ASTException(
+ errorCode: ErrorCode,
+ val nodeType: String? = null,
+ val nodeName: String? = null,
+ val expectedType: String? = null,
+ val actualType: String? = null,
+ val reason: String? = null,
+ message: String = buildASTMessage(errorCode, nodeType, nodeName, expectedType, actualType, reason),
+ cause: Throwable? = null
+) : DomainException(errorCode, message, cause) {
+
+ companion object {
+ /**
+ * AST ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
+ *
+ * @param errorCode ์ค๋ฅ ์ฝ๋
+ * @param nodeType ๋
ธ๋ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ
+ * @param expectedType ์์ ํ์
+ * @param actualType ์ค์ ํ์
+ * @param reason ์ฌ์
+ * @return ๊ตฌ์ฑ๋ ๋ฉ์์ง
+ */
+ private fun buildASTMessage(
+ errorCode: ErrorCode,
+ nodeType: String?,
+ nodeName: String?,
+ expectedType: String?,
+ actualType: String?,
+ reason: String?
+ ): String {
+ val baseMessage = errorCode.description
+ val details = mutableListOf()
+
+ nodeType?.let { details.add("๋
ธ๋ํ์
: $it") }
+ nodeName?.let { details.add("๋
ธ๋๋ช
: $it") }
+ expectedType?.let { details.add("์์ํ์
: $it") }
+ actualType?.let { details.add("์ค์ ํ์
: $it") }
+ reason?.let { details.add("์ฌ์ : $it") }
+
+ return if (details.isNotEmpty()) {
+ "$baseMessage (${details.joinToString(", ")})"
+ } else {
+ baseMessage
+ }
+ }
+
+ /**
+ * AST ๊ตฌ์ถ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ๊ตฌ์ถ ์คํจํ ๋
ธ๋ ํ์
+ * @param cause ์์ธ ์์ธ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun buildError(nodeType: String, cause: Throwable? = null): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_BUILD_ERROR,
+ nodeType = nodeType,
+ cause = cause
+ )
+ }
+
+ /**
+ * AST ๋
ธ๋๊ฐ ์๋ ๊ฒฝ์ฐ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notASTNode(actualType: String, nodeName: String? = null): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.NOT_AST_NODE,
+ actualType = actualType,
+ nodeName = nodeName
+ )
+ }
+
+ /**
+ * ์ง์ํ์ง ์๋ AST ํ์
์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ์ง์ํ์ง ์๋ ๋
ธ๋ ํ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unsupportedASTType(nodeType: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.UNSUPPORTED_AST_TYPE,
+ nodeType = nodeType
+ )
+ }
+
+ /**
+ * ์๋ชป๋ ๋
ธ๋ ๊ตฌ์กฐ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ๋ฌธ์ ๊ฐ ์๋ ๋
ธ๋ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidNodeStructure(nodeType: String, reason: String, nodeName: String? = null): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.INVALID_NODE_STRUCTURE,
+ nodeType = nodeType,
+ nodeName = nodeName,
+ reason = reason
+ )
+ }
+
+ /**
+ * ํ์
๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expectedType ์์๋ ํ์
+ * @param actualType ์ค์ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun typeMismatch(expectedType: String, actualType: String, nodeName: String? = null): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_TYPE_MISMATCH,
+ expectedType = expectedType,
+ actualType = actualType,
+ nodeName = nodeName
+ )
+ }
+
+ /**
+ * ๋ฃจํธ ๋
ธ๋ ์ ํจ์ฑ ์คํจ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param reason ์ ํจํ์ง ์์ ์ฌ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidRootNode(reason: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.INVALID_ROOT_NODE,
+ reason = reason
+ )
+ }
+
+ /**
+ * AST ํฌ๊ธฐ ์ ํ ์ด๊ณผ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun sizeLimitExceeded(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_SIZE_EXCEEDED
+ )
+ }
+
+ /**
+ * AST ๊น์ด ์ ํ ์ด๊ณผ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun depthLimitExceeded(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_DEPTH_EXCEEDED
+ )
+ }
+
+ /**
+ * ๊ต์ฒด ๋
ธ๋ ์ ํจ์ฑ ์คํจ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param reason ์ ํจํ์ง ์์ ์ฌ์
+ * @param nodeType ๋ฌธ์ ๊ฐ ์๋ ๋
ธ๋ ํ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidReplacementNode(reason: String, nodeType: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.INVALID_REPLACEMENT_NODE,
+ reason = reason,
+ nodeType = nodeType
+ )
+ }
+
+ /**
+ * ์ธ์ ๊ฐ์ ์ ํ ์ด๊ณผ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argumentCountExceeded(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_ARGUMENT_COUNT_EXCEEDED
+ )
+ }
+
+ /**
+ * ์ธ๋ฑ์ค๊ฐ ํ์ฉ ๋ฒ์๋ฅผ ๋ฒ์ด๋ฌ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun indexOutOfRange(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_INDEX_OUT_OF_RANGE
+ )
+ }
+
+ /**
+ * ์ฐ์ฐ์ ๊ฐ์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun operatorEmpty(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_OPERATOR_EMPTY
+ )
+ }
+
+ /**
+ * ์ง์ํ์ง ์๋ ์ฐ์ฐ์์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unsupportedOperator(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_UNSUPPORTED_OPERATOR
+ )
+ }
+
+ /**
+ * ๊ตํ๋ฒ์น์ ์๊ตฌํ๋ ๋ฌธ๋งฅ์์ ๊ตํ๋ฒ์น์ด ์ฑ๋ฆฝํ์ง ์๋ ์ฐ์ฐ์์ผ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun operatorNotCommutative(operator: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_OPERATOR_NOT_COMMUTATIVE,
+ reason = "operator: $operator"
+ )
+ }
+
+ /**
+ * ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ด ์ ํจํ์ง ์์ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidBooleanValue(value: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.INVALID_BOOLEAN_VALUE,
+ reason = "value: $value"
+ )
+ }
+
+ /**
+ * ํจ์๋ช
์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionNameEmpty(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_NAME_EMPTY
+ )
+ }
+
+ /**
+ * ์ ํจํ์ง ์์ ํจ์๋ช
์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidFunctionName(name: String): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_INVALID_FUNCTION_NAME,
+ reason = "function name: $name"
+ )
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argumentsEmpty(): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_ARGUMENTS_EMPTY
+ )
+ }
+
+ /**
+ * IF ๋
ธ๋๋ฅผ ๋จ์ํํ ์ ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun ifNotSimplifiable(
+ nodeName: String? = "IfNode"
+ ): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_IF_NOT_SIMPLIFIABLE,
+ nodeType = "IfNode",
+ nodeName = nodeName,
+ )
+ }
+
+ /**
+ * ๋จ์ํ ๋ก์ง์์ ์ฒ๋ฆฌํ์ง ๋ชปํ ์์์น ๋ชปํ ์ผ์ด์ค๊ฐ ๋ฐ์ํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun simplificationUnexpectedCase(
+ nodeName: String? = "IfNode"
+ ): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_SIMPLIFICATION_UNEXPECTED_CASE,
+ nodeType = "IfNode",
+ nodeName = nodeName,
+ )
+ }
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์ ํํ์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๊ฒ์ฌํ ์๋ณธ ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun numberNotFinite(value: Double): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NON_FINITE_NUMBER,
+ nodeType = "NumberNode",
+ reason = "value: $value"
+ )
+
+ /**
+ * ์ ์๊ฐ ์๋ ๊ฐ์ Int๋ก ๋ณํํ๋ ค ํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๋ณํ ๋์ ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notIntegerForInt(value: Double): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NON_INTEGER_TO_INT,
+ nodeType = "NumberNode",
+ reason = "value: $value"
+ )
+
+ /**
+ * ์ ์๊ฐ ์๋ ๊ฐ์ Long์ผ๋ก ๋ณํํ๋ ค ํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๋ณํ ๋์ ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notIntegerForLong(value: Double): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NON_INTEGER_TO_LONG,
+ nodeType = "NumberNode",
+ reason = "value: $value"
+ )
+
+ /**
+ * 0์ผ๋ก ๋๋๋ ค ํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun divisionByZero(): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_DIVISION_BY_ZERO,
+ nodeType = "NumberNode",
+ reason = "denominator=0"
+ )
+
+ /**
+ * ์ง์ํ์ง ์๋ ๋จํญ ์ฐ์ฐ์์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ ๋ฌ๋ ์ฐ์ฐ์ ๋ฌธ์์ด(์: "!", "-")
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unsupportedUnaryOperator(
+ operator: String,
+ nodeName: String? = null
+ ): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_UNSUPPORTED_UNARY_OPERATOR,
+ nodeType = "UnaryOpNode",
+ nodeName = nodeName,
+ reason = "operator: $operator"
+ )
+ }
+
+ /**
+ * ์ด์ค ์์(์: `--x`)๋ฅผ ๋จ์ํํ ์ ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * ์ ์ฉ ์ค๋ฅ ์ฝ๋: [ErrorCode.AST_DOUBLE_NEGATION_NOT_SIMPLIFIABLE]
+ *
+ * @param detail ๋ถ๊ฐ ์ฌ์ ์์ธ(์ ํ)
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(๊ธฐ๋ณธ๊ฐ: `"UnaryOpNode"`)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun doubleNegationNotSimplifiable(
+ detail: String? = null,
+ nodeName: String? = "UnaryOpNode"
+ ): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_DOUBLE_NEGATION_NOT_SIMPLIFIABLE,
+ nodeType = "UnaryOpNode",
+ nodeName = nodeName,
+ reason = detail
+ )
+ }
+
+ /**
+ * ์ด์ค ๋
ผ๋ฆฌ ๋ถ์ (์: `!!x`)์ ๋จ์ํํ ์ ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param detail ๋ถ๊ฐ ์ฌ์ ์์ธ(์ ํ)
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(๊ธฐ๋ณธ๊ฐ: `"UnaryOpNode"`)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun doubleLogicalNegationNotSimplifiable(
+ detail: String? = null,
+ nodeName: String? = "UnaryOpNode"
+ ): ASTException {
+ return ASTException(
+ errorCode = ErrorCode.AST_DOUBLE_LOGICAL_NEGATION_NOT_SIMPLIFIABLE,
+ nodeType = "UnaryOpNode",
+ nodeName = nodeName,
+ reason = detail
+ )
+ }
+
+ /**
+ * ๋ณ์๋ช
์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ๋
ธ๋ ํ์
(๊ธฐ๋ณธ: "VariableNode")
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableNameEmpty(
+ nodeType: String? = "VariableNode",
+ nodeName: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_NAME_EMPTY,
+ nodeType = nodeType,
+ nodeName = nodeName
+ )
+
+ /**
+ * ์ ํจํ์ง ์์ ๋ณ์๋ช
์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ๊ฒ์ฆ ์คํจํ ๋ณ์๋ช
+ * @param nodeType ๋
ธ๋ ํ์
(๊ธฐ๋ณธ: "VariableNode")
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidVariableName(
+ name: String,
+ nodeType: String? = "VariableNode",
+ nodeName: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_INVALID_VARIABLE_NAME,
+ nodeType = nodeType,
+ nodeName = nodeName,
+ reason = "name: $name"
+ )
+
+ /**
+ * ๋ณ์ ํ๊ธฐ ๋ฌธ์์ด์ด ์ค๊ดํธ๋ก ๋๋ฌ์ธ์ฌ ์์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์๋ณธ ๋ฌธ์์ด(์: "{USER_NAME}")
+ * @param expectedOpen ๊ธฐ๋ ์ฌ๋ ๊ดํธ(๊ธฐ๋ณธ: "{")
+ * @param expectedClose ๊ธฐ๋ ๋ซ๋ ๊ดํธ(๊ธฐ๋ณธ: "}")
+ * @param nodeType ๋
ธ๋ ํ์
(๊ธฐ๋ณธ: "VariableNode")
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ ๋๋ ์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableNotBracketed(
+ value: String,
+ expectedOpen: String = "{",
+ expectedClose: String = "}",
+ nodeType: String? = "VariableNode",
+ nodeName: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_NOT_BRACKETED,
+ nodeType = nodeType,
+ nodeName = nodeName,
+ reason = "value: $value, expected: $expectedOpen...$expectedClose"
+ )
+
+ /**
+ * ๋
ธ๋ ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ๋
ธ๋ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ/์๋ณ์(์ ํ)
+ * @param reason ์คํจ ์ฌ์ (์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeValidationFailed(
+ nodeName: String? = null,
+ reason: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_VALIDATION_FAILED,
+ nodeName = nodeName,
+ reason = reason
+ )
+
+ /**
+ * ๋
ธ๋ ๊ตฌ์กฐ ๊ฒ์ฆ ์คํจ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param nodeType ๋
ธ๋ ํ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ/์๋ณ์(์ ํ)
+ * @param reason ์คํจ ์ฌ์ (์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeStructureFailed(
+ nodeName: String? = null,
+ reason: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.INVALID_NODE_STRUCTURE,
+ nodeName = nodeName,
+ reason = reason
+ )
+
+ /**
+ * ์ ํจํ์ง ์์ ์ซ์ ๋ฆฌํฐ๋ด ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์๋ณธ ๋ฌธ์์ด
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun invalidNumberLiteral(value: String): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_INVALID_NUMBER_LITERAL,
+ nodeType = "NumberNode",
+ reason = "value=$value"
+ )
+
+ /**
+ * ์ฐ์ ์ฐ์ฐ์๊ฐ ์๋ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ ๋ฌ๋ ์ฐ์ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notArithmeticOperator(operator: String): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NOT_ARITHMETIC_OPERATOR,
+ nodeType = "BinaryOpNode",
+ reason = "operator=$operator"
+ )
+
+ /**
+ * ๋น๊ต ์ฐ์ฐ์๊ฐ ์๋ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ ๋ฌ๋ ์ฐ์ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notComparisonOperator(operator: String): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NOT_COMPARISON_OPERATOR,
+ nodeType = "BinaryOpNode",
+ reason = "operator=$operator"
+ )
+
+ /**
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ์๊ฐ ์๋ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ ๋ฌ๋ ์ฐ์ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun notLogicalOperator(operator: String): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NOT_LOGICAL_OPERATOR,
+ nodeType = "BinaryOpNode",
+ reason = "operator=$operator"
+ )
+
+ /**
+ * ์ง์๋์ง ์๋ ์ํ ํจ์์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unsupportedMathFunction(name: String): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_UNSUPPORTED_MATH_FUNCTION,
+ nodeType = "FunctionCallNode",
+ reason = "name=$name"
+ )
+
+ /**
+ * ArgsMultiple ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์(๊ธฐ๋ณธ: 3)
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ/์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argsMultipleChildrenMismatch(
+ expected: Int = 3,
+ actual: Int,
+ nodeName: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_ARGS_MULTIPLE_CHILDREN_MISMATCH,
+ nodeName = nodeName,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * ArgsSingle ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์(๊ธฐ๋ณธ: 1)
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @param nodeName ๋
ธ๋ ์ด๋ฆ/์๋ณ์(์ ํ)
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argsSingleChildMismatch(
+ expected: Int = 1,
+ actual: Int,
+ nodeName: String? = null
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_ARGS_SINGLE_CHILD_MISMATCH,
+ nodeName = nodeName,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * BinaryOp ๋น๋์ ์์ ์๊ฐ ์๊ตฌ์ฌํญ์ ๋ชป ๋ฏธ์น ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param required ํ์ํ ์ต์ ์์ ์
+ * @param actual ์ค์ ์์ ์
+ * @param leftIndex ์ผ์ชฝ ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @param rightIndex ์ค๋ฅธ์ชฝ ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun binaryChildrenInsufficient(
+ required: Int,
+ actual: Int,
+ leftIndex: Int,
+ rightIndex: Int
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_BINARY_CHILDREN_INSUFFICIENT,
+ reason = "required: $required(actual indices need 0..${required-1}), " +
+ "actual: $actual, leftIndex: $leftIndex, rightIndex: $rightIndex"
+ )
+
+ /**
+ * BinaryOp ๋น๋์ ํผ์ฐ์ฐ์๊ฐ AST ๋
ธ๋๊ฐ ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param side "left" ๋๋ "right"
+ * @param actualType ๋ฐํ์ ํ์
๋ช
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun operandNotAst(
+ side: String,
+ actualType: String?
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.NOT_AST_NODE, // AST002 ์ฌ์ฌ์ฉ
+ reason = "operand: $side, actualType: $actualType"
+ )
+
+ /**
+ * FunctionCall ๋น๋์ ์์ ๊ฐ์๊ฐ ์์๊ณผ ๋ค๋ฅผ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ํ๋ ์์ ๊ฐ์ (๊ธฐ๋ณธ: 3)
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallChildrenMismatch(
+ expected: Int = 3,
+ actual: Int,
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * FunctionCall ๋น๋์ ์ฒซ ๋ฒ์งธ ์์์ด Token์ด ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallFirstNotToken(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_FIRST_NOT_TOKEN,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * FunctionCall ๋น๋์ ์ธ ๋ฒ์งธ ์์์ด List๊ฐ ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallThirdNotList(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_THIRD_NOT_LIST,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * FunctionCall ๋น๋์ ์ธ์ ๋ชฉ๋ก์ ASTNode๊ฐ ์๋ ์์๊ฐ ํฌํจ๋์ด ์์ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallArgsNotAstNode(): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_ARGS_NOT_AST_NODE,
+ )
+
+ /**
+ * FunctionCallEmpty ๋น๋์ ์์ ๊ฐ์๊ฐ ์์๊ณผ ๋ค๋ฅผ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ํ๋ ์์ ๊ฐ์ (๊ธฐ๋ณธ: 3)
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallEmptyChildrenMismatch(
+ expected: Int = 3,
+ actual: Int
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * FunctionCallEmpty ๋น๋์ ์ฒซ ๋ฒ์งธ ์์์ด Token์ด ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallEmptyFirstNotToken(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_FIRST_NOT_TOKEN,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * FunctionCallEmpty ๋น๋์ ๋ ๋ฒ์งธ ์์์ด Token์ด ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallEmptySecondNotToken(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_SECOND_NOT_TOKEN,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * FunctionCallEmpty ๋น๋์ ์ธ ๋ฒ์งธ ์์์ด Token์ด ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionCallEmptyThirdNotToken(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_THIRD_NOT_TOKEN,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * Identity ๋น๋์ ์์ ๋ชฉ๋ก์ด ๋น์ด ์์ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expectedAtLeast ์ต์ ํ์ ๊ฐ์(๊ธฐ๋ณธ: 1)
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun identityChildrenEmpty(
+ expectedAtLeast: Int = 1,
+ actual: Int
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_IDENTITY_CHILDREN_EMPTY,
+ reason = "expectedโฅ$expectedAtLeast, actual=$actual"
+ )
+
+ /**
+ * Identity ๋น๋์ ์ฒซ ๋ฒ์งธ ์์์ด ASTNode๊ฐ ์๋ ๋ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
์ด๋ฆ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun identityFirstNotAstNode(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.NOT_AST_NODE, // ์ฌ์ฌ์ฉ
+ reason = "child=first, actualType=$actualType"
+ )
+
+ /**
+ * If ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ */
+ fun ifChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_IF_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * Number ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ */
+ fun numberChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NUMBER_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * Parenthesized ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ */
+ fun parenthesizedChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_PARENTHESIZED_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * Parenthesized ๋น๋ ๋ ๋ฒ์งธ ์์ ํ์
์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
๋ช
+ */
+ fun parenthesizedSecondNotAst(actualType: String?): ASTException = ASTException(
+ errorCode = ErrorCode.AST_PARENTHESIZED_SECOND_NOT_AST,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * Start ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun startChildrenMismatch(expected: Int, actual: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_START_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * Start ๋น๋ ์ฒซ ๋ฒ์งธ ์์ ํ์
์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
๋ช
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun startFirstNotAst(actualType: String?): ASTException =
+ ASTException(
+ errorCode = ErrorCode.NOT_AST_NODE, // ์ฌ์ฌ์ฉ
+ reason = "child=first, actualType: $actualType"
+ )
+
+ /**
+ * UnaryOp ๋น๋ ์์ ๊ฐ์ ๋ถ์กฑ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param required ์ต์ ํ์ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @param operandIndex ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unaryChildrenInsufficient(
+ required: Int,
+ actual: Int,
+ operandIndex: Int
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_UNARY_CHILDREN_INSUFFICIENT,
+ reason = "required: $required, actual: $actual, operandIndex: $operandIndex"
+ )
+
+ /**
+ * Variable ๋น๋ ์์ ๊ฐ์ ๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param expected ๊ธฐ๋ ์์ ๊ฐ์
+ * @param actual ์ค์ ์์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableChildrenMismatch(expected: Int, actual: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_CHILDREN_MISMATCH,
+ reason = "expected: $expected, actual: $actual"
+ )
+
+ /**
+ * Variable ๋น๋ ์ฒซ ๋ฒ์งธ ์์ ํ์
์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์ค์ ํ์
๋ช
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableFirstNotToken(actualType: String?): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_FIRST_NOT_TOKEN,
+ reason = "actualType: $actualType"
+ )
+
+ /**
+ * ์ซ์ ๊ฐ์ด NaN์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value NaN์ธ ์ซ์ ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun numberIsNaN(value: Double): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NUMBER_IS_NAN,
+ reason = "value=$value"
+ )
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์ต์๊ฐ ๋ฏธ๋ง์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ค์ ์ซ์ ๊ฐ
+ * @param min ์ต์ ํ์ฉ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun numberTooSmall(value: Double, min: Double): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NUMBER_TOO_SMALL,
+ reason = "value=$value, min=$min"
+ )
+
+ /**
+ * ์ซ์ ๊ฐ์ด ์ต๋๊ฐ์ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ค์ ์ซ์ ๊ฐ
+ * @param max ์ต๋ ํ์ฉ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun numberTooLarge(value: Double, max: Double): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NUMBER_TOO_LARGE,
+ reason = "value=$value, max=$max"
+ )
+
+ /**
+ * ๋ณ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param length ์ค์ ๋ณ์๋ช
๊ธธ์ด
+ * @param max ์ต๋ ํ์ฉ ๊ธธ์ด
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableNameTooLong(length: Int, max: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_NAME_TOO_LONG,
+ reason = "length=$length, max=$max"
+ )
+
+ /**
+ * ์์ฝ์ด๋ฅผ ๋ณ์๋ช
์ผ๋ก ์ฌ์ฉํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ์์ฝ์ด ๋ณ์๋ช
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun variableReservedWord(name: String): ASTException = ASTException(
+ errorCode = ErrorCode.AST_VARIABLE_RESERVED_WORD,
+ reason = "name=$name"
+ )
+
+ /**
+ * ์ง์๋์ง ์๋ ์ดํญ ์ฐ์ฐ์์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์ ๊ธฐํธ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun unsupportedBinaryOperator(operator: String): ASTException = ASTException(
+ errorCode = ErrorCode.AST_UNSUPPORTED_BINARY_OPERATOR,
+ reason = "operator=$operator"
+ )
+
+ /**
+ * 0์ผ๋ก ๋๋จธ์ง ์ฐ์ฐ์ ์๋ํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun moduloByZero(): ASTException = ASTException(
+ errorCode = ErrorCode.AST_MODULO_BY_ZERO,
+ )
+
+ /**
+ * 0^0 ์ฐ์ฐ์ ์๋ํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun zeroPowerZero(): ASTException = ASTException(
+ errorCode = ErrorCode.AST_ZERO_POWER_ZERO_UNDEFINED,
+ )
+
+ /**
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ์์ ํผ์ฐ์ฐ์๊ฐ ๋
ผ๋ฆฌ์ ์ผ๋ก ํธํ๋์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun logicalIncompatibleOperand(): ASTException = ASTException(
+ errorCode = ErrorCode.AST_LOGICAL_INCOMPATIBLE_OPERAND,
+ )
+
+ /**
+ * ํจ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param length ์ค์ ํจ์๋ช
๊ธธ์ด
+ * @param max ์ต๋ ํ์ฉ ๊ธธ์ด
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionNameTooLong(length: Int, max: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_NAME_TOO_LONG,
+ reason = "length=$length, max=$max"
+ )
+
+ /**
+ * ํจ์ ์ธ์ ๊ฐ์๊ฐ ์ต๋ ํ์ฉ ๊ฐ์๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param size ์ค์ ์ธ์ ๊ฐ์
+ * @param max ์ต๋ ํ์ฉ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionArgumentsExceeded(size: Int, max: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_ARGUMENTS_EXCEEDED,
+ reason = "args=$size, max=$max"
+ )
+
+ /**
+ * ํจ์ ํธ์ถ ์ธ์ ๊ฐ์๊ฐ ์๊ตฌ์ฌํญ๊ณผ ์ผ์นํ์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param expectedDesc ์๊ตฌ๋๋ ์ธ์ ๊ฐ์ ์ค๋ช
+ * @param actual ์ค์ ์ธ์ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun functionArgumentCountMismatch(
+ name: String,
+ expectedDesc: String,
+ actual: Int
+ ): ASTException = ASTException(
+ errorCode = ErrorCode.AST_FUNCTION_ARGUMENT_COUNT_MISMATCH,
+ reason = "name=$name, expected=$expectedDesc, actual=$actual"
+ )
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ์ ์ด ๊น์ด๊ฐ ์ต๋ ํ์ฉ ๊น์ด๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param totalDepth ์ค์ ์ด ๊น์ด
+ * @param max ์ต๋ ํ์ฉ ๊น์ด
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun ifTotalDepthExceeded(totalDepth: Int, max: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_IF_TOTAL_DEPTH_EXCEEDED,
+ reason = "totalDepth=$totalDepth, max=$max"
+ )
+
+ /**
+ * ์ธ์ ๊ฐ์๊ฐ ์ต๋ ํ์ฉ ๊ฐ์๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param size ์ค์ ์ธ์ ๊ฐ์
+ * @param max ์ต๋ ํ์ฉ ๊ฐ์
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argumentsExceeded(size: Int, max: Int): ASTException = ASTException(
+ errorCode = ErrorCode.AST_ARGUMENTS_EXCEEDED,
+ reason = "args=$size, max=$max"
+ )
+
+ /**
+ * ์ค๋ณต๋ ์ธ์๊ฐ ์กด์ฌํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param duplicates ์ค๋ณต๋ ์ธ์ ๋ชฉ๋ก
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun argumentsDuplicated(duplicates: Collection<*>): ASTException = ASTException(
+ errorCode = ErrorCode.AST_ARGUMENTS_DUPLICATED,
+ reason = "duplicates=$duplicates"
+ )
+
+ /**
+ * ๋
ธ๋ ํฌ๊ธฐ๊ฐ ์ต๋ ํ์ฉ ํฌ๊ธฐ๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param size ์ค์ ํฌ๊ธฐ
+ * @param max ์ต๋ ํ์ฉ ํฌ๊ธฐ
+ * @param context ์ปจํ
์คํธ ๋ผ๋ฒจ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeSizeExceeded(size: Int, max: Int, context: String): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NODE_SIZE_EXCEEDED,
+ reason = "$context size=$size, max=$max"
+ )
+
+ /**
+ * ๋
ธ๋ ๊น์ด๊ฐ ์ต๋ ํ์ฉ ๊น์ด๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param depth ์ค์ ๊น์ด
+ * @param max ์ต๋ ํ์ฉ ๊น์ด
+ * @param context ์ปจํ
์คํธ ๋ผ๋ฒจ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeDepthExceeded(depth: Int, max: Int, context: String): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NODE_DEPTH_EXCEEDED,
+ reason = "$context depth=$depth, max=$max"
+ )
+
+ /**
+ * ๋
ธ๋์ ๋ณ์ ๊ฐ์๊ฐ ์ต๋ ํ์ฉ ๊ฐ์๋ฅผ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param count ์ค์ ๋ณ์ ๊ฐ์
+ * @param max ์ต๋ ํ์ฉ ๊ฐ์
+ * @param context ์ปจํ
์คํธ ๋ผ๋ฒจ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeVariablesExceeded(count: Int, max: Int, context: String): ASTException = ASTException(
+ errorCode = ErrorCode.AST_NODE_VARIABLES_EXCEEDED,
+ reason = "$context variables=$count, max=$max"
+ )
+
+ /**
+ * ํธ๋ฆฌ ๊น์ด๊ฐ 0 ๋ฏธ๋ง์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๊น์ด ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun treeDepthNegative(actual: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_TREE_DEPTH_NEGATIVE,
+ nodeType = "Tree",
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ํธ๋ฆฌ ๊น์ด๊ฐ ์ต๋ ํ์ฉ๊ฐ์ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๊น์ด ๊ฐ
+ * @param max ์ต๋ ํ์ฉ ๊น์ด
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun treeDepthTooLarge(actual: Int, max: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_TREE_DEPTH_TOO_LARGE,
+ nodeType = "Tree",
+ reason = "actual=$actual, max=$max"
+ )
+
+ /**
+ * ํ์ฌ ๋ฒ์ ์์ ๋ฐํ์ ๊ท์น ์ถ๊ฐ๋ฅผ ์ง์ํ์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun runtimeRuleNotSupported(): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_RUNTIME_RULE_NOT_SUPPORTED
+ )
+
+ /**
+ * ๋
ธ๋ ํฌ๊ธฐ๊ฐ 0 ๋ฏธ๋ง์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ํฌ๊ธฐ ๊ฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeSizeNegative(actual: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NODE_SIZE_NEGATIVE,
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ๋
ธ๋ ํฌ๊ธฐ๊ฐ ์ต๋ ํ์ฉ๊ฐ์ ์ด๊ณผํ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ํฌ๊ธฐ ๊ฐ
+ * @param max ์ต๋ ํ์ฉ ํฌ๊ธฐ
+ * @return ASTException ์ธ์คํด์ค
+ */
+ fun nodeSizeTooLarge(actual: Int, max: Int): ASTException =
+ ASTException(
+ errorCode = ErrorCode.AST_NODE_SIZE_TOO_LARGE,
+ reason = "actual=$actual, max=$max"
+ )
+
+ }
+
+ /**
+ * AST ์ค๋ฅ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํ๋ ๋งต์ผ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋
ธ๋ ํ์
, ์ด๋ฆ, ์์/์ค์ ํ์
์ ๋ณด๊ฐ ํฌํจ๋ ๋งต
+ */
+ fun getASTInfo(): Map = mapOf(
+ "nodeType" to nodeType,
+ "nodeName" to nodeName,
+ "expectedType" to expectedType,
+ "actualType" to actualType
+ ).filterValues { it != null }
+
+ /**
+ * ์ ์ฒด ์ค๋ฅ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํ๋ ๋งต์ผ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๊ธฐ๋ณธ ์ค๋ฅ ์ ๋ณด์ AST ์ ๋ณด๊ฐ ๊ฒฐํฉ๋ ๋งต
+ */
+ fun getFullErrorInfo(): Map {
+ val baseInfo = super.toErrorInfo().toMutableMap()
+ val astInfo = getASTInfo()
+
+ astInfo.forEach { (key, value) ->
+ baseInfo[key] = value.toString()
+ }
+
+ return baseInfo
+ }
+
+ override fun toString(): String {
+ val astDetails = getASTInfo()
+ return if (astDetails.isNotEmpty()) {
+ "${super.toString()}, ast=${astDetails}"
+ } else {
+ super.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt
new file mode 100644
index 00000000..c78bd590
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt
@@ -0,0 +1,479 @@
+package hs.kr.entrydsm.domain.ast.factories
+
+import hs.kr.entrydsm.domain.ast.entities.*
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.specifications.ASTValiditySpec
+import hs.kr.entrydsm.domain.ast.specifications.NodeStructureSpec
+import hs.kr.entrydsm.domain.ast.policies.ASTValidationPolicy
+import hs.kr.entrydsm.domain.ast.policies.NodeCreationPolicy
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * AST ๋
ธ๋ ๊ฐ์ฒด๋ค์ ์์ฑํ๋ ํฉํ ๋ฆฌ์
๋๋ค.
+ *
+ * ๋ค์ํ ํ์
์ AST ๋
ธ๋๋ฅผ ์์ฑํ๋ฉฐ, ๋๋ฉ์ธ ๊ท์น๊ณผ ์ ์ฑ
์
+ * ์ ์ฉํ์ฌ ์ผ๊ด๋ ๊ฐ์ฒด ์์ฑ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Factory(context = "ast", complexity = Complexity.HIGH, cache = true)
+class ASTNodeFactory {
+
+ private val validitySpec = ASTValiditySpec()
+ private val structureSpec = NodeStructureSpec()
+ private val validationPolicy = ASTValidationPolicy()
+ private val creationPolicy = NodeCreationPolicy()
+
+ companion object {
+
+ private val ARITHMETIC_OPERATORS = setOf(
+ "+", "-", "*", "/", "^", "%"
+ )
+
+ private val COMPARISON_OPERATORS = setOf(
+ "==", "!=", "<", "<=", ">", ">="
+ )
+
+ private val LOGICAL_OPERATORS = setOf(
+ "&&", "||"
+ )
+
+ private const val PLUS = "+"
+ private const val EXCLAMATION = "!"
+ private const val MINUS = "-"
+
+ /**
+ * ์ง์๋๋ ์ํ ํจ์ ๋ชฉ๋ก์
๋๋ค.
+ */
+ private val SUPPORTED_MATH_FUNCTIONS = setOf(
+ "SIN", "COS", "TAN", "SQRT", "ABS", "LOG", "EXP"
+ )
+
+ private val createdNodeCount = AtomicLong(0)
+ private val createdNumberCount = AtomicLong(0)
+ private val createdBooleanCount = AtomicLong(0)
+ private val createdVariableCount = AtomicLong(0)
+ private val createdBinaryOpCount = AtomicLong(0)
+ private val createdUnaryOpCount = AtomicLong(0)
+ private val createdFunctionCallCount = AtomicLong(0)
+ private val createdIfCount = AtomicLong(0)
+ private val createdArgumentsCount = AtomicLong(0)
+
+ private val instance = ASTNodeFactory()
+
+ /**
+ * ์ฑ๊ธํค ํฉํ ๋ฆฌ ์ธ์คํด์ค๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ @JvmStatic
+ fun getInstance(): ASTNodeFactory = instance
+
+ /**
+ * ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ๋
ธ๋๋ฅผ ์์ฑํ๋ ํธ์ ๋ฉ์๋์
๋๋ค.
+ */
+ @JvmStatic
+ fun quickCreateNumber(value: Double): NumberNode {
+ return getInstance().createNumber(value)
+ }
+
+ @JvmStatic
+ fun quickCreateBoolean(value: Boolean): BooleanNode {
+ return getInstance().createBoolean(value)
+ }
+
+ @JvmStatic
+ fun quickCreateVariable(name: String): VariableNode {
+ return getInstance().createVariable(name)
+ }
+ }
+
+ /**
+ * ์ซ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ๊ฐ์ธ ๊ฒฝ์ฐ
+ */
+ fun createNumber(value: Double): NumberNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateNumberCreation(value)
+
+ val node = NumberNode(value)
+
+ // ์์ฑ ํ ์ ํจ์ฑ ๊ฒ์ฆ
+ if (!validitySpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeValidationFailed(
+ reason = validitySpec.getWhyNotSatisfied(node)
+ )
+ }
+
+ createdNumberCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ๋ถ๋ฆฌ์ธ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๋ถ๋ฆฌ์ธ ๊ฐ
+ * @return BooleanNode ์ธ์คํด์ค
+ */
+ fun createBoolean(value: Boolean): BooleanNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateBooleanCreation(value)
+
+ val node = BooleanNode(value)
+
+ // ์์ฑ ํ ์ ํจ์ฑ ๊ฒ์ฆ
+ // ์ซ์/๋ถ๋ฆฌ์ธ/๋ณ์/์ฐ์ฐ/ํจ์ ํธ์ถ/์กฐ๊ฑด๋ฌธ/์ธ์ ๋ชฉ๋ก ๋
ธ๋ ์์ฑ ํ ์ ํจ์ฑ
+ if (!validitySpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeValidationFailed(
+ reason = validitySpec.getWhyNotSatisfied(node)
+ )
+ }
+
+ createdBooleanCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ๋ณ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ๋ณ์๋ช
+ * @return VariableNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ๋ณ์๋ช
์ธ ๊ฒฝ์ฐ
+ */
+ fun createVariable(name: String): VariableNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateVariableCreation(name)
+
+ val node = VariableNode(name)
+
+ // ์์ฑ ํ ์ ํจ์ฑ ๊ฒ์ฆ
+ if (!validitySpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeValidationFailed(
+ reason = validitySpec.getWhyNotSatisfied(node)
+ )
+ }
+
+ createdVariableCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ์ดํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return BinaryOpNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ์ฐ์ฐ์์ด๊ฑฐ๋ ํผ์ฐ์ฐ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createBinaryOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateBinaryOpCreation(left, operator, right)
+
+ val node = BinaryOpNode(left, operator, right)
+ validateNodeAfterBuild(node)
+
+ createdBinaryOpCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ๋จํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์
+ * @param operand ํผ์ฐ์ฐ์
+ * @return UnaryOpNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ์ฐ์ฐ์์ด๊ฑฐ๋ ํผ์ฐ์ฐ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createUnaryOp(operator: String, operand: ASTNode): UnaryOpNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateUnaryOpCreation(operator, operand)
+
+ val node = UnaryOpNode(operator, operand)
+ validateNodeAfterBuild(node)
+
+ createdUnaryOpCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param args ์ธ์ ๋ชฉ๋ก
+ * @return FunctionCallNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ํจ์๋ช
์ด๊ฑฐ๋ ์ธ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createFunctionCall(name: String, args: List): FunctionCallNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateFunctionCallCreation(name, args)
+
+ val node = FunctionCallNode(name, args)
+ validateNodeAfterBuild(node)
+
+ createdFunctionCallCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param condition ์กฐ๊ฑด์
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ * @return IfNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ์กฐ๊ฑด์ด๊ฑฐ๋ ๊ฐ์ธ ๊ฒฝ์ฐ
+ */
+ fun createIf(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateIfCreation(condition, trueValue, falseValue)
+
+ val node = IfNode(condition, trueValue, falseValue)
+ validateNodeAfterBuild(node)
+
+ createdIfCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param arguments ์ธ์ ๋ชฉ๋ก
+ * @return ArgumentsNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ์ธ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createArguments(arguments: List): ArgumentsNode {
+ // ์์ฑ ์ ์ ์ฑ
๊ฒ์ฆ
+ creationPolicy.validateArgumentsCreation(arguments)
+
+ val node = ArgumentsNode(arguments)
+
+ // ์์ฑ ํ ์ ํจ์ฑ ๊ฒ์ฆ
+ if (!validitySpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeValidationFailed(
+ reason = validitySpec.getWhyNotSatisfied(node)
+ )
+ }
+
+ createdArgumentsCount.incrementAndGet()
+
+ return node
+ }
+
+ /**
+ * ์ ์ ๊ฐ์ผ๋ก ์ซ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ ์ ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun createNumber(value: Int): NumberNode = createNumber(value.toDouble())
+
+ /**
+ * Long ๊ฐ์ผ๋ก ์ซ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value Long ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun createNumber(value: Long): NumberNode = createNumber(value.toDouble())
+
+ /**
+ * Float ๊ฐ์ผ๋ก ์ซ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value Float ๊ฐ
+ * @return NumberNode ์ธ์คํด์ค
+ */
+ fun createNumber(value: Float): NumberNode = createNumber(value.toDouble())
+
+ /**
+ * ๋ฌธ์์ด์์ ์ซ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๋ฌธ์์ด
+ * @return NumberNode ์ธ์คํด์ค
+ * @throws NumberFormatException ์ ํจํ์ง ์์ ์ซ์ ํ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createNumberFromString(value: String): NumberNode {
+ val doubleValue = value.toDoubleOrNull()
+ ?: throw ASTException.invalidNumberLiteral(value)
+ return createNumber(doubleValue)
+ }
+
+ /**
+ * ๋ฌธ์์ด์์ ๋ถ๋ฆฌ์ธ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param value ๋ถ๋ฆฌ์ธ ๋ฌธ์์ด
+ * @return BooleanNode ์ธ์คํด์ค
+ * @throws IllegalArgumentException ์ ํจํ์ง ์์ ๋ถ๋ฆฌ์ธ ํ์์ธ ๊ฒฝ์ฐ
+ */
+ fun createBooleanFromString(value: String): BooleanNode {
+ val booleanValue = when (value.lowercase()) {
+ "true", "1", "yes", "y", "on" -> true
+ "false", "0", "no", "n", "off" -> false
+ else -> throw ASTException.invalidBooleanValue(value)
+ }
+ return createBoolean(booleanValue)
+ }
+
+ /**
+ * ์ฐ์ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ์ฐ์ ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return BinaryOpNode ์ธ์คํด์ค
+ */
+ fun createArithmeticOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode {
+ if (operator !in ARITHMETIC_OPERATORS) {
+ throw ASTException.notArithmeticOperator(operator)
+ }
+ return createBinaryOp(left, operator, right)
+ }
+
+ /**
+ * ๋น๊ต ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ๋น๊ต ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return BinaryOpNode ์ธ์คํด์ค
+ */
+ fun createComparisonOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode {
+ if (operator !in COMPARISON_OPERATORS) {
+ throw ASTException.notComparisonOperator(operator)
+ }
+ return createBinaryOp(left, operator, right)
+ }
+
+ /**
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ๋
ผ๋ฆฌ ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return BinaryOpNode ์ธ์คํด์ค
+ */
+ fun createLogicalOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode {
+ if (operator !in LOGICAL_OPERATORS) {
+ throw ASTException.notLogicalOperator(operator)
+ }
+ return createBinaryOp(left, operator, right)
+ }
+
+ /**
+ * ๋จํญ ๋ง์ด๋์ค ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return UnaryOpNode ์ธ์คํด์ค
+ */
+ fun createUnaryMinus(operand: ASTNode): UnaryOpNode = createUnaryOp(MINUS, operand)
+
+ /**
+ * ๋จํญ ํ๋ฌ์ค ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return UnaryOpNode ์ธ์คํด์ค
+ */
+ fun createUnaryPlus(operand: ASTNode): UnaryOpNode = createUnaryOp(PLUS, operand)
+
+ /**
+ * ๋
ผ๋ฆฌ ๋ถ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operand ํผ์ฐ์ฐ์
+ * @return UnaryOpNode ์ธ์คํด์ค
+ */
+ fun createLogicalNot(operand: ASTNode): UnaryOpNode = createUnaryOp(EXCLAMATION, operand)
+
+ /**
+ * ์ํ ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param name ์ํ ํจ์๋ช
+ * @param args ์ธ์ ๋ชฉ๋ก
+ * @return FunctionCallNode ์ธ์คํด์ค
+ */
+ fun createMathFunction(name: String, args: List): FunctionCallNode {
+ if (!SUPPORTED_MATH_FUNCTIONS.contains(name.uppercase())) {
+ throw ASTException.unsupportedMathFunction(name)
+ }
+ return createFunctionCall(name.uppercase(), args)
+ }
+
+ /**
+ * ์ผํญ ์ฐ์ฐ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param condition ์กฐ๊ฑด์
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ * @return IfNode ์ธ์คํด์ค
+ */
+ fun createTernary(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode {
+ return createIf(condition, trueValue, falseValue)
+ }
+
+ /**
+ * ์ต์ ํ๋ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param node ์ต์ ํํ ๋
ธ๋
+ * @return ์ต์ ํ๋ ๋
ธ๋
+ */
+ fun createOptimized(node: ASTNode): ASTNode {
+ return when (node) {
+ is IfNode -> node.optimize()
+ is UnaryOpNode -> node.simplify()
+ else -> node
+ }
+ }
+
+ /**
+ * ํฉํ ๋ฆฌ ํต๊ณ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํฉํ ๋ฆฌ ์ฌ์ฉ ํต๊ณ
+ */
+ fun getFactoryStatistics(): Map {
+ return mapOf(
+ "totalNodesCreated" to createdNodeCount.get(),
+ "numberNodesCreated" to createdNumberCount.get(),
+ "booleanNodesCreated" to createdBooleanCount.get(),
+ "variableNodesCreated" to createdVariableCount.get(),
+ "binaryOpNodesCreated" to createdBinaryOpCount.get(),
+ "unaryOpNodesCreated" to createdUnaryOpCount.get(),
+ "functionCallNodesCreated" to createdFunctionCallCount.get(),
+ "ifNodesCreated" to createdIfCount.get(),
+ "argumentsNodesCreated" to createdArgumentsCount.get(),
+ "factoryComplexity" to Complexity.HIGH.name,
+ "cacheEnabled" to true
+ )
+ }
+
+
+ init {
+ createdNodeCount.incrementAndGet()
+ }
+
+ private fun validateNodeAfterBuild(node: ASTNode) {
+ // ์์ฑ ํ ์ ํจ์ฑ ๊ฒ์ฆ
+ if (!validitySpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeValidationFailed(
+ reason = validitySpec.getWhyNotSatisfied(node)
+ )
+ }
+
+ // ๊ตฌ์กฐ ๊ฒ์ฆ
+ if (!structureSpec.isSatisfiedBy(node)) {
+ throw ASTException.nodeStructureFailed(
+ reason = structureSpec.getWhyNotSatisfied(node)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt
new file mode 100644
index 00000000..45bb3a5f
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt
@@ -0,0 +1,79 @@
+package hs.kr.entrydsm.domain.ast.factory
+
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+
+/**
+ * AST ๋
ธ๋ ๋น๋์ ๊ธฐ๋ณธ ๊ณ์ฝ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * ํ์์์ ์์ฑ ๊ท์น(Production)์ ์ ์ฉํ ๋ ํด๋นํ๋ AST ๋
ธ๋๋ฅผ ๊ตฌ์ถํ๋
+ * ํฉํ ๋ฆฌ ๋ฉ์๋ ํจํด์ ์ ์ํฉ๋๋ค. ๊ฐ ์์ฑ ๊ท์น๋ง๋ค ๋์ํ๋ ๋น๋๊ฐ ์์ผ๋ฉฐ,
+ * ํ์ฑ ๊ณผ์ ์์ ์์ฑ๋ ์์ ์ฌ๋ณผ๋ค์ ์กฐํฉํ์ฌ ์ ์ ํ AST ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false)
+interface ASTBuilderContract {
+
+ /**
+ * ์์ ์ฌ๋ณผ๋ค๋ก๋ถํฐ AST ๋
ธ๋ ๋๋ ์ฌ๋ณผ ๋ฆฌ์คํธ๋ฅผ ๊ตฌ์ถํฉ๋๋ค.
+ *
+ * ํ์์ reduce ๋์์์ ํธ์ถ๋๋ฉฐ, ์์ฑ ๊ท์น์ ์ฐ๋ณ์ ํด๋นํ๋
+ * ์์ ์ฌ๋ณผ๋ค์ ๋ฐ์์ ์ข๋ณ์ ํด๋นํ๋ AST ๋
ธ๋๋ ์ฌ๋ณผ์ ์์ฑํฉ๋๋ค.
+ * ๋ฐํ ํ์
์ ์์ฑ ๊ท์น์ ๋ฐ๋ผ ASTNode ๋๋ List์
๋๋ค.
+ *
+ * @param children ์์ฑ ๊ท์น์ ์ฐ๋ณ์ ํด๋นํ๋ ์์ ์ฌ๋ณผ ๋ชฉ๋ก
+ * @return ๊ตฌ์ถ๋ AST ๋
ธ๋ ๋๋ ์ฌ๋ณผ ๋ฆฌ์คํธ
+ * @throws IllegalArgumentException ์์ ์ฌ๋ณผ์ ๊ฐ์๋ ํ์
์ด ์ฌ๋ฐ๋ฅด์ง ์์ ๊ฒฝ์ฐ
+ * @throws ClassCastException ์์ ์ฌ๋ณผ์ ํ์
๋ณํ์ ์คํจํ ๊ฒฝ์ฐ
+ */
+ fun build(children: List): Any
+
+ /**
+ * ๋น๋๊ฐ ์๊ตฌํ๋ ์ต์ ์์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ต์ ์์ ๊ฐ์
+ */
+ fun getMinimumChildrenCount(): Int = 0
+
+ /**
+ * ๋น๋๊ฐ ์ฒ๋ฆฌํ ์ ์๋ ์ต๋ ์์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ต๋ ์์ ๊ฐ์ (๋ฌด์ ํ์ธ ๊ฒฝ์ฐ -1)
+ */
+ fun getMaximumChildrenCount(): Int = -1
+
+ /**
+ * ์์ ์ฌ๋ณผ๋ค์ ์ ํจ์ฑ์ ๊ฒ์ฌํฉ๋๋ค.
+ *
+ * @param children ๊ฒ์ฌํ ์์ ์ฌ๋ณผ๋ค
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun validateChildren(children: List): Boolean {
+ val count = children.size
+ val minCount = getMinimumChildrenCount()
+ val maxCount = getMaximumChildrenCount()
+
+ if (count < minCount) return false
+ if (maxCount >= 0 && count > maxCount) return false
+
+ return true
+ }
+
+ /**
+ * ๋น๋์ ์ด๋ฆ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋น๋ ์ด๋ฆ
+ */
+ fun getBuilderName(): String = this::class.simpleName ?: "UnknownBuilder"
+
+ /**
+ * ๋น๋์ ์ค๋ช
์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋น๋ ์ค๋ช
+ */
+ fun getDescription(): String = "AST ๋
ธ๋๋ฅผ ๊ตฌ์ถํ๋ ๋น๋์
๋๋ค"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt
new file mode 100644
index 00000000..d2588d56
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt
@@ -0,0 +1,197 @@
+package hs.kr.entrydsm.domain.ast.factory
+
+import hs.kr.entrydsm.domain.ast.factory.builders.*
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+
+/**
+ * AST ๋น๋๋ค์ ๊ด๋ฆฌํ๋ ํฉํ ๋ฆฌ ๊ฐ์ฒด์
๋๋ค.
+ *
+ * ๋ชจ๋ ๋น๋๋ ๊ฐ๋ณ ํ์ผ๋ก ๋ถ๋ฆฌ๋์ด ๋จ์ผ ์ฑ
์ ์์น์ ์ค์ํ๋ฉฐ,
+ * ์ด ํด๋์ค๋ ๋น๋๋ค์ ์์ฑ๊ณผ ์ ๊ทผ์ ๋ด๋นํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.HIGH, cache = true)
+object ASTBuilders {
+
+ /**
+ * ํญ๋ฑ ๋น๋ - ์ฒซ ๋ฒ์งธ ์์์ ๊ทธ๋๋ก ๋ฐํํฉ๋๋ค.
+ */
+ val Identity = IdentityBuilder
+
+ /**
+ * ์์ ๋น๋ - ๋ฌธ๋ฒ์ ์์ ์ฌ๋ณผ์ฉ ๋น๋์
๋๋ค.
+ */
+ val Start = StartBuilder
+
+ /**
+ * ์ซ์ ๋น๋ - ์ซ์ ๋ฆฌํฐ๋ด ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val Number = NumberBuilder
+
+ /**
+ * ๋ณ์ ๋น๋ - ๋ณ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val Variable = VariableBuilder
+
+ /**
+ * TRUE ๋ถ๋ฆฐ ๋น๋ - true ๋ถ๋ฆฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val BooleanTrue = BooleanTrueBuilder
+
+ /**
+ * FALSE ๋ถ๋ฆฐ ๋น๋ - false ๋ถ๋ฆฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val BooleanFalse = BooleanFalseBuilder
+
+ /**
+ * ๊ดํธ ๋น๋ - ๊ดํธ๋ก ๋๋ฌ์ธ์ธ ํํ์์ ์ฒ๋ฆฌํฉ๋๋ค.
+ */
+ val Parenthesized = ParenthesizedBuilder
+
+ /**
+ * ํจ์ ํธ์ถ ๋น๋ - ์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val FunctionCall = FunctionCallBuilder
+
+ /**
+ * ๋น ํจ์ ํธ์ถ ๋น๋ - ์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val FunctionCallEmpty = FunctionCallEmptyBuilder
+
+ /**
+ * IF ์กฐ๊ฑด๋ฌธ ๋น๋ - IF ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ val If = IfBuilder
+
+ /**
+ * ๋จ์ผ ์ธ์ ๋น๋ - ๋จ์ผ ์ธ์ ๋ชฉ๋ก์ ์์ฑํฉ๋๋ค.
+ */
+ val ArgsSingle = ArgsSingleBuilder
+
+ /**
+ * ๋ค์ค ์ธ์ ๋น๋ - ๊ธฐ์กด ์ธ์ ๋ชฉ๋ก์ ์ ์ธ์๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+ */
+ val ArgsMultiple = ArgsMultipleBuilder
+
+ /**
+ * ์ดํญ ์ฐ์ฐ์ ๋น๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์ ๋ฌธ์์ด
+ * @param leftIndex ์ข์ธก ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @param rightIndex ์ฐ์ธก ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @return BinaryOp ๋น๋ ์ธ์คํด์ค
+ */
+ fun createBinaryOp(operator: String, leftIndex: Int = 0, rightIndex: Int = 2): BinaryOpBuilder =
+ BinaryOpBuilder(operator, leftIndex, rightIndex)
+
+ /**
+ * ๋จํญ ์ฐ์ฐ์ ๋น๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์ ๋ฌธ์์ด
+ * @param operandIndex ํผ์ฐ์ฐ์ ์ธ๋ฑ์ค
+ * @return UnaryOp ๋น๋ ์ธ์คํด์ค
+ */
+ fun createUnaryOp(operator: String, operandIndex: Int = 1): UnaryOpBuilder =
+ UnaryOpBuilder(operator, operandIndex)
+
+ /**
+ * POC ์ฝ๋์ 34๊ฐ ์์ฐ ๊ท์น์ ๋์ํ๋ ๋ชจ๋ AST ๋น๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ์์ฐ ๊ท์น ID -> ๋น๋ ์ธ์คํด์ค ๋งคํ
+ */
+ fun getProductionBuilders(): Map = mapOf(
+ // 0: EXPR โ EXPR || AND_EXPR
+ 0 to createBinaryOp("||", 0, 2),
+ // 1: EXPR โ AND_EXPR
+ 1 to Identity,
+ // 2: AND_EXPR โ AND_EXPR && COMP_EXPR
+ 2 to createBinaryOp("&&", 0, 2),
+ // 3: AND_EXPR โ COMP_EXPR
+ 3 to Identity,
+ // 4: COMP_EXPR โ COMP_EXPR == ARITH_EXPR
+ 4 to createBinaryOp("==", 0, 2),
+ // 5: COMP_EXPR โ COMP_EXPR != ARITH_EXPR
+ 5 to createBinaryOp("!=", 0, 2),
+ // 6: COMP_EXPR โ COMP_EXPR < ARITH_EXPR
+ 6 to createBinaryOp("<", 0, 2),
+ // 7: COMP_EXPR โ COMP_EXPR <= ARITH_EXPR
+ 7 to createBinaryOp("<=", 0, 2),
+ // 8: COMP_EXPR โ COMP_EXPR > ARITH_EXPR
+ 8 to createBinaryOp(">", 0, 2),
+ // 9: COMP_EXPR โ COMP_EXPR >= ARITH_EXPR
+ 9 to createBinaryOp(">=", 0, 2),
+ // 10: COMP_EXPR โ ARITH_EXPR
+ 10 to Identity,
+ // 11: ARITH_EXPR โ ARITH_EXPR + TERM
+ 11 to createBinaryOp("+", 0, 2),
+ // 12: ARITH_EXPR โ ARITH_EXPR - TERM
+ 12 to createBinaryOp("-", 0, 2),
+ // 13: ARITH_EXPR โ TERM
+ 13 to Identity,
+ // 14: TERM โ TERM * FACTOR
+ 14 to createBinaryOp("*", 0, 2),
+ // 15: TERM โ TERM / FACTOR
+ 15 to createBinaryOp("/", 0, 2),
+ // 16: TERM โ TERM % FACTOR
+ 16 to createBinaryOp("%", 0, 2),
+ // 17: TERM โ FACTOR
+ 17 to Identity,
+ // 18: FACTOR โ PRIMARY ^ FACTOR (์ฐ๊ฒฐํฉ)
+ 18 to createBinaryOp("^", 0, 2),
+ // 19: FACTOR โ PRIMARY
+ 19 to Identity,
+ // 20: PRIMARY โ ( EXPR )
+ 20 to Parenthesized,
+ // 21: PRIMARY โ - PRIMARY
+ 21 to createUnaryOp("-", 1),
+ // 22: PRIMARY โ + PRIMARY
+ 22 to createUnaryOp("+", 1),
+ // 23: PRIMARY โ ! PRIMARY
+ 23 to createUnaryOp("!", 1),
+ // 24: PRIMARY โ NUMBER
+ 24 to Number,
+ // 25: PRIMARY โ VARIABLE
+ 25 to Variable,
+ // 26: PRIMARY โ IDENTIFIER
+ 26 to Variable,
+ // 27: PRIMARY โ TRUE
+ 27 to BooleanTrue,
+ // 28: PRIMARY โ FALSE
+ 28 to BooleanFalse,
+ // 29: PRIMARY โ IDENTIFIER ( ARGS )
+ 29 to FunctionCall,
+ // 30: PRIMARY โ IDENTIFIER ( )
+ 30 to FunctionCallEmpty,
+ // 31: PRIMARY โ IF ( EXPR , EXPR , EXPR )
+ 31 to If,
+ // 32: ARGS โ EXPR
+ 32 to ArgsSingle,
+ // 33: ARGS โ ARGS , EXPR
+ 33 to ArgsMultiple
+ )
+
+ /**
+ * ๋ชจ๋ ๋น๋์ ์ธ์คํด์ค๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋น๋ ์ธ์คํด์ค๋ค์ ๋งต (์ด๋ฆ -> ์ธ์คํด์ค)
+ */
+ fun getAllBuilders(): Map = mapOf(
+ "Identity" to Identity,
+ "Start" to Start,
+ "Number" to Number,
+ "Variable" to Variable,
+ "BooleanTrue" to BooleanTrue,
+ "BooleanFalse" to BooleanFalse,
+ "Parenthesized" to Parenthesized,
+ "FunctionCall" to FunctionCall,
+ "FunctionCallEmpty" to FunctionCallEmpty,
+ "If" to If,
+ "ArgsSingle" to ArgsSingle,
+ "ArgsMultiple" to ArgsMultiple
+ )
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt
new file mode 100644
index 00000000..bc00f3cc
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt
@@ -0,0 +1,47 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ๋ค์ค ์ธ์ ๋น๋ - ๊ธฐ์กด ์ธ์ ๋ชฉ๋ก์ ์ ์ธ์๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+ *
+ * ๊ธฐ์กด ์ธ์ ๋ชฉ๋ก๊ณผ ์๋ก์ด ์ธ์๋ฅผ ๊ฒฐํฉํ์ฌ ํ์ฅ๋ ์ธ์ ๋ชฉ๋ก์ ์์ฑํฉ๋๋ค.
+ * ์: ARGS , EXPR -> Args
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Policy(
+ name = "Multiple Arguments Policy",
+ description = "๋ค์ค ์ธ์๋ ๊ธฐ์กด ์ธ์ ๋ชฉ๋ก๊ณผ ์๋ก์ด ์ธ์๋ฅผ ์ผํ๋ก ๊ตฌ๋ถํ์ฌ ๊ฒฐํฉํด์ผ ํจ",
+ domain = "ast",
+ scope = Scope.AGGREGATE
+)
+object ArgsMultipleBuilder : ASTBuilderContract {
+ override fun build(children: List): List {
+ if (children.size != 3) {
+ throw ASTException.argsMultipleChildrenMismatch(actual = children.size)
+ }
+ @Suppress("UNCHECKED_CAST")
+ val existingArgs = children[0] as List
+ val newArg = children[2] as ASTNode
+
+ return existingArgs + newArg
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 3 &&
+ children[0] is List<*> &&
+ (children[0] as List<*>).all { it is ASTNode } &&
+ children[2] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "ArgsMultiple"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt
new file mode 100644
index 00000000..6b8c4af5
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt
@@ -0,0 +1,40 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * ๋จ์ผ ์ธ์ ๋น๋ - ๋จ์ผ ์ธ์ ๋ชฉ๋ก์ ์์ฑํฉ๋๋ค.
+ *
+ * ํ๋์ ํํ์์ ์ธ์ ๋ชฉ๋ก์ผ๋ก ๋ณํํฉ๋๋ค.
+ * ์: EXPR -> Args
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Single Argument Specification",
+ description = "๋จ์ผ ์ธ์๋ ํ๋์ ์ ํจํ ํํ์์ด์ด์ผ ํจ",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+object ArgsSingleBuilder : ASTBuilderContract {
+ override fun build(children: List): List {
+ if (children.size != 1) {
+ throw ASTException.argsSingleChildMismatch(actual = children.size)
+ }
+ return listOf(children[0] as ASTNode)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 1 && children[0] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "ArgsSingle"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt
new file mode 100644
index 00000000..773f96d8
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt
@@ -0,0 +1,72 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ์ดํญ ์ฐ์ฐ์ ๋น๋ - ์ดํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @property operator ์ฐ์ฐ์ ๋ฌธ์์ด
+ * @property leftIndex ์ข์ธก ํผ์ฐ์ฐ์์ ์ธ๋ฑ์ค (๊ธฐ๋ณธ๊ฐ: 0)
+ * @property rightIndex ์ฐ์ธก ํผ์ฐ์ฐ์์ ์ธ๋ฑ์ค (๊ธฐ๋ณธ๊ฐ: 2)
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false)
+@Policy(
+ name = "Binary Operator Policy",
+ description = "์ดํญ ์ฐ์ฐ์๋ ์ ํํ ๋ ๊ฐ์ ํผ์ฐ์ฐ์๋ฅผ ๊ฐ์ ธ์ผ ํ๋ฉฐ, ์ฐ์ฐ์ ์ฐ์ ์์๋ฅผ ์ค์ํด์ผ ํจ",
+ domain = "ast",
+ scope = Scope.AGGREGATE
+)
+class BinaryOpBuilder(
+ private val operator: String,
+ private val leftIndex: Int = 0,
+ private val rightIndex: Int = 2
+) : ASTBuilderContract {
+
+ override fun build(children: List): BinaryOpNode {
+ val required = maxOf(leftIndex, rightIndex) + 1
+ if (children.size < required) {
+ throw ASTException.binaryChildrenInsufficient(
+ required = required,
+ actual = children.size,
+ leftIndex = leftIndex,
+ rightIndex = rightIndex
+ )
+ }
+
+ val left = children[leftIndex]
+ if (left !is ASTNode) {
+ throw ASTException.operandNotAst(
+ side = "left",
+ actualType = left::class.simpleName
+ )
+ }
+
+ val right = children[rightIndex]
+ if (right !is ASTNode) {
+ throw ASTException.operandNotAst(
+ side = "right",
+ actualType = right::class.simpleName
+ )
+ }
+
+ return BinaryOpNode(left, operator, right)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size >= maxOf(leftIndex, rightIndex) + 1 &&
+ leftIndex < children.size && children[leftIndex] is ASTNode &&
+ rightIndex < children.size && children[rightIndex] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "BinaryOp($operator)"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt
new file mode 100644
index 00000000..3d6d1ca9
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt
@@ -0,0 +1,33 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.BooleanNode
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * FALSE ๋ถ๋ฆฐ ๋น๋ - false ๋ถ๋ฆฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Boolean False Specification",
+ description = "false ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ ํญ์ ๊ฑฐ์ง ๊ฐ์ ๋ํ๋ด์ผ ํจ",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+object BooleanFalseBuilder : ASTBuilderContract {
+ override fun build(children: List): BooleanNode {
+ return BooleanNode.FALSE
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return true // ์์ ์์๊ฐ ํ์ํ์ง ์์
+ }
+
+ override fun getBuilderName(): String = "BooleanFalse"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt
new file mode 100644
index 00000000..df4b0212
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt
@@ -0,0 +1,33 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.BooleanNode
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * TRUE ๋ถ๋ฆฐ ๋น๋ - true ๋ถ๋ฆฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Boolean True Specification",
+ description = "true ๋ถ๋ฆฐ ๋ฆฌํฐ๋ด์ ํญ์ ์ฐธ ๊ฐ์ ๋ํ๋ด์ผ ํจ",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+object BooleanTrueBuilder : ASTBuilderContract {
+ override fun build(children: List): BooleanNode {
+ return BooleanNode.TRUE
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return true // ์์ ์์๊ฐ ํ์ํ์ง ์์
+ }
+
+ override fun getBuilderName(): String = "BooleanTrue"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt
new file mode 100644
index 00000000..fa748840
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt
@@ -0,0 +1,59 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.domain.lexer.entities.Token
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ํจ์ ํธ์ถ ๋น๋ - ์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * ํจ์๋ช
๊ณผ ์ธ์ ๋ชฉ๋ก์ ๋ฐ์์ FunctionCallNode๋ฅผ ์์ฑํฉ๋๋ค.
+ * ์: IDENTIFIER ( ARGS ) -> FunctionCall
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false)
+@Policy(
+ name = "Function Call Policy",
+ description = "ํจ์ ํธ์ถ์ ์ ํจํ ํจ์๋ช
๊ณผ ์ ์ ํ ์ธ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ผ ํจ",
+ domain = "ast",
+ scope = Scope.AGGREGATE
+)
+object FunctionCallBuilder : ASTBuilderContract {
+ override fun build(children: List): FunctionCallNode {
+ if (children.size != 3) {
+ throw ASTException.functionCallChildrenMismatch(actual = children.size)
+ }
+ if (children[0] !is Token) {
+ throw ASTException.functionCallFirstNotToken(children[0]::class.simpleName)
+ }
+ if (children[2] !is List<*>) {
+ throw ASTException.functionCallThirdNotList(children[2]::class.simpleName)
+ }
+ if (!(children[2] as List<*>).all { it is ASTNode }) {
+ throw ASTException.functionCallArgsNotAstNode()
+ }
+
+
+ val nameToken = children[0] as Token
+ val args = children[2] as List
+
+ return FunctionCallNode(nameToken.value, args)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 3 &&
+ children[0] is Token &&
+ children[2] is List<*> &&
+ (children[2] as List<*>).all { it is ASTNode }
+ }
+
+ override fun getBuilderName(): String = "FunctionCall"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt
new file mode 100644
index 00000000..91ad437d
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt
@@ -0,0 +1,56 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.domain.lexer.entities.Token
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * ๋น ํจ์ ํธ์ถ ๋น๋ - ์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * ํจ์๋ช
๋ง ๋ฐ์์ ๋น ์ธ์ ๋ชฉ๋ก์ ๊ฐ์ง FunctionCallNode๋ฅผ ์์ฑํฉ๋๋ค.
+ * ์: IDENTIFIER ( ) -> FunctionCall
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Empty Function Call Specification",
+ description = "์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ์ ํจ์๋ช
๊ณผ ๋น ๊ดํธ๋ก ๊ตฌ์ฑ๋์ด์ผ ํจ",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+object FunctionCallEmptyBuilder : ASTBuilderContract {
+ override fun build(children: List): FunctionCallNode {
+ if (children.size != 3) {
+ throw ASTException.functionCallEmptyChildrenMismatch(actual = children.size)
+ }
+ if (children[0] !is Token) {
+ throw ASTException.functionCallEmptyFirstNotToken(children[0]::class.simpleName)
+ }
+ if (children[1] !is Token) {
+ throw ASTException.functionCallEmptySecondNotToken(children[1]::class.simpleName)
+ }
+ if (children[2] !is Token) {
+ throw ASTException.functionCallEmptyThirdNotToken(children[2]::class.simpleName)
+ }
+
+
+ val nameToken = children[0] as Token
+ return FunctionCallNode(nameToken.value, emptyList())
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 3 &&
+ children[0] is Token && // IDENTIFIER
+ children[1] is Token && // LEFT_PAREN
+ children[2] is Token // RIGHT_PAREN
+ }
+
+ override fun getBuilderName(): String = "FunctionCallEmpty"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt
new file mode 100644
index 00000000..9e901569
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt
@@ -0,0 +1,44 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * ํญ๋ฑ ๋น๋ - ์ฒซ ๋ฒ์งธ ์์์ ๊ทธ๋๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * ์ฃผ๋ก ๋จ์ผ ํญ๋ชฉ์ ๊ฐ์ธ๋ ์์ฑ ๊ท์น์์ ์ฌ์ฉ๋ฉ๋๋ค.
+ * ์: EXPR -> AND_EXPR
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Identity Build Rule",
+ description = "๋จ์ผ ์์ ๋
ธ๋๋ฅผ ๊ทธ๋๋ก ๋ฐํํ๋ ํญ๋ฑ ๋ณํ ๊ท์น",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+object IdentityBuilder : ASTBuilderContract {
+ override fun build(children: List): ASTNode {
+ if (children.isEmpty()) {
+ throw ASTException.identityChildrenEmpty(actual = children.size)
+ }
+ if (children[0] !is ASTNode) {
+ throw ASTException.identityFirstNotAstNode(children[0]::class.simpleName)
+ }
+
+ return children[0] as ASTNode
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 1 && children[0] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "Identity"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt
new file mode 100644
index 00000000..0f6219fb
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt
@@ -0,0 +1,48 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.entities.IfNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * IF ์กฐ๊ฑด๋ฌธ ๋น๋ - IF ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * IF(์กฐ๊ฑด, ์ฐธ๊ฐ, ๊ฑฐ์ง๊ฐ) ํํ์ ์กฐ๊ฑด๋ฌธ์ ์ฒ๋ฆฌํฉ๋๋ค.
+ * ์: IF ( EXPR , EXPR , EXPR ) -> If
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.HIGH, cache = false)
+@Policy(
+ name = "Conditional Expression Policy",
+ description = "IF ์กฐ๊ฑด๋ฌธ์ ์กฐ๊ฑด์, ์ฐธ๊ฐ, ๊ฑฐ์ง๊ฐ์ ๋ชจ๋ ๊ฐ์ ธ์ผ ํ๋ฉฐ ์ ์ ํ ํ์์ด์ด์ผ ํจ",
+ domain = "ast",
+ scope = Scope.AGGREGATE
+)
+object IfBuilder : ASTBuilderContract {
+ override fun build(children: List): IfNode {
+ if (children.size != 8) {
+ throw ASTException.ifChildrenMismatch(8, children.size)
+ }
+ val condition = children[2] as ASTNode
+ val trueValue = children[4] as ASTNode
+ val falseValue = children[6] as ASTNode
+
+ return IfNode(condition, trueValue, falseValue)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 8 &&
+ children[2] is ASTNode &&
+ children[4] is ASTNode &&
+ children[6] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "If"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt
new file mode 100644
index 00000000..dee8df25
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt
@@ -0,0 +1,45 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.NumberNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.domain.lexer.entities.Token
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * ์ซ์ ๋น๋ - ์ซ์ ๋ฆฌํฐ๋ด ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * ํ ํฐ์ ๊ฐ์ Double๋ก ๋ณํํ์ฌ NumberNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Specification(
+ name = "Number Literal Specification",
+ description = "์ซ์ ๋ฆฌํฐ๋ด์ ์ ํจํ ์ซ์ ํ์์ด์ด์ผ ํ๋ฉฐ Double๋ก ๋ณํ ๊ฐ๋ฅํด์ผ ํจ",
+ domain = "ast",
+ priority = Priority.HIGH
+)
+object NumberBuilder : ASTBuilderContract {
+ override fun build(children: List): NumberNode {
+ if (children.size != 1) {
+ throw ASTException.numberChildrenMismatch(1, children.size)
+ }
+ val token = children[0] as Token
+ val value = token.value.toDoubleOrNull()
+ ?: throw ASTException.invalidNumberLiteral(token.value)
+
+ return NumberNode(value)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 1 && children[0] is Token &&
+ (children[0] as Token).value.toDoubleOrNull() != null
+ }
+
+ override fun getBuilderName(): String = "Number"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt
new file mode 100644
index 00000000..5f6b410b
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt
@@ -0,0 +1,44 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ๊ดํธ ๋น๋ - ๊ดํธ๋ก ๋๋ฌ์ธ์ธ ํํ์์ ์ฒ๋ฆฌํฉ๋๋ค.
+ *
+ * ๊ดํธ ์์ ํํ์์ ์ถ์ถํ์ฌ ๋ฐํํฉ๋๋ค.
+ * ์: ( EXPR ) -> EXPR
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Policy(
+ name = "Parenthesized Expression Policy",
+ description = "๊ดํธ๋ก ๋๋ฌ์ธ์ธ ํํ์์ ์ข๊ดํธ, ํํ์, ์ฐ๊ดํธ ์์๋ก ๊ตฌ์ฑ๋์ด์ผ ํจ",
+ domain = "ast",
+ scope = Scope.AGGREGATE
+)
+object ParenthesizedBuilder : ASTBuilderContract {
+ override fun build(children: List): ASTNode {
+ if (children.size != 3) {
+ throw ASTException.parenthesizedChildrenMismatch(3, children.size)
+ }
+ if (children[1] !is ASTNode) {
+ throw ASTException.parenthesizedSecondNotAst(children[1]::class.simpleName)
+ }
+
+ return children[1] as ASTNode
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 3 && children[1] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "Parenthesized"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt
new file mode 100644
index 00000000..f5caba92
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt
@@ -0,0 +1,44 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ์์ ๋น๋ - ๋ฌธ๋ฒ์ ์์ ์ฌ๋ณผ์ฉ ๋น๋์
๋๋ค.
+ *
+ * ํ์ฅ๋ ๋ฌธ๋ฒ์ ์์ ๊ท์น์์ ์ฌ์ฉ๋ฉ๋๋ค.
+ * ์: START -> EXPR
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Policy(
+ name = "Start Symbol Policy",
+ description = "๋ฌธ๋ฒ์ ์์ ์ฌ๋ณผ์ ์ ํํ ํ๋์ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ ธ์ผ ํจ",
+ domain = "ast",
+ scope = Scope.DOMAIN
+)
+object StartBuilder : ASTBuilderContract {
+ override fun build(children: List): ASTNode {
+ if (children.size != 1) {
+ throw ASTException.startChildrenMismatch(1, children.size)
+ }
+ if (children[0] !is ASTNode) {
+ throw ASTException.startFirstNotAst(children[0]::class.simpleName)
+ }
+
+ return children[0] as ASTNode
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 1 && children[0] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "Start"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt
new file mode 100644
index 00000000..4083a7c1
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt
@@ -0,0 +1,49 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * ๋จํญ ์ฐ์ฐ์ ๋น๋ - ๋จํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @property operator ์ฐ์ฐ์ ๋ฌธ์์ด
+ * @property operandIndex ํผ์ฐ์ฐ์์ ์ธ๋ฑ์ค (๊ธฐ๋ณธ๊ฐ: 1)
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false)
+@Specification(
+ name = "Unary Operator Specification",
+ description = "๋จํญ ์ฐ์ฐ์๋ ์ ํํ ํ๋์ ํผ์ฐ์ฐ์๋ฅผ ๊ฐ์ ธ์ผ ํจ",
+ domain = "ast",
+ priority = Priority.HIGH
+)
+class UnaryOpBuilder(
+ private val operator: String,
+ private val operandIndex: Int = 1
+) : ASTBuilderContract {
+
+ override fun build(children: List): UnaryOpNode {
+ val required = operandIndex + 1
+ if (children.size < required) {
+ throw ASTException.unaryChildrenInsufficient(required, children.size, operandIndex)
+ }
+
+ val operand = children[operandIndex] as ASTNode
+ return UnaryOpNode(operator, operand)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size >= operandIndex + 1 &&
+ operandIndex < children.size && children[operandIndex] is ASTNode
+ }
+
+ override fun getBuilderName(): String = "UnaryOp($operator)"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt
new file mode 100644
index 00000000..eb420f53
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt
@@ -0,0 +1,46 @@
+package hs.kr.entrydsm.domain.ast.factory.builders
+
+import hs.kr.entrydsm.domain.ast.entities.VariableNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract
+import hs.kr.entrydsm.domain.lexer.entities.Token
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * ๋ณ์ ๋น๋ - ๋ณ์ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * ํ ํฐ์ ๊ฐ์ ๋ณ์๋ช
์ผ๋ก ์ฌ์ฉํ์ฌ VariableNode๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.07.31
+ */
+@Factory(context = "ast", complexity = Complexity.LOW, cache = false)
+@Policy(
+ name = "Variable Naming Policy",
+ description = "๋ณ์๋ช
์ ์ ํจํ ์๋ณ์ ํ์์ด์ด์ผ ํ๋ฉฐ ์์ฝ์ด๊ฐ ์๋์ด์ผ ํจ",
+ domain = "ast",
+ scope = Scope.ENTITY
+)
+object VariableBuilder : ASTBuilderContract {
+ override fun build(children: List): VariableNode {
+ if (children.size != 1) {
+ throw ASTException.variableChildrenMismatch(1, children.size)
+ }
+ if (children[0] !is Token) {
+ throw ASTException.variableFirstNotToken(children[0]::class.simpleName)
+ }
+
+
+ val token = children[0] as Token
+ return VariableNode(token.value)
+ }
+
+ override fun validateChildren(children: List): Boolean {
+ return children.size == 1 && children[0] is Token
+ }
+
+ override fun getBuilderName(): String = "Variable"
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt
new file mode 100644
index 00000000..4170f6a8
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt
@@ -0,0 +1,88 @@
+package hs.kr.entrydsm.domain.ast.interfaces
+
+import hs.kr.entrydsm.domain.ast.entities.NumberNode
+import hs.kr.entrydsm.domain.ast.entities.BooleanNode
+import hs.kr.entrydsm.domain.ast.entities.VariableNode
+import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.entities.IfNode
+import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode
+
+/**
+ * AST ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํ๊ธฐ ์ํ ์ธํฐํ์ด์ค์
๋๋ค (Visitor ํจํด).
+ *
+ * ๊ฐ ๋
ธ๋ ํ์
์ ๋ํ ๋ฐฉ๋ฌธ ๋ฉ์๋๋ฅผ ์ ์ํ๋ฉฐ, ๋ค์ํ AST ์ฒ๋ฆฌ ๋ก์ง์
+ * ๋
ธ๋ ํด๋์ค์ ๋ถ๋ฆฌํ์ฌ ๊ตฌํํ ์ ์๊ฒ ํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+interface ASTVisitor {
+
+ /**
+ * ์ซ์ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ์ซ์ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitNumber(node: NumberNode): T
+
+ /**
+ * ๋ถ๋ฆฌ์ธ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ๋ถ๋ฆฌ์ธ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitBoolean(node: BooleanNode): T
+
+ /**
+ * ๋ณ์ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ๋ณ์ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitVariable(node: VariableNode): T
+
+ /**
+ * ์ดํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ์ดํญ ์ฐ์ฐ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitBinaryOp(node: BinaryOpNode): T
+
+ /**
+ * ๋จํญ ์ฐ์ฐ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ๋จํญ ์ฐ์ฐ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitUnaryOp(node: UnaryOpNode): T
+
+ /**
+ * ํจ์ ํธ์ถ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ํจ์ ํธ์ถ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitFunctionCall(node: FunctionCallNode): T
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ์กฐ๊ฑด๋ฌธ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitIf(node: IfNode): T
+
+ /**
+ * ์ธ์ ๋
ธ๋๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค.
+ *
+ * @param node ๋ฐฉ๋ฌธํ ์ธ์ ๋
ธ๋
+ * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ
+ */
+ fun visitArguments(node: ArgumentsNode): T
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt
new file mode 100644
index 00000000..dccee9b2
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt
@@ -0,0 +1,430 @@
+package hs.kr.entrydsm.domain.ast.policies
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.utils.ASTValidationUtils
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.PolicyResult
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+
+/**
+ * AST ๋
ธ๋ ์ ํจ์ฑ ๊ฒ์ฆ ์ ์ฑ
์ ๊ตฌํํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * AST ๋
ธ๋์ ์์ฑ๊ณผ ์กฐ์์ ๋ํ ๋น์ฆ๋์ค ๊ท์น์ ์ ์ํ๊ณ ๊ฒ์ฆํ๋ฉฐ,
+ * ๋๋ฉ์ธ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Policy(
+ name = "AST ๋
ธ๋ ์ ํจ์ฑ ๊ฒ์ฆ ์ ์ฑ
",
+ description = "AST ๋
ธ๋์ ์์ฑ๊ณผ ์กฐ์์ ๋ํ ๋น์ฆ๋์ค ๊ท์น์ ์ ์ํ๊ณ ๊ฒ์ฆ",
+ domain = "ast",
+ scope = Scope.DOMAIN
+)
+class ASTValidationPolicy {
+
+ object ErrorMessages {
+ // ์ซ์ ๋
ธ๋ ๊ด๋ จ ๋ฉ์์ง
+ const val NUMBER_NOT_FINITE = "์ซ์ ๊ฐ์ ์ ํํด์ผ ํฉ๋๋ค"
+ const val NUMBER_IS_NAN = "์ซ์ ๊ฐ์ NaN์ด ๋ ์ ์์ต๋๋ค"
+ const val NUMBER_EXCEEDS_MAX = "์ซ์ ๊ฐ์ด ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ const val NUMBER_BELOW_MIN = "์ซ์ ๊ฐ์ด ์ต์๊ฐ์ ๋ฏธ๋ง์
๋๋ค"
+
+ // ๋ณ์ ๋
ธ๋ ๊ด๋ จ ๋ฉ์์ง
+ const val VARIABLE_NAME_BLANK = "๋ณ์๋ช
์ ๋น์ด์์ ์ ์์ต๋๋ค"
+ const val VARIABLE_NAME_TOO_LONG = "๋ณ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํฉ๋๋ค"
+ const val VARIABLE_NAME_INVALID = "์ ํจํ์ง ์์ ๋ณ์๋ช
์
๋๋ค"
+ const val VARIABLE_NAME_RESERVED = "์์ฝ์ด๋ ๋ณ์๋ช
์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค"
+
+ // ์ฐ์ฐ์ ๊ด๋ จ ๋ฉ์์ง
+ const val OPERATOR_BLANK = "์ฐ์ฐ์๋ ๋น์ด์์ ์ ์์ต๋๋ค"
+ const val BINARY_OPERATOR_UNSUPPORTED = "์ง์๋์ง ์๋ ์ดํญ ์ฐ์ฐ์์
๋๋ค"
+ const val UNARY_OPERATOR_UNSUPPORTED = "์ง์๋์ง ์๋ ๋จํญ ์ฐ์ฐ์์
๋๋ค"
+ const val DIVISION_BY_ZERO = "0์ผ๋ก ๋๋ ์ ์์ต๋๋ค"
+ const val MODULO_BY_ZERO = "0์ผ๋ก ๋๋ ๋๋จธ์ง๋ฅผ ๊ตฌํ ์ ์์ต๋๋ค"
+
+ // ํผ์ฐ์ฐ์ ๊ด๋ จ ๋ฉ์์ง
+ const val LEFT_OPERAND_INVALID = "์ข์ธก ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+ const val RIGHT_OPERAND_INVALID = "์ฐ์ธก ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+ const val OPERAND_INVALID = "ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ // ํจ์ ๊ด๋ จ ๋ฉ์์ง
+ const val FUNCTION_NAME_BLANK = "ํจ์๋ช
์ ๋น์ด์์ ์ ์์ต๋๋ค"
+ const val FUNCTION_NAME_TOO_LONG = "ํจ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํฉ๋๋ค"
+ const val FUNCTION_NAME_INVALID = "์ ํจํ์ง ์์ ํจ์๋ช
์
๋๋ค"
+ const val FUNCTION_ARGS_EXCEED_MAX = "ํจ์ ์ธ์ ๊ฐ์๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ const val FUNCTION_ARG_INVALID = "์ธ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ // ์กฐ๊ฑด๋ฌธ ๊ด๋ จ ๋ฉ์์ง
+ const val CONDITION_INVALID = "์กฐ๊ฑด์์ด ์ ํจํ์ง ์์ต๋๋ค"
+ const val TRUE_VALUE_INVALID = "์ฐธ ๊ฐ์ด ์ ํจํ์ง ์์ต๋๋ค"
+ const val FALSE_VALUE_INVALID = "๊ฑฐ์ง ๊ฐ์ด ์ ํจํ์ง ์์ต๋๋ค"
+ const val NESTING_DEPTH_EXCEEDED = "์ค์ฒฉ ๊น์ด๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+
+ // ์ธ์ ๊ด๋ จ ๋ฉ์์ง
+ const val ARGUMENTS_COUNT_EXCEEDED = "์ธ์ ๊ฐ์๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ const val ARGUMENT_INVALID = "์ธ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ // ๋
ธ๋ ์ผ๋ฐ ๊ฒ์ฆ ๋ฉ์์ง
+ const val NODE_SIZE_EXCEEDED = "๋
ธ๋ ํฌ๊ธฐ๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ const val NODE_DEPTH_EXCEEDED = "๋
ธ๋ ๊น์ด๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ const val VARIABLES_PER_NODE_EXCEEDED = "๋
ธ๋๋น ๋ณ์ ๊ฐ์๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค"
+ }
+
+ /**
+ * ์ซ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๊ฐ
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateNumberCreation(value: Double): PolicyResult {
+ val violations = mutableListOf()
+
+ if (!value.isFinite()) {
+ violations.add("${ErrorMessages.NUMBER_NOT_FINITE}: $value")
+ }
+
+ if (value.isNaN()) {
+ violations.add(ErrorMessages.NUMBER_IS_NAN)
+ }
+
+ // ๋๋ฌด ํฐ ๊ฐ ๊ฒ์ฆ
+ if (value > MAX_NUMBER_VALUE) {
+ violations.add("${ErrorMessages.NUMBER_EXCEEDS_MAX}: $value > $MAX_NUMBER_VALUE")
+ }
+
+ if (value < MIN_NUMBER_VALUE) {
+ violations.add("${ErrorMessages.NUMBER_BELOW_MIN}: $value < $MIN_NUMBER_VALUE")
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to NUMBER_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ๋ถ๋ฆฌ์ธ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param value ๋ถ๋ฆฌ์ธ ๊ฐ
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateBooleanCreation(value: Boolean): PolicyResult {
+ // ๋ถ๋ฆฌ์ธ ๊ฐ์ ํญ์ ์ ํจ
+ return PolicyResult(
+ success = true,
+ message = "",
+ data = mapOf("policyName" to BOOLEAN_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ๋ณ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ๋ณ์๋ช
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateVariableCreation(name: String): PolicyResult {
+ val violations = mutableListOf()
+
+ if (name.isBlank()) {
+ violations.add(ErrorMessages.VARIABLE_NAME_BLANK)
+ }
+
+ if (name.length > MAX_VARIABLE_NAME_LENGTH) {
+ violations.add("${ErrorMessages.VARIABLE_NAME_TOO_LONG}: ${name.length} > $MAX_VARIABLE_NAME_LENGTH")
+ }
+
+ if (!isValidVariableName(name)) {
+ violations.add("${ErrorMessages.VARIABLE_NAME_INVALID}: $name")
+ }
+
+ if (isReservedWord(name)) {
+ violations.add("${ErrorMessages.VARIABLE_NAME_RESERVED}: $name")
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to VARIABLE_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ์ดํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateBinaryOpCreation(left: ASTNode, operator: String, right: ASTNode): PolicyResult {
+ val violations = mutableListOf()
+
+ if (operator.isBlank()) {
+ violations.add(ErrorMessages.OPERATOR_BLANK)
+ }
+
+ if (!isSupportedBinaryOperator(operator)) {
+ violations.add("${ErrorMessages.BINARY_OPERATOR_UNSUPPORTED}: $operator")
+ }
+
+ // ํผ์ฐ์ฐ์ ๊ฒ์ฆ
+ val leftValidation = validateNode(left)
+ if (!leftValidation.success) {
+ violations.add("${ErrorMessages.LEFT_OPERAND_INVALID}: ${leftValidation.message}")
+ }
+
+ val rightValidation = validateNode(right)
+ if (!rightValidation.success) {
+ violations.add("${ErrorMessages.RIGHT_OPERAND_INVALID}: ${rightValidation.message}")
+ }
+
+ // ์ฐ์ฐ์๋ณ ํน๋ณ ๊ฒ์ฆ
+ when (operator) {
+ "/" -> {
+ if (isZeroConstant(right)) {
+ violations.add(ErrorMessages.DIVISION_BY_ZERO)
+ }
+ }
+ "%" -> {
+ if (isZeroConstant(right)) {
+ violations.add(ErrorMessages.MODULO_BY_ZERO)
+ }
+ }
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to BINARY_OPERATION_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ๋จํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์
+ * @param operand ํผ์ฐ์ฐ์
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateUnaryOpCreation(operator: String, operand: ASTNode): PolicyResult {
+ val violations = mutableListOf()
+
+ if (operator.isBlank()) {
+ violations.add(ErrorMessages.OPERATOR_BLANK)
+ }
+
+ if (!isSupportedUnaryOperator(operator)) {
+ violations.add("${ErrorMessages.UNARY_OPERATOR_UNSUPPORTED}: $operator")
+ }
+
+ // ํผ์ฐ์ฐ์ ๊ฒ์ฆ
+ val operandValidation = validateNode(operand)
+ if (!operandValidation.success) {
+ violations.add("${ErrorMessages.OPERAND_INVALID}: ${operandValidation.message}")
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to UNARY_OPERATION_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ํจ์ ํธ์ถ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param args ์ธ์ ๋ชฉ๋ก
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateFunctionCallCreation(name: String, args: List): PolicyResult {
+ val violations = mutableListOf()
+
+ if (name.isBlank()) {
+ violations.add(ErrorMessages.FUNCTION_NAME_BLANK)
+ }
+
+ if (name.length > MAX_FUNCTION_NAME_LENGTH) {
+ violations.add("${ErrorMessages.FUNCTION_NAME_TOO_LONG}: ${name.length} > $MAX_FUNCTION_NAME_LENGTH")
+ }
+
+ if (!isValidFunctionName(name)) {
+ violations.add("${ErrorMessages.FUNCTION_NAME_INVALID}: $name")
+ }
+
+ if (args.size > MAX_FUNCTION_ARGS) {
+ violations.add("${ErrorMessages.FUNCTION_ARGS_EXCEED_MAX}: ${args.size} > $MAX_FUNCTION_ARGS")
+ }
+
+ // ๊ฐ ์ธ์ ๊ฒ์ฆ
+ args.forEachIndexed { index, arg ->
+ val argValidation = validateNode(arg)
+ if (!argValidation.success) {
+ violations.add("${ErrorMessages.FUNCTION_ARG_INVALID} $index: ${argValidation.message}")
+ }
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to FUNCTION_CALL_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param condition ์กฐ๊ฑด์
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateIfCreation(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): PolicyResult {
+ val violations = mutableListOf()
+
+ // ์กฐ๊ฑด์ ๊ฒ์ฆ
+ val conditionValidation = validateNode(condition)
+ if (!conditionValidation.success) {
+ violations.add("${ErrorMessages.CONDITION_INVALID}: ${conditionValidation.message}")
+ }
+
+ // ์ฐธ ๊ฐ ๊ฒ์ฆ
+ val trueValidation = validateNode(trueValue)
+ if (!trueValidation.success) {
+ violations.add("${ErrorMessages.TRUE_VALUE_INVALID}: ${trueValidation.message}")
+ }
+
+ // ๊ฑฐ์ง ๊ฐ ๊ฒ์ฆ
+ val falseValidation = validateNode(falseValue)
+ if (!falseValidation.success) {
+ violations.add("${ErrorMessages.FALSE_VALUE_INVALID}: ${falseValidation.message}")
+ }
+
+ // ์ค์ฒฉ ๊น์ด ๊ฒ์ฆ
+ val nestingDepth = calculateIfNodeNestingDepth(condition) +
+ calculateIfNodeNestingDepth(trueValue) +
+ calculateIfNodeNestingDepth(falseValue)
+ if (nestingDepth > MAX_NESTING_DEPTH) {
+ violations.add("${ErrorMessages.NESTING_DEPTH_EXCEEDED}: $nestingDepth > $MAX_NESTING_DEPTH")
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to IF_NODE_POLICY)
+ )
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param arguments ์ธ์ ๋ชฉ๋ก
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateArgumentsCreation(arguments: List): PolicyResult {
+ val violations = mutableListOf()
+
+ if (arguments.size > MAX_ARGUMENTS_COUNT) {
+ violations.add("${ErrorMessages.ARGUMENTS_COUNT_EXCEEDED}: ${arguments.size} > $MAX_ARGUMENTS_COUNT")
+ }
+
+ // ๊ฐ ์ธ์ ๊ฒ์ฆ
+ arguments.forEachIndexed { index, arg ->
+ val argValidation = validateNode(arg)
+ if (!argValidation.success) {
+ violations.add("${ErrorMessages.ARGUMENT_INVALID} $index: ${argValidation.message}")
+ }
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to ARGUMENT_LIST_POLICY)
+ )
+ }
+
+ /**
+ * ๋
ธ๋ ์ผ๋ฐ ๊ฒ์ฆ์ ์ํํฉ๋๋ค.
+ *
+ * @param node ๊ฒ์ฆํ ๋
ธ๋
+ * @return ์ ์ฑ
๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun validateNode(node: ASTNode): PolicyResult {
+ val violations = mutableListOf()
+
+ // ๋
ธ๋ ํฌ๊ธฐ ๊ฒ์ฆ
+ if (node.getSize() > MAX_NODE_SIZE) {
+ violations.add("${ErrorMessages.NODE_SIZE_EXCEEDED}: ${node.getSize()} > $MAX_NODE_SIZE")
+ }
+
+ // ๋
ธ๋ ๊น์ด ๊ฒ์ฆ
+ if (node.getDepth() > MAX_NODE_DEPTH) {
+ violations.add("${ErrorMessages.NODE_DEPTH_EXCEEDED}: ${node.getDepth()} > $MAX_NODE_DEPTH")
+ }
+
+ // ๋ณ์ ๊ฐ์ ๊ฒ์ฆ
+ if (node.getVariables().size > MAX_VARIABLES_PER_NODE) {
+ violations.add("${ErrorMessages.VARIABLES_PER_NODE_EXCEEDED}: ${node.getVariables().size} > $MAX_VARIABLES_PER_NODE")
+ }
+
+ return PolicyResult(
+ success = violations.isEmpty(),
+ message = violations.joinToString("; "),
+ data = mapOf("policyName" to NODE_GENERAL_VERIFICATION_POLICY)
+ )
+ }
+
+ // ์ค๋ณต ๋ฉ์๋๋ค์ ASTValidationUtils๋ก ๋์ฒด
+ private fun isValidVariableName(name: String): Boolean = ASTValidationUtils.isValidVariableName(name)
+ private fun isValidFunctionName(name: String): Boolean = ASTValidationUtils.isValidFunctionName(name)
+ private fun isReservedWord(name: String): Boolean = ASTValidationUtils.isReservedWord(name)
+ private fun isSupportedBinaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedBinaryOperator(operator)
+ private fun isSupportedUnaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedUnaryOperator(operator)
+ private fun isZeroConstant(node: ASTNode): Boolean = ASTValidationUtils.isZeroConstant(node)
+
+ /**
+ * IfNode์ ์ค์ฒฉ ๊น์ด๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ * ๋ค๋ฅธ ๋
ธ๋ ํ์
์ ๊ฒฝ์ฐ 0์ ๋ฐํํฉ๋๋ค.
+ */
+ private fun calculateIfNodeNestingDepth(node: ASTNode): Int {
+ return when (node) {
+ is hs.kr.entrydsm.domain.ast.entities.IfNode -> 1 + maxOf(
+ calculateIfNodeNestingDepth(node.condition),
+ calculateIfNodeNestingDepth(node.trueValue),
+ calculateIfNodeNestingDepth(node.falseValue)
+ )
+ else -> 0
+ }
+ }
+
+ companion object {
+ private const val MAX_NUMBER_VALUE = 1e15
+ private const val MIN_NUMBER_VALUE = -1e15
+ private const val MAX_VARIABLE_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_ARGS = 10
+ private const val MAX_ARGUMENTS_COUNT = 100
+ private const val MAX_NODE_SIZE = 1000
+ private const val MAX_NODE_DEPTH = 50
+ private const val MAX_VARIABLES_PER_NODE = 100
+ private const val MAX_NESTING_DEPTH = 20
+
+ private const val NUMBER_NODE_POLICY = "์ซ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val BOOLEAN_NODE_POLICY = "๋ถ๋ฆฌ์ธ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val VARIABLE_NODE_POLICY = "๋ณ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val BINARY_OPERATION_NODE_POLICY = "์ดํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val UNARY_OPERATION_NODE_POLICY = "๋จํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val FUNCTION_CALL_NODE_POLICY = "ํจ์ ํธ์ถ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val IF_NODE_POLICY = "์กฐ๊ฑด๋ฌธ ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val ARGUMENT_LIST_POLICY = "์ธ์ ๋ชฉ๋ก ๋
ธ๋ ์์ฑ ์ ์ฑ
"
+ private const val NODE_GENERAL_VERIFICATION_POLICY = "๋
ธ๋ ์ผ๋ฐ ๊ฒ์ฆ ์ ์ฑ
"
+
+ // ์ค๋ณต ์์๋ค์ ASTValidationUtils๋ก ๋์ฒด
+ // RESERVED_WORDS, BINARY_OPERATORS, UNARY_OPERATORS๋ ASTValidationUtils์์ ๊ด๋ฆฌ
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt
new file mode 100644
index 00000000..41b75ad1
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt
@@ -0,0 +1,592 @@
+package hs.kr.entrydsm.domain.ast.policies
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.utils.ASTValidationUtils
+import hs.kr.entrydsm.domain.ast.utils.FunctionValidationRules
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import hs.kr.entrydsm.domain.ast.policies.validation.*
+import hs.kr.entrydsm.global.annotation.policy.Policy
+import hs.kr.entrydsm.global.annotation.policy.PolicyResult
+import hs.kr.entrydsm.global.annotation.policy.type.Scope
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * AST ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ตฌํํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * ๋
ธ๋ ์์ฑ ์ ์ ์ฉ๋๋ ๋น์ฆ๋์ค ๊ท์น๊ณผ ์ ์ฝ์ฌํญ์ ์ ์ํ๋ฉฐ,
+ * ์์ฑ ์ ๊ฒ์ฆ๊ณผ ์์ฑ ํ ์ต์ ํ ๊ท์น์ ๊ด๋ฆฌํฉ๋๋ค.
+ *
+ * @see = mapOf(
+ OPERATOR_DIVISION to DivisionValidationStrategy(),
+ OPERATOR_MODULO to ModuloValidationStrategy(),
+ OPERATOR_POWER to PowerValidationStrategy(),
+ OPERATOR_MULTIPLICATION to MultiplicationValidationStrategy(),
+ OPERATOR_ADDITION to DefaultValidationStrategy(OPERATOR_ADDITION),
+ OPERATOR_SUBTRACTION to DefaultValidationStrategy(OPERATOR_SUBTRACTION)
+ )
+
+ /**
+ * ์ซ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param value ์ซ์ ๊ฐ
+ */
+ fun validateNumberCreation(value: Double) {
+ if (!value.isFinite()) {
+ throw ASTException.numberNotFinite(value)
+ }
+ if (value.isNaN()) {
+ throw ASTException.numberIsNaN(value)
+ }
+ if (value < MIN_NUMBER_VALUE) {
+ throw ASTException.numberTooSmall(value, MIN_NUMBER_VALUE)
+ }
+ if (value > MAX_NUMBER_VALUE) {
+ throw ASTException.numberTooLarge(value, MAX_NUMBER_VALUE)
+ }
+ }
+
+ /**
+ * ๋ถ๋ฆฌ์ธ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param value ๋ถ๋ฆฌ์ธ ๊ฐ
+ */
+ fun validateBooleanCreation(value: Boolean) {
+ // ๋ถ๋ฆฌ์ธ ๊ฐ์ ํญ์ ์ ํจ
+ }
+
+ /**
+ * ๋ณ์ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ๋ณ์๋ช
+ */
+ fun validateVariableCreation(name: String) {
+ if (name.isBlank()) {
+ throw ASTException.variableNameEmpty()
+ }
+ if (name.length > MAX_VARIABLE_NAME_LENGTH) {
+ throw ASTException.variableNameTooLong(name.length, MAX_VARIABLE_NAME_LENGTH)
+ }
+ if (!isValidVariableName(name)) {
+ throw ASTException.invalidVariableName(name)
+ }
+ if (isReservedWord(name)) {
+ throw ASTException.variableReservedWord(name)
+ }
+
+ // ๋ณ์๋ช
ํจํด ๊ฒ์ฆ (์ต์
)
+ if (ENFORCE_NAMING_CONVENTION && !isValidNamingConvention(name)) {
+ throw ASTException.nodeValidationFailed(
+ reason = "${ErrorMessages.NAMING_CONVENTION_VIOLATION}: $name"
+ )
+ }
+ }
+
+ /**
+ * ์ดํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param operator ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ */
+ fun validateBinaryOpCreation(left: ASTNode, operator: String, right: ASTNode) {
+ if (operator.isBlank()) {
+ throw ASTException.operatorEmpty()
+ }
+ if (!isSupportedBinaryOperator(operator)) {
+ throw ASTException.unsupportedBinaryOperator(operator)
+ }
+
+ // ํผ์ฐ์ฐ์ ๊ฒ์ฆ
+ validateNodeForOperation(left, NodeContextMessages.LEFT_OPERAND)
+ validateNodeForOperation(right, NodeContextMessages.RIGHT_OPERAND)
+
+ // ์ฐ์ฐ์๋ณ ํน๋ณ ๊ฒ์ฆ - Strategy ํจํด ์ ์ฉ
+ val strategy = validationStrategies[operator]
+ if (strategy != null) {
+ strategy.validate(left, right, zeroConstantOptimizationCount)
+ }
+
+ // ์ถ๊ฐ ๊ณ ๊ธ ์ต์ ํ ๋ก์ง (๋
ผ๋ฆฌ ์ฐ์ฐ์, ๋น๊ต ์ฐ์ฐ์ ๋ฑ)
+ when (operator) {
+ OPERATOR_LOGICAL_AND -> {
+ if (isTrueConstant(left) || isFalseConstant(left) ||
+ isTrueConstant(right) || isFalseConstant(right) ||
+ left.isStructurallyEqual(right)
+ ) {
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ }
+ OPERATOR_LOGICAL_OR -> {
+ if (isTrueConstant(left) || isFalseConstant(left) ||
+ isTrueConstant(right) || isFalseConstant(right) ||
+ left.isStructurallyEqual(right)
+ ) {
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ }
+ OPERATOR_EQUAL, OPERATOR_NOT_EQUAL -> {
+ if (left.isStructurallyEqual(right)) {
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ }
+ OPERATOR_LESS_THAN, OPERATOR_LESS_THAN_OR_EQUAL, OPERATOR_GREATER_THAN, OPERATOR_GREATER_THAN_OR_EQUAL -> {
+ if (left.isStructurallyEqual(right)) {
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ }
+ }
+
+ // ์ํ ์ฐธ์กฐ ๊ฒ์ฆ(์ต์
)
+ if (PREVENT_CIRCULAR_REFERENCES && hasCircularReference(left, right)) {
+ circularReferenceDetectionCount.incrementAndGet()
+ throw ASTException.nodeValidationFailed(
+ reason = ErrorMessages.CIRCULAR_REFERENCE_DETECTED
+ )
+ }
+ }
+
+ /**
+ * ๋จํญ ์ฐ์ฐ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param operator ์ฐ์ฐ์
+ * @param operand ํผ์ฐ์ฐ์
+ */
+ fun validateUnaryOpCreation(operator: String, operand: ASTNode) {
+ if (operator.isBlank()) {
+ throw ASTException.operatorEmpty()
+ }
+ if (!isSupportedUnaryOperator(operator)) {
+ throw ASTException.unsupportedUnaryOperator(operator)
+ }
+
+ // ํผ์ฐ์ฐ์ ๊ฒ์ฆ
+ validateNodeForOperation(operand, NodeContextMessages.OPERAND)
+
+ // ์ฐ์ฐ์๋ณ ํน๋ณ ๊ฒ์ฆ ๋ฐ ์ต์ ํ ํํธ
+ when (operator) {
+ OPERATOR_LOGICAL_NOT -> {
+ if (STRICT_LOGICAL_OPERATIONS && !isLogicalCompatible(operand)) {
+ throw ASTException.logicalIncompatibleOperand()
+ }
+ if (isTrueConstant(operand) || isFalseConstant(operand) ||
+ (operand is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode && operand.isLogicalNot())
+ ) {
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ }
+ OPERATOR_UNARY_MINUS -> {
+ if (isZeroConstant(operand)) {
+ zeroConstantOptimizationCount.incrementAndGet() // -0 = 0
+ } else if (operand is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode && operand.isNegation()) {
+ zeroConstantOptimizationCount.incrementAndGet() // -(-x) = x
+ } else if (operand is hs.kr.entrydsm.domain.ast.entities.NumberNode && operand.value < 0) {
+ zeroConstantOptimizationCount.incrementAndGet() // -(์์) = ์์
+ }
+ }
+ OPERATOR_UNARY_PLUS -> {
+ // +x = x
+ zeroConstantOptimizationCount.incrementAndGet()
+ }
+ }
+ }
+
+ /**
+ * ํจ์ ํธ์ถ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param args ์ธ์ ๋ชฉ๋ก
+ */
+ fun validateFunctionCallCreation(name: String, args: List) {
+ if (name.isBlank()) {
+ throw ASTException.functionNameEmpty()
+ }
+ if (name.length > MAX_FUNCTION_NAME_LENGTH) {
+ throw ASTException.functionNameTooLong(name.length, MAX_FUNCTION_NAME_LENGTH)
+ }
+ if (!isValidFunctionName(name)) {
+ throw ASTException.invalidFunctionName(name)
+ }
+ if (args.size > MAX_FUNCTION_ARGS) {
+ throw ASTException.functionArgumentsExceeded(args.size, MAX_FUNCTION_ARGS)
+ }
+
+ // ๊ฐ ์ธ์ ๊ฒ์ฆ
+ args.forEachIndexed { index, arg ->
+ validateNodeForOperation(arg, "${NodeContextMessages.ARGUMENT} $index")
+ }
+
+ // ํจ์๋ณ ๊ท์น
+ validateFunctionSpecificRules(name, args)
+ }
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param condition ์กฐ๊ฑด์
+ * @param trueValue ์ฐธ ๊ฐ
+ * @param falseValue ๊ฑฐ์ง ๊ฐ
+ */
+ fun validateIfCreation(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode) {
+ // ๊ฐ ๋
ธ๋ ๊ฒ์ฆ
+ validateNodeForOperation(condition, NodeContextMessages.CONDITION)
+ validateNodeForOperation(trueValue, NodeContextMessages.TRUE_VALUE)
+ validateNodeForOperation(falseValue, NodeContextMessages.FALSE_VALUE)
+
+ // ์ค์ฒฉ ๊น์ด ๊ฒ์ฆ
+ val totalDepth = condition.getDepth() + trueValue.getDepth() + falseValue.getDepth()
+ if (totalDepth > MAX_TOTAL_DEPTH) {
+ throw ASTException.ifTotalDepthExceeded(totalDepth, MAX_TOTAL_DEPTH)
+ }
+
+ // ์กฐ๊ฑด๋ฌธ ํน๋ณ ๊ฒ์ฆ(์์ ์กฐ๊ฑด ์ต์ ํ ๊ฐ์ง)
+ if (OPTIMIZE_CONSTANT_CONDITIONS && condition.isLiteral()) {
+ when (condition) {
+ is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> {
+ // ํญ์ ์ฐธ/๊ฑฐ์ง
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ is hs.kr.entrydsm.domain.ast.entities.NumberNode -> {
+ // 0/๋น0
+ constantConditionOptimizationCount.incrementAndGet()
+ }
+ else -> { /* no-op */ }
+ }
+ }
+ }
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก ๋
ธ๋ ์์ฑ ์ ์ฑ
์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param arguments ์ธ์ ๋ชฉ๋ก
+ */
+ fun validateArgumentsCreation(arguments: List) {
+ if (arguments.size > MAX_ARGUMENTS_COUNT) {
+ throw ASTException.argumentsExceeded(arguments.size, MAX_ARGUMENTS_COUNT)
+ }
+
+ // ๊ฐ ์ธ์ ๊ฒ์ฆ
+ arguments.forEachIndexed { index, arg ->
+ validateNodeForOperation(arg, "${NodeContextMessages.ARGUMENT} $index")
+ }
+
+ // ์ธ์ ์ค๋ณต ๊ฒ์ฆ(์ต์
)
+ if (PREVENT_DUPLICATE_ARGUMENTS) {
+ val duplicates = findDuplicateArguments(arguments)
+ if (duplicates.isNotEmpty()) {
+ throw ASTException.argumentsDuplicated(duplicates)
+ }
+ }
+ }
+
+ /**
+ * ์ฐ์ฐ์ ์ฌ์ฉ๋ ๋
ธ๋๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param node ๊ฒ์ฆํ ๋
ธ๋
+ * @param context ์ปจํ
์คํธ ์ ๋ณด
+ */
+ private fun validateNodeForOperation(node: ASTNode, context: String) {
+ val size = node.getSize()
+ val depth = node.getDepth()
+ val vars = node.getVariables().size
+
+ if (size > MAX_NODE_SIZE) {
+ throw ASTException.nodeSizeExceeded(size, MAX_NODE_SIZE, context)
+ }
+ if (depth > MAX_NODE_DEPTH) {
+ throw ASTException.nodeDepthExceeded(depth, MAX_NODE_DEPTH, context)
+ }
+ if (vars > MAX_VARIABLES_PER_NODE) {
+ throw ASTException.nodeVariablesExceeded(vars, MAX_VARIABLES_PER_NODE, context)
+ }
+ }
+
+ // === ASTValidationUtils ์์ ===
+ private fun isValidVariableName(name: String): Boolean = ASTValidationUtils.isValidVariableName(name)
+ private fun isValidFunctionName(name: String): Boolean = ASTValidationUtils.isValidFunctionName(name)
+ private fun isReservedWord(name: String): Boolean = ASTValidationUtils.isReservedWord(name)
+ private fun isSupportedBinaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedBinaryOperator(operator)
+ private fun isSupportedUnaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedUnaryOperator(operator)
+ private fun isZeroConstant(node: ASTNode): Boolean = ASTValidationUtils.isZeroConstant(node)
+
+ /**
+ * ๋ค์ด๋ฐ ๊ท์น์ ์ค์ํ๋์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isValidNamingConvention(name: String): Boolean {
+ // ์นด๋ฉ ์ผ์ด์ค ๋๋ ์ค๋ค์ดํฌ ์ผ์ด์ค ํ์ฉ
+ return name.matches(Regex(NAMING_CONVENTION_PATTERN))
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ 1 ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isOneConstant(node: ASTNode): Boolean {
+ return node is hs.kr.entrydsm.domain.ast.entities.NumberNode && node.value == 1.0
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ true ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isTrueConstant(node: ASTNode): Boolean {
+ return node is hs.kr.entrydsm.domain.ast.entities.BooleanNode && node.value
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ false ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isFalseConstant(node: ASTNode): Boolean {
+ return node is hs.kr.entrydsm.domain.ast.entities.BooleanNode && !node.value
+ }
+
+ /**
+ * ๋
ผ๋ฆฌ ์ฐ์ฐ์ ํธํ๋๋ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isLogicalCompatible(node: ASTNode): Boolean {
+ return when (node) {
+ is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> true
+ is hs.kr.entrydsm.domain.ast.entities.NumberNode -> true
+ is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode ->
+ node.isComparisonOperator() || node.isLogicalOperator()
+ is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode ->
+ node.isLogicalOperator()
+ else -> false
+ }
+ }
+
+ /**
+ * ์ํ ์ฐธ์กฐ๋ฅผ ํ์ธํฉ๋๋ค.
+ */
+ private fun hasCircularReference(left: ASTNode, right: ASTNode): Boolean {
+ // ์ง์ ์ ์ธ ๋์ผ์ฑ ๊ฒ์ฌ
+ if (left === right) {
+ return true
+ }
+
+ // ์ข์ธก ๋
ธ๋๊ฐ ์ฐ์ธก ๋
ธ๋๋ฅผ ์ฐธ์กฐํ๋์ง ํ์ธ
+ if (containsNode(left, right)) {
+ return true
+ }
+
+ // ์ฐ์ธก ๋
ธ๋๊ฐ ์ข์ธก ๋
ธ๋๋ฅผ ์ฐธ์กฐํ๋์ง ํ์ธ
+ if (containsNode(right, left)) {
+ return true
+ }
+
+ return false
+ }
+
+ /**
+ * ์ฃผ์ด์ง ๋
ธ๋๊ฐ ๋ค๋ฅธ ๋
ธ๋๋ฅผ ํฌํจํ๋์ง DFS๋ก ํ์ธํฉ๋๋ค.
+ */
+ private fun containsNode(
+ container: ASTNode,
+ target: ASTNode,
+ visited: MutableSet = mutableSetOf()
+ ): Boolean {
+ // ๋ฌดํ ๋ฃจํ ๋ฐฉ์ง
+ if (container in visited) {
+ return false
+ }
+
+ // ์ง์ ์ผ์น
+ if (container === target) {
+ return true
+ }
+
+ // ๋ฐฉ๋ฌธ ํ์
+ visited.add(container)
+
+ // ์์ ๋
ธ๋๋ค์ ์ฌ๊ท์ ์ผ๋ก ๊ฒ์ฌ
+ val hasTarget = container.getChildren().any { child ->
+ containsNode(child, target, visited)
+ }
+
+ // ๋ฐฉ๋ฌธ ํ์ ํด์ (๋ฐฑํธ๋ํน)
+ visited.remove(container)
+ return hasTarget
+ }
+
+ /**
+ * ๋
ธ๋ ํธ๋ฆฌ์์ ์ํ ์ฐธ์กฐ๋ฅผ ๊ฐ์งํฉ๋๋ค.
+ */
+ private fun detectCircularReferenceInTree(root: ASTNode): Boolean {
+ val visiting = mutableSetOf() // ํ์ฌ ๋ฐฉ๋ฌธ ์ค์ธ ๋
ธ๋๋ค
+ val visited = mutableSetOf() // ์์ ํ ์ฒ๋ฆฌ๋ ๋
ธ๋๋ค
+
+ fun dfs(node: ASTNode): Boolean {
+ // ํ์ฌ ๋ฐฉ๋ฌธ ์ค์ธ ๋
ธ๋๋ฅผ ๋ค์ ๋ฐฉ๋ฌธํ๋ฉด ์ํ ์ฐธ์กฐ
+ if (node in visiting) {
+ return true
+ }
+
+ // ์ด๋ฏธ ์ฒ๋ฆฌ๋ ๋
ธ๋๋ ๊ฑด๋๋ฐ๊ธฐ
+ if (node in visited) {
+ return false
+ }
+
+ // ๋ฐฉ๋ฌธ ์์
+ visiting.add(node)
+ for (child in node.getChildren()) {
+ if (dfs(child)) {
+ return true
+ }
+ }
+
+ // ๋ฐฉ๋ฌธ ์๋ฃ
+ visiting.remove(node)
+ visited.add(node)
+ return false
+ }
+ return dfs(root)
+ }
+
+ /**
+ * ์ค๋ณต ์ธ์๋ฅผ ์ฐพ์ต๋๋ค.
+ */
+ private fun findDuplicateArguments(arguments: List): List {
+ val seen = mutableSetOf()
+ val duplicates = mutableListOf()
+ arguments.forEach { arg ->
+ val argString = arg.toString()
+ if (argString in seen) {
+ duplicates.add(argString)
+ } else {
+ seen.add(argString)
+ }
+ }
+
+ return duplicates
+ }
+
+ /**
+ * ํจ์๋ณ ํน๋ณ ๊ท์น์ ๊ฒ์ฆํฉ๋๋ค.
+ */
+ private fun validateFunctionSpecificRules(name: String, args: List) {
+ if (!FunctionValidationRules.isValidFunctionCall(name, args)) {
+ val description = FunctionValidationRules.getArgumentCountDescription(name)
+ throw ASTException.functionArgumentCountMismatch(
+ name = name,
+ expectedDesc = description,
+ actual = args.size
+ )
+ }
+ }
+
+ companion object {
+ // ์ ์ฝ ์์
+ private const val MAX_NUMBER_VALUE = 1e15
+ private const val MIN_NUMBER_VALUE = -1e15
+ private const val MAX_VARIABLE_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_ARGS = 10
+ private const val MAX_ARGUMENTS_COUNT = 100
+ private const val MAX_NODE_SIZE = 1000
+ private const val MAX_NODE_DEPTH = 50
+ private const val MAX_VARIABLES_PER_NODE = 100
+ private const val MAX_TOTAL_DEPTH = 100
+
+ // ์ ์ฑ
ํ๋๊ทธ
+ private const val ENFORCE_NAMING_CONVENTION = true
+ private const val STRICT_LOGICAL_OPERATIONS = true
+ private const val PREVENT_CIRCULAR_REFERENCES = true
+ private const val OPTIMIZE_CONSTANT_CONDITIONS = true
+ private const val PREVENT_DUPLICATE_ARGUMENTS = false
+
+ // ์ฐ์ฐ์ ์์
+ private const val OPERATOR_DIVISION = "/"
+ private const val OPERATOR_MODULO = "%"
+ private const val OPERATOR_POWER = "^"
+ private const val OPERATOR_MULTIPLICATION = "*"
+ private const val OPERATOR_ADDITION = "+"
+ private const val OPERATOR_SUBTRACTION = "-"
+ private const val OPERATOR_LOGICAL_AND = "&&"
+ private const val OPERATOR_LOGICAL_OR = "||"
+ private const val OPERATOR_EQUAL = "=="
+ private const val OPERATOR_NOT_EQUAL = "!="
+ private const val OPERATOR_LESS_THAN = "<"
+ private const val OPERATOR_LESS_THAN_OR_EQUAL = "<="
+ private const val OPERATOR_GREATER_THAN = ">"
+ private const val OPERATOR_GREATER_THAN_OR_EQUAL = ">="
+ private const val OPERATOR_LOGICAL_NOT = "!"
+ private const val OPERATOR_UNARY_MINUS = "-"
+ private const val OPERATOR_UNARY_PLUS = "+"
+
+ // ํจํด ์์
+ private const val NAMING_CONVENTION_PATTERN = "^[a-z_][a-zA-Z0-9_]*$"
+
+ // ์ต์ ํ ํต๊ณ ์นด์ดํฐ
+ private val constantConditionOptimizationCount = AtomicLong(0)
+ private val zeroConstantOptimizationCount = AtomicLong(0)
+ private val circularReferenceDetectionCount = AtomicLong(0)
+
+ // ์๋ฌ ๋ฉ์์ง ์์
+ object ErrorMessages {
+ const val NAMING_CONVENTION_VIOLATION = "๋ค์ด๋ฐ ๊ท์น ์๋ฐ"
+ const val CIRCULAR_REFERENCE_DETECTED = "์ํ ์ฐธ์กฐ๊ฐ ๊ฐ์ง๋์์ต๋๋ค"
+ }
+
+ // ๋
ธ๋ ์ปจํ
์คํธ ๋ฉ์์ง
+ object NodeContextMessages {
+ const val LEFT_OPERAND = "์ข์ธก ํผ์ฐ์ฐ์"
+ const val RIGHT_OPERAND = "์ฐ์ธก ํผ์ฐ์ฐ์"
+ const val OPERAND = "ํผ์ฐ์ฐ์"
+ const val ARGUMENT = "์ธ์"
+ const val CONDITION = "์กฐ๊ฑด์"
+ const val TRUE_VALUE = "์ฐธ ๊ฐ"
+ const val FALSE_VALUE = "๊ฑฐ์ง ๊ฐ"
+ }
+
+ // ํต๊ณ ํค ์์
+ object StatisticsKeys {
+ const val CONSTANT_CONDITION_OPTIMIZATIONS = "constantConditionOptimizations"
+ const val ZERO_CONSTANT_OPTIMIZATIONS = "zeroConstantOptimizations"
+ const val CIRCULAR_REFERENCE_DETECTIONS = "circularReferenceDetections"
+ const val OPTIMIZATION_FLAGS = "optimizationFlags"
+ const val ENFORCE_NAMING_CONVENTION_FLAG = "enforceNamingConvention"
+ const val STRICT_LOGICAL_OPERATIONS_FLAG = "strictLogicalOperations"
+ const val PREVENT_CIRCULAR_REFERENCES_FLAG = "preventCircularReferences"
+ const val OPTIMIZE_CONSTANT_CONDITIONS_FLAG = "optimizeConstantConditions"
+ const val PREVENT_DUPLICATE_ARGUMENTS_FLAG = "preventDuplicateArguments"
+ }
+
+ /**
+ * ์ ์ฑ
ํต๊ณ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getPolicyStatistics(): Map {
+ return mapOf(
+ StatisticsKeys.CONSTANT_CONDITION_OPTIMIZATIONS to constantConditionOptimizationCount.get(),
+ StatisticsKeys.ZERO_CONSTANT_OPTIMIZATIONS to zeroConstantOptimizationCount.get(),
+ StatisticsKeys.CIRCULAR_REFERENCE_DETECTIONS to circularReferenceDetectionCount.get(),
+ StatisticsKeys.OPTIMIZATION_FLAGS to mapOf(
+ StatisticsKeys.ENFORCE_NAMING_CONVENTION_FLAG to ENFORCE_NAMING_CONVENTION,
+ StatisticsKeys.STRICT_LOGICAL_OPERATIONS_FLAG to STRICT_LOGICAL_OPERATIONS,
+ StatisticsKeys.PREVENT_CIRCULAR_REFERENCES_FLAG to PREVENT_CIRCULAR_REFERENCES,
+ StatisticsKeys.OPTIMIZE_CONSTANT_CONDITIONS_FLAG to OPTIMIZE_CONSTANT_CONDITIONS,
+ StatisticsKeys.PREVENT_DUPLICATE_ARGUMENTS_FLAG to PREVENT_DUPLICATE_ARGUMENTS
+ )
+ )
+ }
+
+ /**
+ * ํต๊ณ ์นด์ดํฐ๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
+ */
+ fun resetStatistics() {
+ constantConditionOptimizationCount.set(0)
+ zeroConstantOptimizationCount.set(0)
+ circularReferenceDetectionCount.set(0)
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt
new file mode 100644
index 00000000..7bf21714
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt
@@ -0,0 +1,48 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ์ดํญ ์ฐ์ฐ์๋ณ ๊ฒ์ฆ ์ ๋ต์ ์ ์ํ๋ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * Strategy ํจํด์ ์ ์ฉํ์ฌ ์ฐ์ฐ์๋ณ๋ก ์๋ก ๋ค๋ฅธ ๊ฒ์ฆ ๋ก์ง์
+ * ๋
๋ฆฝ์ ์ผ๋ก ๊ตฌํํ ์ ์๋๋ก ํฉ๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+interface BinaryOperatorValidationStrategy {
+
+ /**
+ * ์ด ์ ๋ต์ด ์ง์ํ๋ ์ฐ์ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun supportedOperator(): String
+
+ /**
+ * ์ฐ์ฐ์๋ณ ํน๋ณ ๊ฒ์ฆ์ ์ํํฉ๋๋ค.
+ *
+ * @param left ์ข์ธก ํผ์ฐ์ฐ์
+ * @param right ์ฐ์ธก ํผ์ฐ์ฐ์
+ * @param optimizationCounter ์ต์ ํ ์นด์ดํฐ (ํ์ ์ ์ฌ์ฉ)
+ */
+ fun validate(
+ left: ASTNode,
+ right: ASTNode,
+ optimizationCounter: AtomicLong
+ )
+
+ /**
+ * ๋
ธ๋๊ฐ 0 ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isZeroConstant(node: ASTNode): Boolean {
+ return node.getNodeType() == "NumberNode" && node.toString() == "0"
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ 1 ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isOneConstant(node: ASTNode): Boolean {
+ return node.getNodeType() == "NumberNode" && node.toString() == "1"
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt
new file mode 100644
index 00000000..da471866
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt
@@ -0,0 +1,46 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๊ธฐ๋ณธ ์ฐ์ฐ์ (+, -) ๊ฒ์ฆ ์ ๋ต์
๋๋ค.
+ *
+ * ํน๋ณํ ๊ฒ์ฆ ๋ก์ง์ด ์๋ ์ฐ์ฐ์๋ค์ ์ํ ๊ธฐ๋ณธ ์ ๋ต์
๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+class DefaultValidationStrategy(private val operator: String) : BinaryOperatorValidationStrategy {
+
+ override fun supportedOperator(): String = operator
+
+ override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) {
+ when (operator) {
+ "+" -> {
+ // 0๊ณผ์ ๋ง์
์ต์ ํ ๊ฐ์ง (x + 0 = x, 0 + x = x)
+ if (isZeroConstant(left) || isZeroConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ // ๊ฐ์ ํผ์ฐ์ฐ์ ์ต์ ํ ๊ฐ์ง (x + x = 2*x)
+ if (left.isStructurallyEqual(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ }
+ "-" -> {
+ // 0๊ณผ์ ๋บ์
์ต์ ํ ๊ฐ์ง (x - 0 = x)
+ if (isZeroConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ // 0์์ ๋นผ๊ธฐ ์ต์ ํ ๊ฐ์ง (0 - x = -x)
+ if (isZeroConstant(left)) {
+ optimizationCounter.incrementAndGet()
+ }
+ // ๊ฐ์ ํผ์ฐ์ฐ์ ์ต์ ํ ๊ฐ์ง (x - x = 0)
+ if (left.isStructurallyEqual(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt
new file mode 100644
index 00000000..7b4cbefb
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt
@@ -0,0 +1,25 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๋๋์
์ฐ์ฐ์ (/) ๊ฒ์ฆ ์ ๋ต์
๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+class DivisionValidationStrategy : BinaryOperatorValidationStrategy {
+
+ override fun supportedOperator(): String = "/"
+
+ override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) {
+ if (isZeroConstant(right)) {
+ throw ASTException.divisionByZero()
+ }
+ if (isOneConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt
new file mode 100644
index 00000000..c867ce42
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt
@@ -0,0 +1,25 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๋๋จธ์ง ์ฐ์ฐ์ (%) ๊ฒ์ฆ ์ ๋ต์
๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+class ModuloValidationStrategy : BinaryOperatorValidationStrategy {
+
+ override fun supportedOperator(): String = "%"
+
+ override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) {
+ if (isZeroConstant(right)) {
+ throw ASTException.moduloByZero()
+ }
+ if (isOneConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt
new file mode 100644
index 00000000..668e0ce3
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt
@@ -0,0 +1,26 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๊ณฑ์
์ฐ์ฐ์ (*) ๊ฒ์ฆ ์ ๋ต์
๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+class MultiplicationValidationStrategy : BinaryOperatorValidationStrategy {
+
+ override fun supportedOperator(): String = "*"
+
+ override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) {
+ // 0๊ณผ์ ๊ณฑ์
์ต์ ํ ๊ฐ์ง (x * 0 = 0, 0 * x = 0)
+ if (isZeroConstant(left) || isZeroConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ // 1๊ณผ์ ๊ณฑ์
์ต์ ํ ๊ฐ์ง (x * 1 = x, 1 * x = x)
+ else if (isOneConstant(left) || isOneConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt
new file mode 100644
index 00000000..7895f754
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt
@@ -0,0 +1,39 @@
+package hs.kr.entrydsm.domain.ast.policies.validation
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๊ฑฐ๋ญ์ ๊ณฑ ์ฐ์ฐ์ (^) ๊ฒ์ฆ ์ ๋ต์
๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.13
+ */
+class PowerValidationStrategy : BinaryOperatorValidationStrategy {
+
+ override fun supportedOperator(): String = "^"
+
+ override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) {
+ if (isZeroConstant(left) && isZeroConstant(right)) {
+ optimizationCounter.incrementAndGet()
+ throw ASTException.zeroPowerZero()
+ }
+
+ // ๊ฑฐ๋ญ์ ๊ณฑ ์ต์ ํ ๊ฐ์ง
+ when {
+ isOneConstant(left) -> {
+ // 1^x = 1
+ optimizationCounter.incrementAndGet()
+ }
+ isZeroConstant(right) -> {
+ // x^0 = 1 (x != 0)
+ optimizationCounter.incrementAndGet()
+ }
+ isOneConstant(right) -> {
+ // x^1 = x
+ optimizationCounter.incrementAndGet()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt
new file mode 100644
index 00000000..f7888ae4
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt
@@ -0,0 +1,690 @@
+package hs.kr.entrydsm.domain.ast.services
+
+import hs.kr.entrydsm.domain.ast.entities.*
+import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.entities.IfNode
+import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode
+import hs.kr.entrydsm.domain.ast.factories.ASTNodeFactory
+import hs.kr.entrydsm.domain.ast.values.NodeSize
+import hs.kr.entrydsm.domain.ast.values.TreeDepth
+import hs.kr.entrydsm.global.annotation.service.Service
+import hs.kr.entrydsm.global.annotation.service.type.ServiceType
+import hs.kr.entrydsm.global.exception.DomainException
+import hs.kr.entrydsm.global.exception.ErrorCode
+import hs.kr.entrydsm.global.configuration.ASTConfiguration
+import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider
+import kotlin.math.*
+
+/**
+ * AST ํธ๋ฆฌ๋ฅผ ์ต์ ํํ๋ ์๋น์ค์
๋๋ค.
+ *
+ * ์์ ํด๋ฉ, ๊ณตํต ํ์ ํํ์ ์ ๊ฑฐ, ๋ถํ์ํ ๋
ธ๋ ์ ๊ฑฐ ๋ฑ์
+ * ์ต์ ํ ๊ธฐ๋ฒ์ ์ ์ฉํ์ฌ ํธ๋ฆฌ๋ฅผ ์ต์ ํํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Service(
+ name = "AST ํธ๋ฆฌ ์ต์ ํ ์๋น์ค",
+ type = ServiceType.DOMAIN_SERVICE
+)
+class TreeOptimizer(
+ private val configurationProvider: ConfigurationProvider? = null
+) {
+
+ private val factory = ASTNodeFactory()
+ private val traverser = TreeTraverser()
+
+ // ์ค์ ์ ConfigurationProvider๋ฅผ ํตํด ๋์ ์ผ๋ก ์ ๊ทผ (๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ ๊ฐ๋ฅ)
+ private val config: ASTConfiguration
+ get() = configurationProvider?.getASTConfiguration() ?: ASTConfiguration()
+
+ /**
+ * ํธ๋ฆฌ๋ฅผ ์ต์ ํํฉ๋๋ค.
+ *
+ * @param root ์ต์ ํํ ๋ฃจํธ ๋
ธ๋
+ * @return ์ต์ ํ๋ ํธ๋ฆฌ
+ */
+ fun optimize(root: ASTNode): ASTNode {
+ var optimized = root
+
+ for (pass in 1..5) {
+ val beforeSize = optimized.getSize()
+
+ optimized = performOptimizationPass(optimized)
+
+ val afterSize = optimized.getSize()
+
+ if (beforeSize == afterSize) {
+ break
+ }
+ }
+
+ return optimized
+ }
+
+ /**
+ * ๋จ์ผ ์ต์ ํ ํจ์ค๋ฅผ ์ํํฉ๋๋ค.
+ */
+ private fun performOptimizationPass(root: ASTNode): ASTNode {
+ var optimized = root
+
+ // 1. ์์ ํด๋ฉ
+ optimized = constantFolding(optimized)
+
+ // 2. ํญ๋ฑ ์์ ์ ๊ฑฐ
+ optimized = eliminateIdentityElements(optimized)
+
+ // 3. ๋ถํ์ํ ์กฐ๊ฑด๋ฌธ ์ ๊ฑฐ
+ optimized = eliminateUnnecessaryConditionals(optimized)
+
+ // 4. ๋จํญ ์ฐ์ฐ์ ๋จ์ํ
+ optimized = simplifyUnaryOperations(optimized)
+
+ // 5. ์ค๋ณต ๋
ธ๋ ์ ๊ฑฐ
+ optimized = eliminateDuplicateNodes(optimized)
+
+ // 6. ๊ณตํต ํ์ ํํ์ ์ ๊ฑฐ
+ optimized = eliminateCommonSubexpressions(optimized)
+
+ return optimized
+ }
+
+ /**
+ * ์์ ํด๋ฉ์ ์ํํฉ๋๋ค.
+ */
+ private fun constantFolding(root: ASTNode): ASTNode {
+ return when (root) {
+ is BinaryOpNode -> {
+ val leftOptimized = constantFolding(root.left)
+ val rightOptimized = constantFolding(root.right)
+
+ // ์์ชฝ ๋ชจ๋ ์์์ธ ๊ฒฝ์ฐ ๊ณ์ฐ
+ if (leftOptimized is NumberNode && rightOptimized is NumberNode) {
+ evaluateConstantBinaryOp(leftOptimized, root.operator, rightOptimized)
+ } else {
+ factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+
+ is UnaryOpNode -> {
+ val operandOptimized = constantFolding(root.operand)
+
+ // ํผ์ฐ์ฐ์๊ฐ ์์์ธ ๊ฒฝ์ฐ ๊ณ์ฐ
+ if (operandOptimized is NumberNode) {
+ evaluateConstantUnaryOp(root.operator, operandOptimized)
+ } else {
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { constantFolding(it) }
+
+ // ๋ชจ๋ ์ธ์๊ฐ ์์์ธ ๊ฒฝ์ฐ ๊ณ์ฐ
+ if (optimizedArgs.all { it is NumberNode }) {
+ evaluateConstantFunctionCall(root.name, optimizedArgs.map { it as NumberNode })
+ } else {
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+ }
+
+ is IfNode -> {
+ val conditionOptimized = constantFolding(root.condition)
+ val trueOptimized = constantFolding(root.trueValue)
+ val falseOptimized = constantFolding(root.falseValue)
+
+ // ์กฐ๊ฑด์ด ์์์ธ ๊ฒฝ์ฐ ๋ถ๊ธฐ ์ ํ
+ when (conditionOptimized) {
+ is BooleanNode -> if (conditionOptimized.value) trueOptimized else falseOptimized
+ is NumberNode -> if (conditionOptimized.value != 0.0) trueOptimized else falseOptimized
+ else -> factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { constantFolding(it) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+ }
+
+ /**
+ * ํญ๋ฑ ์์๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
+ */
+ private fun eliminateIdentityElements(root: ASTNode): ASTNode {
+ return when (root) {
+ is BinaryOpNode -> {
+ val leftOptimized = eliminateIdentityElements(root.left)
+ val rightOptimized = eliminateIdentityElements(root.right)
+
+ when (root.operator) {
+ "+" -> {
+ when {
+ isZero(leftOptimized) -> rightOptimized
+ isZero(rightOptimized) -> leftOptimized
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+ "-" -> {
+ when {
+ isZero(rightOptimized) -> leftOptimized
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+ "*" -> {
+ when {
+ isZero(leftOptimized) || isZero(rightOptimized) -> factory.createNumber(0.0)
+ isOne(leftOptimized) -> rightOptimized
+ isOne(rightOptimized) -> leftOptimized
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+ "/" -> {
+ when {
+ isZero(leftOptimized) -> factory.createNumber(0.0)
+ isOne(rightOptimized) -> leftOptimized
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+ "^" -> {
+ when {
+ isOne(rightOptimized) -> leftOptimized // x^1 = x
+ isOne(leftOptimized) -> factory.createNumber(1.0) // 1^x = 1
+
+ isZero(leftOptimized) && rightOptimized is NumberNode && rightOptimized.value > 0.0 -> {
+ factory.createNumber(0.0)
+ }
+
+ isZero(rightOptimized) && leftOptimized is NumberNode && leftOptimized.value != 0.0 -> {
+ factory.createNumber(1.0)
+ }
+
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+ else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+ }
+
+ is UnaryOpNode -> {
+ val operandOptimized = eliminateIdentityElements(root.operand)
+
+ when (root.operator) {
+ "+" -> operandOptimized // ๋จํญ ํ๋ฌ์ค ์ ๊ฑฐ
+ "-" -> {
+ // ์ด์ค ๋ถ์ ์ ๊ฑฐ
+ if (operandOptimized is UnaryOpNode && operandOptimized.operator == "-") {
+ operandOptimized.operand
+ } else {
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+ }
+ else -> factory.createUnaryOp(root.operator, operandOptimized)
+ }
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { eliminateIdentityElements(it) }
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+
+ is IfNode -> {
+ val conditionOptimized = eliminateIdentityElements(root.condition)
+ val trueOptimized = eliminateIdentityElements(root.trueValue)
+ val falseOptimized = eliminateIdentityElements(root.falseValue)
+ factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { eliminateIdentityElements(it) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+ }
+
+ /**
+ * ๋ถํ์ํ ์กฐ๊ฑด๋ฌธ์ ์ ๊ฑฐํฉ๋๋ค.
+ */
+ private fun eliminateUnnecessaryConditionals(root: ASTNode): ASTNode {
+ return when (root) {
+ is IfNode -> {
+ val conditionOptimized = eliminateUnnecessaryConditionals(root.condition)
+ val trueOptimized = eliminateUnnecessaryConditionals(root.trueValue)
+ val falseOptimized = eliminateUnnecessaryConditionals(root.falseValue)
+
+ // ์ฐธ ๊ฐ๊ณผ ๊ฑฐ์ง ๊ฐ์ด ๊ฐ์ ๊ฒฝ์ฐ
+ if (nodesEqual(trueOptimized, falseOptimized)) {
+ trueOptimized
+ } else {
+ factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+ }
+
+ is BinaryOpNode -> {
+ val leftOptimized = eliminateUnnecessaryConditionals(root.left)
+ val rightOptimized = eliminateUnnecessaryConditionals(root.right)
+ factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+
+ is UnaryOpNode -> {
+ val operandOptimized = eliminateUnnecessaryConditionals(root.operand)
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { eliminateUnnecessaryConditionals(it) }
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { eliminateUnnecessaryConditionals(it) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+ }
+
+ /**
+ * ๋จํญ ์ฐ์ฐ์๋ฅผ ๋จ์ํํฉ๋๋ค.
+ */
+ private fun simplifyUnaryOperations(root: ASTNode): ASTNode {
+ return when (root) {
+ is UnaryOpNode -> {
+ val operandOptimized = simplifyUnaryOperations(root.operand)
+
+ when (root.operator) {
+ "!" -> {
+ // ์ด์ค ๋ถ์ ์ ๊ฑฐ
+ if (operandOptimized is UnaryOpNode && operandOptimized.operator == "!") {
+ operandOptimized.operand
+ } else {
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+ }
+ else -> factory.createUnaryOp(root.operator, operandOptimized)
+ }
+ }
+
+ is BinaryOpNode -> {
+ val leftOptimized = simplifyUnaryOperations(root.left)
+ val rightOptimized = simplifyUnaryOperations(root.right)
+ factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { simplifyUnaryOperations(it) }
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+
+ is IfNode -> {
+ val conditionOptimized = simplifyUnaryOperations(root.condition)
+ val trueOptimized = simplifyUnaryOperations(root.trueValue)
+ val falseOptimized = simplifyUnaryOperations(root.falseValue)
+ factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { simplifyUnaryOperations(it) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+ }
+
+ /**
+ * ์ค๋ณต ๋
ธ๋๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
+ */
+ private fun eliminateDuplicateNodes(root: ASTNode): ASTNode {
+ val seenNodes = mutableMapOf()
+ return eliminateDuplicateNodesHelper(root, seenNodes)
+ }
+
+ /**
+ * ์ค๋ณต ๋
ธ๋ ์ ๊ฑฐ ํฌํผ ํจ์
+ */
+ private fun eliminateDuplicateNodesHelper(root: ASTNode, seenNodes: MutableMap): ASTNode {
+ val nodeKey = root.toString()
+
+ // ์ด๋ฏธ ๋ณธ ๋
ธ๋์ธ ๊ฒฝ์ฐ ์ฌ์ฌ์ฉ
+ if (seenNodes.containsKey(nodeKey) && root.isLeaf()) {
+ return seenNodes[nodeKey]!!
+ }
+
+ val optimized = when (root) {
+ is BinaryOpNode -> {
+ val leftOptimized = eliminateDuplicateNodesHelper(root.left, seenNodes)
+ val rightOptimized = eliminateDuplicateNodesHelper(root.right, seenNodes)
+ factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+
+ is UnaryOpNode -> {
+ val operandOptimized = eliminateDuplicateNodesHelper(root.operand, seenNodes)
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { eliminateDuplicateNodesHelper(it, seenNodes) }
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+
+ is IfNode -> {
+ val conditionOptimized = eliminateDuplicateNodesHelper(root.condition, seenNodes)
+ val trueOptimized = eliminateDuplicateNodesHelper(root.trueValue, seenNodes)
+ val falseOptimized = eliminateDuplicateNodesHelper(root.falseValue, seenNodes)
+ factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { eliminateDuplicateNodesHelper(it, seenNodes) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+
+ // ๋ฆฌํ ๋
ธ๋์ธ ๊ฒฝ์ฐ ์ ์ฅ
+ if (optimized.isLeaf()) {
+ seenNodes[nodeKey] = optimized
+ }
+
+ return optimized
+ }
+
+ /**
+ * ๊ณตํต ํ์ ํํ์์ ์ ๊ฑฐํฉ๋๋ค.
+ */
+ private fun eliminateCommonSubexpressions(root: ASTNode): ASTNode {
+ val subexpressions = mutableMapOf()
+ return eliminateCommonSubexpressionsHelper(root, subexpressions)
+ }
+
+ /**
+ * ๊ณตํต ํ์ ํํ์ ์ ๊ฑฐ ํฌํผ ํจ์
+ */
+ private fun eliminateCommonSubexpressionsHelper(root: ASTNode, subexpressions: MutableMap): ASTNode {
+ val nodeKey = root.toString()
+
+ // ์ด๋ฏธ ๋ณธ ํ์ ํํ์์ธ ๊ฒฝ์ฐ ์ฌ์ฌ์ฉ
+ if (subexpressions.containsKey(nodeKey) && !root.isLeaf()) {
+ return subexpressions[nodeKey]!!
+ }
+
+ val optimized = when (root) {
+ is BinaryOpNode -> {
+ val leftOptimized = eliminateCommonSubexpressionsHelper(root.left, subexpressions)
+ val rightOptimized = eliminateCommonSubexpressionsHelper(root.right, subexpressions)
+ factory.createBinaryOp(leftOptimized, root.operator, rightOptimized)
+ }
+
+ is UnaryOpNode -> {
+ val operandOptimized = eliminateCommonSubexpressionsHelper(root.operand, subexpressions)
+ factory.createUnaryOp(root.operator, operandOptimized)
+ }
+
+ is FunctionCallNode -> {
+ val optimizedArgs = root.args.map { eliminateCommonSubexpressionsHelper(it, subexpressions) }
+ factory.createFunctionCall(root.name, optimizedArgs)
+ }
+
+ is IfNode -> {
+ val conditionOptimized = eliminateCommonSubexpressionsHelper(root.condition, subexpressions)
+ val trueOptimized = eliminateCommonSubexpressionsHelper(root.trueValue, subexpressions)
+ val falseOptimized = eliminateCommonSubexpressionsHelper(root.falseValue, subexpressions)
+ factory.createIf(conditionOptimized, trueOptimized, falseOptimized)
+ }
+
+ is ArgumentsNode -> {
+ val optimizedArgs = root.arguments.map { eliminateCommonSubexpressionsHelper(it, subexpressions) }
+ factory.createArguments(optimizedArgs)
+ }
+
+ else -> root
+ }
+
+ // ๋ณตํฉ ๋
ธ๋์ธ ๊ฒฝ์ฐ ์ ์ฅ
+ if (!optimized.isLeaf()) {
+ subexpressions[nodeKey] = optimized
+ }
+
+ return optimized
+ }
+
+ /**
+ * ์์ ์ดํญ ์ฐ์ฐ์ ํ๊ฐํฉ๋๋ค.
+ */
+ private fun evaluateConstantBinaryOp(left: NumberNode, operator: String, right: NumberNode): ASTNode {
+ return try {
+ val result = when (operator) {
+ "+" -> left.value + right.value
+ "-" -> left.value - right.value
+ "*" -> left.value * right.value
+ "/" -> if (right.value != 0.0) left.value / right.value else return factory.createBinaryOp(left, operator, right)
+ "%" -> if (right.value != 0.0) left.value % right.value else return factory.createBinaryOp(left, operator, right)
+ "^" -> left.value.pow(right.value)
+ else -> return factory.createBinaryOp(left, operator, right)
+ }
+ factory.createNumber(result)
+ } catch (e: ArithmeticException) {
+ // ์ฐ์ ์ฐ์ฐ ์ค๋ฅ ๋ฐ์ ์ ์๋ณธ ๋
ธ๋ ๋ฐํ
+ throw DomainException(
+ errorCode = ErrorCode.MATH_ERROR,
+ message = "์ดํญ ์ฐ์ฐ ์ค ์ฐ์ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "operator" to operator,
+ "leftValue" to left.value,
+ "rightValue" to right.value
+ )
+ )
+ } catch (e: NumberFormatException) {
+ // ์ซ์ ํ์ ์ค๋ฅ๋ ์ฒ๋ฆฌ
+ throw DomainException(
+ errorCode = ErrorCode.NUMBER_CONVERSION_ERROR,
+ message = "์ซ์ ๋ณํ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "operator" to operator,
+ "leftValue" to left.value,
+ "rightValue" to right.value
+ )
+ )
+ }
+ }
+
+ /**
+ * ์์ ๋จํญ ์ฐ์ฐ์ ํ๊ฐํฉ๋๋ค.
+ */
+ private fun evaluateConstantUnaryOp(operator: String, operand: NumberNode): ASTNode {
+ return try {
+ val result = when (operator) {
+ "-" -> -operand.value
+ "+" -> operand.value
+ else -> return factory.createUnaryOp(operator, operand)
+ }
+ factory.createNumber(result)
+ } catch (e: ArithmeticException) {
+ // ์ฐ์ ์ฐ์ฐ ์ค๋ฅ ๋ฐ์ ์ ๋๋ฉ์ธ ์์ธ๋ก ๋ณํ
+ throw DomainException(
+ errorCode = ErrorCode.MATH_ERROR,
+ message = "๋จํญ ์ฐ์ฐ ์ค ์ฐ์ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "operator" to operator,
+ "operandValue" to operand.value
+ )
+ )
+ } catch (e: NumberFormatException) {
+ // ์ซ์ ํ์ ์ค๋ฅ๋ ์ฒ๋ฆฌ
+ throw DomainException(
+ errorCode = ErrorCode.NUMBER_CONVERSION_ERROR,
+ message = "์ซ์ ๋ณํ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "operator" to operator,
+ "operandValue" to operand.value
+ )
+ )
+ }
+ }
+
+ /**
+ * ์์ ํจ์ ํธ์ถ์ ํ๊ฐํฉ๋๋ค.
+ */
+ private fun evaluateConstantFunctionCall(name: String, args: List): ASTNode {
+ return try {
+ val result = when (name.uppercase()) {
+ "ABS" -> if (args.size == 1) kotlin.math.abs(args[0].value) else return factory.createFunctionCall(name, args)
+ "SQRT" -> if (args.size == 1) kotlin.math.sqrt(args[0].value) else return factory.createFunctionCall(name, args)
+ "SIN" -> if (args.size == 1) kotlin.math.sin(args[0].value) else return factory.createFunctionCall(name, args)
+ "COS" -> if (args.size == 1) kotlin.math.cos(args[0].value) else return factory.createFunctionCall(name, args)
+ "TAN" -> if (args.size == 1) kotlin.math.tan(args[0].value) else return factory.createFunctionCall(name, args)
+ "LOG" -> if (args.size == 1) kotlin.math.ln(args[0].value) else return factory.createFunctionCall(name, args)
+ "EXP" -> if (args.size == 1) kotlin.math.exp(args[0].value) else return factory.createFunctionCall(name, args)
+ "POW" -> if (args.size == 2) args[0].value.pow(args[1].value) else return factory.createFunctionCall(name, args)
+ "MAX" -> if (args.isNotEmpty()) args.maxOf { it.value } else return factory.createFunctionCall(name, args)
+ "MIN" -> if (args.isNotEmpty()) args.minOf { it.value } else return factory.createFunctionCall(name, args)
+ else -> return factory.createFunctionCall(name, args)
+ }
+ factory.createNumber(result)
+ } catch (e: ArithmeticException) {
+ // ์ฐ์ ์ฐ์ฐ ์ค๋ฅ ๋ฐ์ ์ ๋๋ฉ์ธ ์์ธ๋ก ๋ณํ
+ throw DomainException(
+ errorCode = ErrorCode.MATH_ERROR,
+ message = "ํจ์ ํธ์ถ ์ค ์ฐ์ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "functionName" to name,
+ "argumentCount" to args.size,
+ "arguments" to args.map { it.value }
+ )
+ )
+ } catch (e: NumberFormatException) {
+ // ์ซ์ ํ์ ์ค๋ฅ๋ ์ฒ๋ฆฌ
+ throw DomainException(
+ errorCode = ErrorCode.NUMBER_CONVERSION_ERROR,
+ message = "ํจ์ ํธ์ถ ์ค ์ซ์ ๋ณํ ์ค๋ฅ ๋ฐ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "functionName" to name,
+ "argumentCount" to args.size,
+ "arguments" to args.map { it.value }
+ )
+ )
+ } catch (e: IllegalArgumentException) {
+ // ์๋ชป๋ ์ธ์ (์: SQRT(-1), LOG(0) ๋ฑ)
+ throw DomainException(
+ errorCode = ErrorCode.WRONG_ARGUMENT_COUNT,
+ message = "ํจ์ ํธ์ถ ์ค ์๋ชป๋ ์ธ์: ${e.message}",
+ cause = e,
+ context = mapOf(
+ "functionName" to name,
+ "argumentCount" to args.size,
+ "arguments" to args.map { it.value }
+ )
+ )
+ }
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ 0์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isZero(node: ASTNode): Boolean {
+ return node is NumberNode && node.value == 0.0
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ 1์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isOne(node: ASTNode): Boolean {
+ return node is NumberNode && node.value == 1.0
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isPositive(node: ASTNode): Boolean {
+ return node is NumberNode && node.value > 0.0
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun isNegative(node: ASTNode): Boolean {
+ return node is NumberNode && node.value < 0.0
+ }
+
+ /**
+ * ๋ ๋
ธ๋๊ฐ ๊ตฌ์กฐ์ ์ผ๋ก ๊ฐ์์ง ํ์ธํฉ๋๋ค.
+ */
+ private fun nodesEqual(node1: ASTNode, node2: ASTNode): Boolean {
+ return node1.isStructurallyEqual(node2)
+ }
+
+ /**
+ * ์ต์ ํ ํต๊ณ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun calculateOptimizationStatistics(original: ASTNode, optimized: ASTNode): OptimizationStatistics {
+ val originalStats = traverser.calculateStatistics(original)
+ val optimizedStats = traverser.calculateStatistics(optimized)
+
+ return OptimizationStatistics(
+ originalNodeCount = originalStats.nodeCount,
+ optimizedNodeCount = optimizedStats.nodeCount,
+ reductionRatio = calculateReductionRatio(originalStats.nodeCount, optimizedStats.nodeCount),
+ originalDepth = originalStats.maxDepth,
+ optimizedDepth = optimizedStats.maxDepth,
+ depthReduction = TreeDepth.of(originalStats.maxDepth.value - optimizedStats.maxDepth.value)
+ )
+ }
+
+ /**
+ * ๊ฐ์ ๋น์จ์ ๊ณ์ฐํฉ๋๋ค.
+ */
+ private fun calculateReductionRatio(original: NodeSize, optimized: NodeSize): Double {
+ return if (original.value > 0) {
+ (original.value - optimized.value).toDouble() / original.value.toDouble()
+ } else {
+ 0.0
+ }
+ }
+
+ /**
+ * ์ต์ ํ ํต๊ณ ๋ฐ์ดํฐ ํด๋์ค
+ */
+ data class OptimizationStatistics(
+ val originalNodeCount: NodeSize,
+ val optimizedNodeCount: NodeSize,
+ val reductionRatio: Double,
+ val originalDepth: TreeDepth,
+ val optimizedDepth: TreeDepth,
+ val depthReduction: TreeDepth
+ ) {
+ /**
+ * ๋
ธ๋ ์ ๊ฐ์๋์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getNodeReduction(): NodeSize {
+ return NodeSize.of(originalNodeCount.value - optimizedNodeCount.value)
+ }
+
+ /**
+ * ์ต์ ํ ํจ๊ณผ๊ฐ ์๋์ง ํ์ธํฉ๋๋ค.
+ */
+ fun hasOptimizationEffect(): Boolean {
+ return reductionRatio > 0.0 || depthReduction.value > 0
+ }
+ }
+
+ companion object {
+ private const val MAX_OPTIMIZATION_PASSES = 10
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt
new file mode 100644
index 00000000..847a071c
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt
@@ -0,0 +1,422 @@
+package hs.kr.entrydsm.domain.ast.services
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor
+import hs.kr.entrydsm.domain.ast.values.NodeSize
+import hs.kr.entrydsm.domain.ast.values.NodeType
+import hs.kr.entrydsm.domain.ast.values.TreeDepth
+import hs.kr.entrydsm.global.annotation.service.Service
+import hs.kr.entrydsm.global.annotation.service.type.ServiceType
+
+/**
+ * AST ํธ๋ฆฌ๋ฅผ ์ํํ๋ ์๋น์ค์
๋๋ค.
+ *
+ * ๋ค์ํ ์ํ ๋ฐฉ๋ฒ์ ์ ๊ณตํ๋ฉฐ, ์ํ ์ค ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ๋
+ * ๋
ธ๋๋ฅผ ์ฐพ๊ฑฐ๋ ๋ณํํ๋ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Service(
+ name = "AST ํธ๋ฆฌ ์ํ ์๋น์ค",
+ type = ServiceType.DOMAIN_SERVICE
+)
+class TreeTraverser {
+
+ /**
+ * ์ ์ ์ํ๋ฅผ ์ํํฉ๋๋ค.
+ *
+ * @param root ์ํํ ๋ฃจํธ ๋
ธ๋
+ * @param visitor ๋ฐฉ๋ฌธ์
+ */
+ fun preOrderTraversal(root: ASTNode, visitor: ASTVisitor) {
+ visitNode(root, visitor)
+ root.getChildren().forEach { child ->
+ preOrderTraversal(child, visitor)
+ }
+ }
+
+ /**
+ * ์ค์ ์ํ๋ฅผ ์ํํฉ๋๋ค (์ด์ง ํธ๋ฆฌ์ ์ ํฉ).
+ *
+ * @param root ์ํํ ๋ฃจํธ ๋
ธ๋
+ * @param visitor ๋ฐฉ๋ฌธ์
+ */
+ fun inOrderTraversal(root: ASTNode, visitor: ASTVisitor) {
+ val children = root.getChildren()
+
+ if (children.isNotEmpty()) {
+ inOrderTraversal(children[0], visitor)
+ }
+
+ visitNode(root, visitor)
+
+ if (children.size > 1) {
+ for (i in 1 until children.size) {
+ inOrderTraversal(children[i], visitor)
+ }
+ }
+ }
+
+ /**
+ * ํ์ ์ํ๋ฅผ ์ํํฉ๋๋ค.
+ *
+ * @param root ์ํํ ๋ฃจํธ ๋
ธ๋
+ * @param visitor ๋ฐฉ๋ฌธ์
+ */
+ fun postOrderTraversal(root: ASTNode, visitor: ASTVisitor) {
+ root.getChildren().forEach { child ->
+ postOrderTraversal(child, visitor)
+ }
+ visitNode(root, visitor)
+ }
+
+ /**
+ * ๋ ๋ฒจ ์ํ๋ฅผ ์ํํฉ๋๋ค.
+ *
+ * @param root ์ํํ ๋ฃจํธ ๋
ธ๋
+ * @param visitor ๋ฐฉ๋ฌธ์
+ */
+ fun levelOrderTraversal(root: ASTNode, visitor: ASTVisitor) {
+ val queue = mutableListOf()
+ queue.add(root)
+
+ while (queue.isNotEmpty()) {
+ val current = queue.removeFirst()
+ visitNode(current, visitor)
+ queue.addAll(current.getChildren())
+ }
+ }
+
+ /**
+ * ๊น์ด ์ฐ์ ํ์์ ์ํํฉ๋๋ค.
+ *
+ * @param root ํ์ํ ๋ฃจํธ ๋
ธ๋
+ * @param condition ์กฐ๊ฑด ํจ์
+ * @return ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ฒซ ๋ฒ์งธ ๋
ธ๋
+ */
+ fun depthFirstSearch(root: ASTNode, condition: (ASTNode) -> Boolean): ASTNode? {
+ if (condition(root)) {
+ return root
+ }
+
+ for (child in root.getChildren()) {
+ val result = depthFirstSearch(child, condition)
+ if (result != null) {
+ return result
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * ๋๋น ์ฐ์ ํ์์ ์ํํฉ๋๋ค.
+ *
+ * @param root ํ์ํ ๋ฃจํธ ๋
ธ๋
+ * @param condition ์กฐ๊ฑด ํจ์
+ * @return ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์ฒซ ๋ฒ์งธ ๋
ธ๋
+ */
+ fun breadthFirstSearch(root: ASTNode, condition: (ASTNode) -> Boolean): ASTNode? {
+ val queue = mutableListOf()
+ queue.add(root)
+
+ while (queue.isNotEmpty()) {
+ val current = queue.removeFirst()
+ if (condition(current)) {
+ return current
+ }
+ queue.addAll(current.getChildren())
+ }
+
+ return null
+ }
+
+ /**
+ * ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๋ชจ๋ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ํ์ํ ๋ฃจํธ ๋
ธ๋
+ * @param condition ์กฐ๊ฑด ํจ์
+ * @return ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๋ชจ๋ ๋
ธ๋
+ */
+ fun findAll(root: ASTNode, condition: (ASTNode) -> Boolean): List {
+ val result = mutableListOf()
+
+ if (condition(root)) {
+ result.add(root)
+ }
+
+ for (child in root.getChildren()) {
+ result.addAll(findAll(child, condition))
+ }
+
+ return result
+ }
+
+ /**
+ * ํน์ ํ์
์ ๋ชจ๋ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ํ์ํ ๋ฃจํธ ๋
ธ๋
+ * @param nodeClass ์ฐพ์ ๋
ธ๋ ํด๋์ค
+ * @return ํด๋น ํ์
์ ๋ชจ๋ ๋
ธ๋
+ */
+ fun findByType(root: ASTNode, nodeClass: Class): List {
+ val result = mutableListOf()
+
+ if (nodeClass.isInstance(root)) {
+ @Suppress("UNCHECKED_CAST")
+ result.add(root as T)
+ }
+
+ for (child in root.getChildren()) {
+ result.addAll(findByType(child, nodeClass))
+ }
+
+ return result
+ }
+
+ /**
+ * ๋
ธ๋์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ๋ฃจํธ ๋
ธ๋
+ * @param target ์ฐพ์ ๋
ธ๋
+ * @return ๋ฃจํธ์์ ํ๊ฒ๊น์ง์ ๊ฒฝ๋ก
+ */
+ fun findPath(root: ASTNode, target: ASTNode): List? {
+ val path = mutableListOf()
+
+ if (findPathHelper(root, target, path)) {
+ return path
+ }
+
+ return null
+ }
+
+ /**
+ * ๊ฒฝ๋ก ์ฐพ๊ธฐ ํฌํผ ํจ์
+ */
+ private fun findPathHelper(current: ASTNode, target: ASTNode, path: MutableList): Boolean {
+ path.add(current)
+
+ if (current == target) {
+ return true
+ }
+
+ for (child in current.getChildren()) {
+ if (findPathHelper(child, target, path)) {
+ return true
+ }
+ }
+
+ path.removeAt(path.size - 1)
+ return false
+ }
+
+ /**
+ * ํน์ ๊น์ด์ ๋ชจ๋ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ๋ฃจํธ ๋
ธ๋
+ * @param depth ์ฐพ์ ๊น์ด
+ * @return ํด๋น ๊น์ด์ ๋ชจ๋ ๋
ธ๋
+ */
+ fun findAtDepth(root: ASTNode, depth: Int): List {
+ val result = mutableListOf()
+ findAtDepthHelper(root, depth, 0, result)
+ return result
+ }
+
+ /**
+ * ๊น์ด๋ณ ๋
ธ๋ ์ฐพ๊ธฐ ํฌํผ ํจ์
+ */
+ private fun findAtDepthHelper(current: ASTNode, targetDepth: Int, currentDepth: Int, result: MutableList) {
+ if (currentDepth == targetDepth) {
+ result.add(current)
+ return
+ }
+
+ for (child in current.getChildren()) {
+ findAtDepthHelper(child, targetDepth, currentDepth + 1, result)
+ }
+ }
+
+ /**
+ * ๋ฆฌํ ๋
ธ๋๋ค์ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ๋ฃจํธ ๋
ธ๋
+ * @return ๋ชจ๋ ๋ฆฌํ ๋
ธ๋
+ */
+ fun findLeaves(root: ASTNode): List {
+ return findAll(root) { it.isLeaf() }
+ }
+
+ /**
+ * ๊ฐ์ฅ ๊น์ ๋
ธ๋๋ฅผ ์ฐพ์ต๋๋ค.
+ *
+ * @param root ๋ฃจํธ ๋
ธ๋
+ * @return ๊ฐ์ฅ ๊น์ ๋
ธ๋์ ๊ทธ ๊น์ด
+ */
+ fun findDeepestNode(root: ASTNode): Pair {
+ var deepestNode = root
+ var maxDepth = TreeDepth.zero()
+
+ findDeepestNodeHelper(root, TreeDepth.zero(), { node, depth ->
+ if (depth.isGreaterThan(maxDepth)) {
+ deepestNode = node
+ maxDepth = depth
+ }
+ })
+
+ return Pair(deepestNode, maxDepth)
+ }
+
+ /**
+ * ๊ฐ์ฅ ๊น์ ๋
ธ๋ ์ฐพ๊ธฐ ํฌํผ ํจ์
+ */
+ private fun findDeepestNodeHelper(current: ASTNode, currentDepth: TreeDepth, callback: (ASTNode, TreeDepth) -> Unit) {
+ callback(current, currentDepth)
+
+ for (child in current.getChildren()) {
+ findDeepestNodeHelper(child, currentDepth.increment(), callback)
+ }
+ }
+
+ /**
+ * ํธ๋ฆฌ์ ํต๊ณ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param root ๋ฃจํธ ๋
ธ๋
+ * @return ํธ๋ฆฌ ํต๊ณ
+ */
+ fun calculateStatistics(root: ASTNode): TreeStatistics {
+ var nodeCount = 0
+ var leafCount = 0
+ var maxDepth = TreeDepth.zero()
+ var totalDepth = 0
+ val nodeTypeCounts = mutableMapOf()
+
+ // ๊น์ด๋ฅผ ์ถ์ ํ๋ฉด์ ์ํํ๋ ํฌํผ ํจ์
+ fun traverseWithDepth(node: ASTNode, currentDepth: Int) {
+ nodeCount++
+ totalDepth += currentDepth
+
+ // ์ต๋ ๊น์ด ์
๋ฐ์ดํธ
+ if (currentDepth > maxDepth.value) {
+ maxDepth = TreeDepth.of(currentDepth)
+ }
+
+ // ๋
ธ๋ ํ์
๋ณ ์ฒ๋ฆฌ
+ when (node) {
+ is hs.kr.entrydsm.domain.ast.entities.NumberNode -> {
+ leafCount++
+ updateNodeTypeCount(NodeType.NUMBER, nodeTypeCounts)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> {
+ leafCount++
+ updateNodeTypeCount(NodeType.BOOLEAN, nodeTypeCounts)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.VariableNode -> {
+ leafCount++
+ updateNodeTypeCount(NodeType.VARIABLE, nodeTypeCounts)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> {
+ updateNodeTypeCount(NodeType.BINARY_OP, nodeTypeCounts)
+ // ์์ ๋
ธ๋๋ค์ ๋ ๊น์ ๋ ๋ฒจ์์ ์ํ
+ traverseWithDepth(node.left, currentDepth + 1)
+ traverseWithDepth(node.right, currentDepth + 1)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> {
+ updateNodeTypeCount(NodeType.UNARY_OP, nodeTypeCounts)
+ // ์์ ๋
ธ๋๋ฅผ ๋ ๊น์ ๋ ๋ฒจ์์ ์ํ
+ traverseWithDepth(node.operand, currentDepth + 1)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> {
+ updateNodeTypeCount(NodeType.FUNCTION_CALL, nodeTypeCounts)
+ // ๋ชจ๋ ์ธ์๋ค์ ๋ ๊น์ ๋ ๋ฒจ์์ ์ํ
+ node.args.forEach { arg ->
+ traverseWithDepth(arg, currentDepth + 1)
+ }
+ }
+ is hs.kr.entrydsm.domain.ast.entities.IfNode -> {
+ updateNodeTypeCount(NodeType.IF, nodeTypeCounts)
+ // ์กฐ๊ฑด, ์ฐธ ๊ฐ, ๊ฑฐ์ง ๊ฐ์ ๋ ๊น์ ๋ ๋ฒจ์์ ์ํ
+ traverseWithDepth(node.condition, currentDepth + 1)
+ traverseWithDepth(node.trueValue, currentDepth + 1)
+ traverseWithDepth(node.falseValue, currentDepth + 1)
+ }
+ is hs.kr.entrydsm.domain.ast.entities.ArgumentsNode -> {
+ updateNodeTypeCount(NodeType.ARGUMENTS, nodeTypeCounts)
+ // ๋ชจ๋ ์ธ์๋ค์ ๋ ๊น์ ๋ ๋ฒจ์์ ์ํ
+ node.arguments.forEach { arg ->
+ traverseWithDepth(arg, currentDepth + 1)
+ }
+ }
+ }
+ }
+
+ // ๋ฃจํธ ๋
ธ๋๋ถํฐ ๊น์ด 0์์ ์์
+ traverseWithDepth(root, 0)
+
+ return TreeStatistics(
+ nodeCount = NodeSize.of(nodeCount),
+ leafCount = NodeSize.of(leafCount),
+ maxDepth = maxDepth,
+ averageDepth = if (nodeCount > 0) TreeDepth.of(totalDepth / nodeCount) else TreeDepth.zero(),
+ nodeTypeCounts = nodeTypeCounts.toMap()
+ )
+ }
+
+ /**
+ * ๋
ธ๋ ํ์
์นด์ดํธ ์
๋ฐ์ดํธ
+ */
+ private fun updateNodeTypeCount(type: NodeType, counts: MutableMap) {
+ counts[type] = counts.getOrDefault(type, 0) + 1
+ }
+
+ /**
+ * ๋
ธ๋ ๋ฐฉ๋ฌธ ์ฒ๋ฆฌ
+ */
+ private fun visitNode(node: ASTNode, visitor: ASTVisitor) {
+ node.accept(visitor)
+ }
+
+ /**
+ * ํธ๋ฆฌ ํต๊ณ ๋ฐ์ดํฐ ํด๋์ค
+ */
+ data class TreeStatistics(
+ val nodeCount: NodeSize,
+ val leafCount: NodeSize,
+ val maxDepth: TreeDepth,
+ val averageDepth: TreeDepth,
+ val nodeTypeCounts: Map
+ ) {
+ /**
+ * ๋ฆฌํ ๋
ธ๋ ๋น์จ์ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun getLeafRatio(): Double {
+ return if (nodeCount.value > 0) {
+ leafCount.value.toDouble() / nodeCount.value.toDouble()
+ } else {
+ 0.0
+ }
+ }
+
+ /**
+ * ๊ฐ์ฅ ๋ง์ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getMostCommonNodeType(): NodeType? {
+ return nodeTypeCounts.maxByOrNull { it.value }?.key
+ }
+
+ /**
+ * ํธ๋ฆฌ ๋ฐ๋๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun getTreeDensity(): Double {
+ return if (maxDepth.value > 0) {
+ nodeCount.value.toDouble() / maxDepth.value.toDouble()
+ } else {
+ 0.0
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt
new file mode 100644
index 00000000..15cfa645
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt
@@ -0,0 +1,415 @@
+package hs.kr.entrydsm.domain.ast.specifications
+
+import hs.kr.entrydsm.domain.ast.entities.*
+import hs.kr.entrydsm.domain.ast.utils.FunctionValidationRules
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.SpecificationResult
+import hs.kr.entrydsm.global.annotation.specification.SpecificationContract
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * AST ๋
ธ๋ ์ ํจ์ฑ ์ฌ์์ ์ ์ํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * AST ๋
ธ๋๊ฐ ๋๋ฉ์ธ ๊ท์น์ ๋ง์กฑํ๋์ง ๊ฒ์ฆํ๋ฉฐ,
+ * ๋ณตํฉ ์ฌ์์ ํตํด ๋ณต์กํ ๊ฒ์ฆ ๋ก์ง์ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Specification(
+ name = "AST ๋
ธ๋ ์ ํจ์ฑ ์ฌ์",
+ description = "AST ๋
ธ๋๊ฐ ๋๋ฉ์ธ ๊ท์น์ ๋ง์กฑํ๋์ง ๊ฒ์ฆํ๋ ์ฌ์",
+ domain = "ast",
+ priority = Priority.HIGH
+)
+class ASTValiditySpec : SpecificationContract {
+
+ override fun isSatisfiedBy(node: ASTNode): Boolean {
+ return when (node) {
+ is NumberNode -> isValidNumberNode(node)
+ is BooleanNode -> isValidBooleanNode(node)
+ is VariableNode -> isValidVariableNode(node)
+ is BinaryOpNode -> isValidBinaryOpNode(node)
+ is UnaryOpNode -> isValidUnaryOpNode(node)
+ is FunctionCallNode -> isValidFunctionCallNode(node)
+ is IfNode -> isValidIfNode(node)
+ is ArgumentsNode -> isValidArgumentsNode(node)
+ else -> false
+ }
+ }
+
+ fun getWhyNotSatisfied(node: ASTNode): String {
+ return when (node) {
+ is NumberNode -> getNumberNodeViolations(node)
+ is BooleanNode -> getBooleanNodeViolations(node)
+ is VariableNode -> getVariableNodeViolations(node)
+ is BinaryOpNode -> getBinaryOpNodeViolations(node)
+ is UnaryOpNode -> getUnaryOpNodeViolations(node)
+ is FunctionCallNode -> getFunctionCallNodeViolations(node)
+ is IfNode -> getIfNodeViolations(node)
+ is ArgumentsNode -> getArgumentsNodeViolations(node)
+ else -> Msg.unsupportedNodeType(node::class.simpleName ?: UNKNOWN)
+ }
+ }
+
+ fun getValidationResult(node: ASTNode): SpecificationResult {
+ val isValid = isSatisfiedBy(node)
+ val message = if (isValid) Msg.VALIDATION_SUCCESS else getWhyNotSatisfied(node)
+ return SpecificationResult(
+ success = isValid,
+ message = message,
+ specification = this
+ )
+ }
+
+ // ---- validators ----
+
+ private fun isValidNumberNode(node: NumberNode): Boolean {
+ return node.value.isFinite() &&
+ !node.value.isNaN() &&
+ node.value >= MIN_NUMBER_VALUE &&
+ node.value <= MAX_NUMBER_VALUE
+ }
+
+ private fun isValidBooleanNode(@Suppress("UNUSED_PARAMETER") node: BooleanNode): Boolean = true
+
+ private fun isValidVariableNode(node: VariableNode): Boolean {
+ return node.name.isNotBlank() &&
+ node.name.length <= MAX_VARIABLE_NAME_LENGTH &&
+ isValidVariableName(node.name) &&
+ !isReservedWord(node.name)
+ }
+
+ private fun isValidBinaryOpNode(node: BinaryOpNode): Boolean {
+ return node.operator.isNotBlank() &&
+ isSupportedBinaryOperator(node.operator) &&
+ isSatisfiedBy(node.left) &&
+ isSatisfiedBy(node.right) &&
+ isValidBinaryOperation(node.left, node.operator, node.right)
+ }
+
+ private fun isValidUnaryOpNode(node: UnaryOpNode): Boolean {
+ return node.operator.isNotBlank() &&
+ isSupportedUnaryOperator(node.operator) &&
+ isSatisfiedBy(node.operand) &&
+ isValidUnaryOperation(node.operator, node.operand)
+ }
+
+ private fun isValidFunctionCallNode(node: FunctionCallNode): Boolean {
+ return node.name.isNotBlank() &&
+ node.name.length <= MAX_FUNCTION_NAME_LENGTH &&
+ isValidFunctionName(node.name) &&
+ node.args.size <= MAX_FUNCTION_ARGS &&
+ node.args.all { isSatisfiedBy(it) } &&
+ isValidFunctionCall(node.name, node.args)
+ }
+
+ private fun isValidIfNode(node: IfNode): Boolean {
+ return isSatisfiedBy(node.condition) &&
+ isSatisfiedBy(node.trueValue) &&
+ isSatisfiedBy(node.falseValue) &&
+ node.getDepth() <= MAX_NODE_DEPTH &&
+ node.getSize() <= MAX_NODE_SIZE
+ }
+
+ private fun isValidArgumentsNode(node: ArgumentsNode): Boolean {
+ return node.arguments.size <= MAX_ARGUMENTS_COUNT &&
+ node.arguments.all { isSatisfiedBy(it) }
+ }
+
+ // ---- violation builders ----
+
+ private fun getNumberNodeViolations(node: NumberNode): String {
+ val violations = mutableListOf()
+ if (!node.value.isFinite()) {
+ violations.add(Msg.numberNotFinite(node.value))
+ }
+ if (node.value.isNaN()) {
+ violations.add(Msg.numberIsNaN())
+ }
+ if (node.value < MIN_NUMBER_VALUE) {
+ violations.add(Msg.numberBelowMin(node.value, MIN_NUMBER_VALUE))
+ }
+ if (node.value > MAX_NUMBER_VALUE) {
+ violations.add(Msg.numberAboveMax(node.value, MAX_NUMBER_VALUE))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getBooleanNodeViolations(@Suppress("UNUSED_PARAMETER") node: BooleanNode): String {
+ return Msg.BOOLEAN_ALWAYS_VALID
+ }
+
+ private fun getVariableNodeViolations(node: VariableNode): String {
+ val violations = mutableListOf()
+ if (node.name.isBlank()) {
+ violations.add(Msg.variableNameBlank())
+ }
+ if (node.name.length > MAX_VARIABLE_NAME_LENGTH) {
+ violations.add(Msg.variableNameTooLong(node.name.length, MAX_VARIABLE_NAME_LENGTH))
+ }
+ if (!isValidVariableName(node.name)) {
+ violations.add(Msg.variableNameInvalid(node.name))
+ }
+ if (isReservedWord(node.name)) {
+ violations.add(Msg.variableNameReserved(node.name))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getBinaryOpNodeViolations(node: BinaryOpNode): String {
+ val violations = mutableListOf()
+ if (node.operator.isBlank()) {
+ violations.add(Msg.operatorBlank())
+ }
+ if (!isSupportedBinaryOperator(node.operator)) {
+ violations.add(Msg.binaryOperatorUnsupported(node.operator))
+ }
+ if (!isSatisfiedBy(node.left)) {
+ violations.add(Msg.leftOperandInvalid(getWhyNotSatisfied(node.left)))
+ }
+ if (!isSatisfiedBy(node.right)) {
+ violations.add(Msg.rightOperandInvalid(getWhyNotSatisfied(node.right)))
+ }
+ if (!isValidBinaryOperation(node.left, node.operator, node.right)) {
+ violations.add(Msg.binaryOperationInvalid(node.left, node.operator, node.right))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getUnaryOpNodeViolations(node: UnaryOpNode): String {
+ val violations = mutableListOf()
+ if (node.operator.isBlank()) {
+ violations.add(Msg.operatorBlank())
+ }
+ if (!isSupportedUnaryOperator(node.operator)) {
+ violations.add(Msg.unaryOperatorUnsupported(node.operator))
+ }
+ if (!isSatisfiedBy(node.operand)) {
+ violations.add(Msg.operandInvalid(getWhyNotSatisfied(node.operand)))
+ }
+ if (!isValidUnaryOperation(node.operator, node.operand)) {
+ violations.add(Msg.unaryOperationInvalid(node.operator, node.operand))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getFunctionCallNodeViolations(node: FunctionCallNode): String {
+ val violations = mutableListOf()
+ if (node.name.isBlank()) {
+ violations.add(Msg.functionNameBlank())
+ }
+ if (node.name.length > MAX_FUNCTION_NAME_LENGTH) {
+ violations.add(Msg.functionNameTooLong(node.name.length, MAX_FUNCTION_NAME_LENGTH))
+ }
+ if (!isValidFunctionName(node.name)) {
+ violations.add(Msg.functionNameInvalid(node.name))
+ }
+ if (node.args.size > MAX_FUNCTION_ARGS) {
+ violations.add(Msg.functionArgsTooMany(node.args.size, MAX_FUNCTION_ARGS))
+ }
+ node.args.forEachIndexed { index, arg ->
+ if (!isSatisfiedBy(arg)) {
+ violations.add(Msg.functionArgInvalid(index, getWhyNotSatisfied(arg)))
+ }
+ }
+ if (!isValidFunctionCall(node.name, node.args)) {
+ violations.add(Msg.functionCallInvalid(node.name, node.args))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getIfNodeViolations(node: IfNode): String {
+ val violations = mutableListOf()
+ if (!isSatisfiedBy(node.condition)) {
+ violations.add(Msg.ifConditionInvalid(getWhyNotSatisfied(node.condition)))
+ }
+ if (!isSatisfiedBy(node.trueValue)) {
+ violations.add(Msg.ifTrueInvalid(getWhyNotSatisfied(node.trueValue)))
+ }
+ if (!isSatisfiedBy(node.falseValue)) {
+ violations.add(Msg.ifFalseInvalid(getWhyNotSatisfied(node.falseValue)))
+ }
+ val depth = node.getDepth()
+ if (depth > MAX_NODE_DEPTH) {
+ violations.add(Msg.nodeDepthExceeded(depth, MAX_NODE_DEPTH))
+ }
+ val size = node.getSize()
+ if (size > MAX_NODE_SIZE) {
+ violations.add(Msg.nodeSizeExceeded(size, MAX_NODE_SIZE))
+ }
+ return violations.joinToString("; ")
+ }
+
+ private fun getArgumentsNodeViolations(node: ArgumentsNode): String {
+ val violations = mutableListOf()
+ if (node.arguments.size > MAX_ARGUMENTS_COUNT) {
+ violations.add(Msg.argumentsTooMany(node.arguments.size, MAX_ARGUMENTS_COUNT))
+ }
+ node.arguments.forEachIndexed { index, arg ->
+ if (!isSatisfiedBy(arg)) {
+ violations.add(Msg.argumentInvalid(index, getWhyNotSatisfied(arg)))
+ }
+ }
+ return violations.joinToString("; ")
+ }
+
+ // ---- helpers ----
+
+ private fun isValidVariableName(name: String): Boolean {
+ if (name.isEmpty()) return false
+ if (!name[0].isLetter() && name[0] != '_') return false
+ return name.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ private fun isValidFunctionName(name: String): Boolean {
+ if (name.isEmpty()) return false
+ if (!name[0].isLetter()) return false
+ return name.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ private fun isReservedWord(name: String): Boolean = RESERVED_WORDS.contains(name.lowercase())
+
+ private fun isSupportedBinaryOperator(operator: String): Boolean = BINARY_OPERATORS.contains(operator)
+
+ private fun isSupportedUnaryOperator(operator: String): Boolean = UNARY_OPERATORS.contains(operator)
+
+ private fun isValidBinaryOperation(left: ASTNode, operator: String, right: ASTNode): Boolean {
+ return when (operator) {
+ "/" -> !isZeroConstant(right)
+ "%" -> !isZeroConstant(right)
+ "^" -> !(isZeroConstant(left) && isZeroConstant(right))
+ else -> true
+ }
+ }
+
+ private fun isValidUnaryOperation(operator: String, operand: ASTNode): Boolean {
+ return when (operator) {
+ "!" -> true
+ "-", "+" -> true
+ else -> false
+ }
+ }
+
+ private fun isValidFunctionCall(name: String, args: List): Boolean {
+ return FunctionValidationRules.isValidFunctionCall(name, args)
+ }
+
+ private fun isZeroConstant(node: ASTNode): Boolean {
+ return node is NumberNode && node.value == 0.0
+ }
+
+ companion object {
+ // --- constraints ---
+ private const val MAX_NUMBER_VALUE = 1e15
+ private const val MIN_NUMBER_VALUE = -1e15
+ private const val MAX_VARIABLE_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_NAME_LENGTH = 50
+ private const val MAX_FUNCTION_ARGS = 10
+ private const val MAX_ARGUMENTS_COUNT = 100
+ private const val MAX_NODE_SIZE = 1000
+ private const val MAX_NODE_DEPTH = 50
+
+ private val RESERVED_WORDS = setOf(
+ "if", "else", "while", "for", "do", "break", "continue",
+ "function", "return", "var", "let", "const", "true", "false",
+ "null", "undefined", "this", "new", "typeof", "instanceof",
+ "try", "catch", "finally", "throw", "switch", "case", "default"
+ )
+
+ private val BINARY_OPERATORS = setOf(
+ "+", "-", "*", "/", "%", "^",
+ "==", "!=", "<", "<=", ">", ">=",
+ "&&", "||"
+ )
+
+ private val UNARY_OPERATORS = setOf("-", "+", "!")
+
+ private const val UNKNOWN = "Unknown"
+
+ // --- messages / builders ---
+ object Msg {
+ // common
+ const val VALIDATION_SUCCESS = "๊ฒ์ฆ ์ฑ๊ณต"
+ fun unsupportedNodeType(type: String) = "์ง์๋์ง ์๋ ๋
ธ๋ ํ์
์
๋๋ค: $type"
+
+ // number
+ fun numberNotFinite(value: Double) = "์ซ์ ๊ฐ์ด ์ ํํ์ง ์์ต๋๋ค: $value"
+ fun numberIsNaN() = "์ซ์ ๊ฐ์ด NaN์
๋๋ค"
+ fun numberBelowMin(value: Double, min: Double) =
+ "์ซ์ ๊ฐ์ด ์ต์๊ฐ ๋ฏธ๋ง์
๋๋ค: $value < $min"
+ fun numberAboveMax(value: Double, max: Double) =
+ "์ซ์ ๊ฐ์ด ์ต๋๊ฐ ์ด๊ณผ์
๋๋ค: $value > $max"
+
+ // boolean
+ const val BOOLEAN_ALWAYS_VALID = ""
+
+ // variable
+ fun variableNameBlank() = "๋ณ์๋ช
์ด ๋น์ด์์ต๋๋ค"
+ fun variableNameTooLong(actual: Int, max: Int) =
+ "๋ณ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+ fun variableNameInvalid(name: String) =
+ "์ ํจํ์ง ์์ ๋ณ์๋ช
ํ์์
๋๋ค: $name"
+ fun variableNameReserved(name: String) =
+ "์์ฝ์ด๋ ๋ณ์๋ช
์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค: $name"
+
+ // operators (common)
+ fun operatorBlank() = "์ฐ์ฐ์๊ฐ ๋น์ด์์ต๋๋ค"
+
+ // binary
+ fun binaryOperatorUnsupported(op: String) =
+ "์ง์๋์ง ์๋ ์ดํญ ์ฐ์ฐ์์
๋๋ค: $op"
+ fun leftOperandInvalid(reason: String) =
+ "์ข์ธก ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun rightOperandInvalid(reason: String) =
+ "์ฐ์ธก ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun binaryOperationInvalid(left: ASTNode, op: String, right: ASTNode) =
+ "์ ํจํ์ง ์์ ์ดํญ ์ฐ์ฐ์
๋๋ค: $left $op $right"
+
+ // unary
+ fun unaryOperatorUnsupported(op: String) =
+ "์ง์๋์ง ์๋ ๋จํญ ์ฐ์ฐ์์
๋๋ค: $op"
+ fun operandInvalid(reason: String) =
+ "ํผ์ฐ์ฐ์๊ฐ ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun unaryOperationInvalid(op: String, operand: ASTNode) =
+ "์ ํจํ์ง ์์ ๋จํญ ์ฐ์ฐ์
๋๋ค: $op$operand"
+
+ // function call
+ fun functionNameBlank() = "ํจ์๋ช
์ด ๋น์ด์์ต๋๋ค"
+ fun functionNameTooLong(actual: Int, max: Int) =
+ "ํจ์๋ช
์ด ์ต๋ ๊ธธ์ด๋ฅผ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+ fun functionNameInvalid(name: String) =
+ "์ ํจํ์ง ์์ ํจ์๋ช
ํ์์
๋๋ค: $name"
+ fun functionArgsTooMany(actual: Int, max: Int) =
+ "ํจ์ ์ธ์ ๊ฐ์๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+ fun functionArgInvalid(index: Int, reason: String) =
+ "์ธ์ $index ๊ฐ ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun functionCallInvalid(name: String, args: List) =
+ "์ ํจํ์ง ์์ ํจ์ ํธ์ถ์
๋๋ค: $name(${args.joinToString(", ")})"
+
+ // if
+ fun ifConditionInvalid(reason: String) =
+ "์กฐ๊ฑด์์ด ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun ifTrueInvalid(reason: String) =
+ "์ฐธ ๊ฐ์ด ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun ifFalseInvalid(reason: String) =
+ "๊ฑฐ์ง ๊ฐ์ด ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ fun nodeDepthExceeded(actual: Int, max: Int) =
+ "๋
ธ๋ ๊น์ด๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+ fun nodeSizeExceeded(actual: Int, max: Int) =
+ "๋
ธ๋ ํฌ๊ธฐ๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+
+ // arguments
+ fun argumentsTooMany(actual: Int, max: Int) =
+ "์ธ์ ๊ฐ์๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+ fun argumentInvalid(index: Int, reason: String) =
+ "์ธ์ $index ๊ฐ ์ ํจํ์ง ์์ต๋๋ค: $reason"
+ }
+ }
+
+ // SpecificationContract ๊ตฌํ
+ override fun getName(): String = "AST ๋
ธ๋ ์ ํจ์ฑ ์ฌ์"
+ override fun getDescription(): String = "AST ๋
ธ๋๊ฐ ๋๋ฉ์ธ ๊ท์น์ ๋ง์กฑํ๋์ง ๊ฒ์ฆํ๋ ์ฌ์"
+ override fun getDomain(): String = "ast"
+ override fun getPriority(): Priority = Priority.HIGH
+}
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt
new file mode 100644
index 00000000..cdb25a67
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt
@@ -0,0 +1,503 @@
+package hs.kr.entrydsm.domain.ast.specifications
+
+import hs.kr.entrydsm.domain.ast.entities.*
+import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode
+import hs.kr.entrydsm.domain.ast.entities.IfNode
+import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode
+import hs.kr.entrydsm.domain.ast.entities.VariableNode
+import hs.kr.entrydsm.global.annotation.specification.Specification
+import hs.kr.entrydsm.global.annotation.specification.SpecificationResult
+import hs.kr.entrydsm.global.annotation.specification.SpecificationContract
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+
+/**
+ * AST ๋
ธ๋ ๊ตฌ์กฐ ์ฌ์์ ์ ์ํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * AST ๋
ธ๋์ ๊ตฌ์กฐ์ ์ ํฉ์ฑ๊ณผ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ฉฐ,
+ * ๋
ธ๋ ๊ฐ์ ๊ด๊ณ์ ํธ๋ฆฌ ๊ตฌ์กฐ์ ์ ํจ์ฑ์ ํ์ธํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+@Specification(
+ name = "AST ๋
ธ๋ ๊ตฌ์กฐ ์ฌ์",
+ description = "AST ๋
ธ๋์ ๊ตฌ์กฐ์ ์ ํฉ์ฑ๊ณผ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ ์ฌ์",
+ domain = "ast",
+ priority = Priority.NORMAL
+)
+class NodeStructureSpec : SpecificationContract {
+
+ /**
+ * AST ๋
ธ๋๊ฐ ๊ตฌ์กฐ ์ฌ์์ ๋ง์กฑํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param node ๊ฒ์ฆํ AST ๋
ธ๋
+ * @return ๊ตฌ์กฐ ์ฌ์ ๋ง์กฑ ์ฌ๋ถ
+ */
+ override fun isSatisfiedBy(node: ASTNode): Boolean {
+ return when (node) {
+ is NumberNode -> isValidNumberStructure(node)
+ is BooleanNode -> isValidBooleanStructure(node)
+ is VariableNode -> isValidVariableStructure(node)
+ is BinaryOpNode -> isValidBinaryOpStructure(node)
+ is UnaryOpNode -> isValidUnaryOpStructure(node)
+ is FunctionCallNode -> isValidFunctionCallStructure(node)
+ is IfNode -> isValidIfStructure(node)
+ is ArgumentsNode -> isValidArgumentsStructure(node)
+ else -> false
+ }
+ }
+
+ /**
+ * ๊ตฌ์กฐ ์ฌ์์ ๋ง์กฑํ์ง ์๋ ์ด์ ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param node ๊ฒ์ฆํ AST ๋
ธ๋
+ * @return ๊ตฌ์กฐ ์ฌ์ ๋ถ๋ง์กฑ ์ด์
+ */
+ fun getWhyNotSatisfied(node: ASTNode): String {
+ return when (node) {
+ is NumberNode -> getNumberStructureViolations(node)
+ is BooleanNode -> getBooleanStructureViolations(node)
+ is VariableNode -> getVariableStructureViolations(node)
+ is BinaryOpNode -> getBinaryOpStructureViolations(node)
+ is UnaryOpNode -> getUnaryOpStructureViolations(node)
+ is FunctionCallNode -> getFunctionCallStructureViolations(node)
+ is IfNode -> getIfStructureViolations(node)
+ is ArgumentsNode -> getArgumentsStructureViolations(node)
+ else -> "$UNSUPPORTED_NODE_TYPE ${node::class.simpleName}"
+ }
+ }
+
+ /**
+ * ์์ธํ ๊ตฌ์กฐ ๊ฒ์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param node ๊ฒ์ฆํ AST ๋
ธ๋
+ * @return ๊ตฌ์กฐ ๊ฒ์ฆ ๊ฒฐ๊ณผ
+ */
+ fun getStructureValidationResult(node: ASTNode): SpecificationResult {
+ val isValid = isSatisfiedBy(node)
+ val violations = mutableListOf()
+
+ if (!isValid) {
+ violations.add(getWhyNotSatisfied(node))
+ }
+
+ // ์ถ๊ฐ ๊ตฌ์กฐ ๊ฒ์ฆ
+ violations.addAll(getStructuralIntegrityViolations(node))
+
+ val finalValid = isValid && violations.isEmpty()
+ val message = if (finalValid) STRUCTURE_VERIFICATION_SUCCESS else violations.joinToString(", ")
+
+ return SpecificationResult(
+ success = finalValid,
+ message = message,
+ specification = this
+ )
+ }
+
+ // ===== validators =====
+
+ private fun isValidNumberStructure(node: NumberNode): Boolean {
+ // ์ซ์ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํจ
+ return node.isLeaf() &&
+ node.getChildren().isEmpty() &&
+ node.isLiteral() &&
+ !node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional()
+ }
+
+ private fun isValidBooleanStructure(node: BooleanNode): Boolean {
+ // ๋ถ๋ฆฌ์ธ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํจ
+ return node.isLeaf() &&
+ node.getChildren().isEmpty() &&
+ node.isLiteral() &&
+ !node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional()
+ }
+
+ private fun isValidVariableStructure(node: VariableNode): Boolean {
+ // ๋ณ์ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํจ
+ return node.isLeaf() &&
+ node.getChildren().isEmpty() &&
+ !node.isLiteral() &&
+ !node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional()
+ }
+
+ private fun isValidBinaryOpStructure(node: BinaryOpNode): Boolean {
+ val children = node.getChildren()
+
+ return !node.isLeaf() &&
+ children.size == 2 &&
+ children[0] == node.left &&
+ children[1] == node.right &&
+ !node.isLiteral() &&
+ node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional() &&
+ hasValidBinaryOperatorPrecedence(node) &&
+ hasValidBinaryOperatorAssociativity(node)
+ }
+
+ private fun isValidUnaryOpStructure(node: UnaryOpNode): Boolean {
+ val children = node.getChildren()
+
+ return !node.isLeaf() &&
+ children.size == 1 &&
+ children[0] == node.operand &&
+ !node.isLiteral() &&
+ node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional() &&
+ hasValidUnaryOperatorPrecedence(node)
+ }
+
+ private fun isValidFunctionCallStructure(node: FunctionCallNode): Boolean {
+ val children = node.getChildren()
+
+ return !node.isLeaf() &&
+ children.size == node.args.size &&
+ children == node.args &&
+ !node.isLiteral() &&
+ !node.isOperator() &&
+ node.isFunctionCall() &&
+ !node.isConditional() &&
+ hasValidFunctionSignature(node)
+ }
+
+ private fun isValidIfStructure(node: IfNode): Boolean {
+ val children = node.getChildren()
+
+ return !node.isLeaf() &&
+ children.size == 3 &&
+ children[0] == node.condition &&
+ children[1] == node.trueValue &&
+ children[2] == node.falseValue &&
+ !node.isLiteral() &&
+ !node.isOperator() &&
+ !node.isFunctionCall() &&
+ node.isConditional() &&
+ hasValidConditionalStructure(node)
+ }
+
+ private fun isValidArgumentsStructure(node: ArgumentsNode): Boolean {
+ val children = node.getChildren()
+
+ return children.size == node.arguments.size &&
+ children == node.arguments &&
+ !node.isLiteral() &&
+ !node.isOperator() &&
+ !node.isFunctionCall() &&
+ !node.isConditional()
+ }
+
+ // ===== structural integrity violations =====
+
+ private fun getStructuralIntegrityViolations(node: ASTNode): List {
+ val violations = mutableListOf()
+
+ // ์ํ ์ฐธ์กฐ ๊ฒ์ฆ
+ if (hasCircularReference(node)) {
+ violations.add(ERR_CIRCULAR_REFERENCE)
+ }
+
+ // ๊น์ด ์ ํ ๊ฒ์ฆ
+ val depth = node.getDepth()
+ if (depth > MAX_STRUCTURE_DEPTH) {
+ violations.add(errDepthExceeded(depth, MAX_STRUCTURE_DEPTH))
+ }
+
+ // ๋๋น ์ ํ ๊ฒ์ฆ
+ val size = node.getSize()
+ if (size > MAX_STRUCTURE_SIZE) {
+ violations.add(errSizeExceeded(size, MAX_STRUCTURE_SIZE))
+ }
+
+ // ์์ ๋
ธ๋ ์ผ๊ด์ฑ ๊ฒ์ฆ
+ violations.addAll(validateChildrenConsistency(node))
+
+ return violations
+ }
+
+ // ===== per-node violations =====
+
+ private fun getNumberStructureViolations(node: NumberNode): String {
+ val violations = mutableListOf()
+
+ if (!node.isLeaf()) {
+ violations.add(ERR_NUMBER_NOT_LEAF)
+ }
+ if (node.getChildren().isNotEmpty()) {
+ violations.add(ERR_NUMBER_HAS_CHILDREN)
+ }
+ if (!node.isLiteral()) {
+ violations.add(ERR_NUMBER_NOT_LITERAL)
+ }
+ if (node.isOperator()) {
+ violations.add(ERR_NUMBER_IS_OPERATOR)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getBooleanStructureViolations(node: BooleanNode): String {
+ val violations = mutableListOf()
+
+ if (!node.isLeaf()) {
+ violations.add(ERR_BOOLEAN_NOT_LEAF)
+ }
+ if (node.getChildren().isNotEmpty()) {
+ violations.add(ERR_BOOLEAN_HAS_CHILDREN)
+ }
+ if (!node.isLiteral()) {
+ violations.add(ERR_BOOLEAN_NOT_LITERAL)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getVariableStructureViolations(node: VariableNode): String {
+ val violations = mutableListOf()
+
+ if (!node.isLeaf()) {
+ violations.add(ERR_VARIABLE_NOT_LEAF)
+ }
+ if (node.getChildren().isNotEmpty()) {
+ violations.add(ERR_VARIABLE_HAS_CHILDREN)
+ }
+ if (node.isLiteral()) {
+ violations.add(ERR_VARIABLE_IS_LITERAL)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getBinaryOpStructureViolations(node: BinaryOpNode): String {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ if (node.isLeaf()) {
+ violations.add(ERR_BINARY_IS_LEAF)
+ }
+ if (children.size != 2) {
+ violations.add(ERR_BINARY_CHILDREN_COUNT)
+ }
+ if (!node.isOperator()) {
+ violations.add(ERR_BINARY_NOT_OPERATOR)
+ }
+ if (!hasValidBinaryOperatorPrecedence(node)) {
+ violations.add(ERR_BINARY_INVALID_PRECEDENCE)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getUnaryOpStructureViolations(node: UnaryOpNode): String {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ if (node.isLeaf()) {
+ violations.add(ERR_UNARY_IS_LEAF)
+ }
+ if (children.size != 1) {
+ violations.add(ERR_UNARY_CHILDREN_COUNT)
+ }
+ if (!node.isOperator()) {
+ violations.add(ERR_UNARY_NOT_OPERATOR)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getFunctionCallStructureViolations(node: FunctionCallNode): String {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ if (node.isLeaf() && node.args.isNotEmpty()) {
+ violations.add(ERR_FUNC_LEAF_WITH_ARGS)
+ }
+ if (children.size != node.args.size) {
+ violations.add(ERR_FUNC_CHILDREN_COUNT)
+ }
+ if (!node.isFunctionCall()) {
+ violations.add(ERR_FUNC_NOT_FUNCTION)
+ }
+ if (!hasValidFunctionSignature(node)) {
+ violations.add(ERR_FUNC_INVALID_SIGNATURE)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getIfStructureViolations(node: IfNode): String {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ if (node.isLeaf()) {
+ violations.add(ERR_IF_IS_LEAF)
+ }
+ if (children.size != 3) {
+ violations.add(ERR_IF_CHILDREN_COUNT)
+ }
+ if (!node.isConditional()) {
+ violations.add(ERR_IF_NOT_CONDITIONAL)
+ }
+ if (!hasValidConditionalStructure(node)) {
+ violations.add(ERR_IF_INVALID_STRUCTURE)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ private fun getArgumentsStructureViolations(node: ArgumentsNode): String {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ if (children.size != node.arguments.size) {
+ violations.add(ERR_ARGS_CHILDREN_COUNT)
+ }
+
+ return violations.joinToString("; ")
+ }
+
+ // ===== integrity helpers =====
+
+ private fun hasValidBinaryOperatorPrecedence(node: BinaryOpNode): Boolean {
+ return node.getPrecedence() > 0
+ }
+
+ private fun hasValidBinaryOperatorAssociativity(node: BinaryOpNode): Boolean {
+ return node.isLeftAssociative() || node.isRightAssociative()
+ }
+
+ private fun hasValidUnaryOperatorPrecedence(node: UnaryOpNode): Boolean {
+ return node.getPrecedence() > 0
+ }
+
+ private fun hasValidFunctionSignature(node: FunctionCallNode): Boolean {
+ return when (node.name.uppercase()) {
+ "SQRT", "ABS", "SIN", "COS", "TAN", "LOG", "EXP" -> node.args.size == 1
+ "POW", "ATAN2", "MAX", "MIN" -> node.args.size >= 1
+ "IF" -> node.args.size == 3
+ else -> true
+ }
+ }
+
+ private fun hasValidConditionalStructure(node: IfNode): Boolean {
+ // ์กฐ๊ฑด๋ฌธ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ฒ์ฆ
+ return node.condition != null &&
+ node.trueValue != null &&
+ node.falseValue != null &&
+ node.getNestingDepth() <= MAX_CONDITIONAL_NESTING
+ }
+
+ private fun hasCircularReference(node: ASTNode): Boolean {
+ return hasCircularReferenceHelper(node, mutableSetOf())
+ }
+
+ /**
+ * ์ํ ์ฐธ์กฐ ๊ฒ์ฆ ํฌํผ
+ * - ๊ฐ ๊ฒฝ๋ก๋ณ ๋
๋ฆฝ ์ถ์ ์ ์ํด path๋ฅผ ๋ณต์ฌ
+ */
+ private fun hasCircularReferenceHelper(node: ASTNode, path: MutableSet): Boolean {
+ if (node in path) {
+ return true
+ }
+
+ path.add(node)
+ return node.getChildren().any { child ->
+ hasCircularReferenceHelper(child, path.toMutableSet())
+ }
+ }
+
+ /**
+ * ์์ ๋
ธ๋ ์ผ๊ด์ฑ ๊ฒ์ฆ
+ */
+ private fun validateChildrenConsistency(node: ASTNode): List {
+ val violations = mutableListOf()
+ val children = node.getChildren()
+
+ children.forEach { child ->
+ try {
+ if (!child.validate()) {
+ violations.add(errInvalidChildFound(child::class.simpleName ?: UNKNOWN))
+ }
+ } catch (e: Exception) {
+ violations.add(errChildValidationException(child::class.simpleName ?: UNKNOWN, e.message ?: ""))
+ }
+ }
+
+ return violations
+ }
+
+ companion object {
+ private const val MAX_STRUCTURE_DEPTH = 100
+ private const val MAX_STRUCTURE_SIZE = 10000
+ private const val MAX_CONDITIONAL_NESTING = 20
+
+ // ===== violation message constants =====
+ const val ERR_CIRCULAR_REFERENCE = "์ํ ์ฐธ์กฐ๊ฐ ๊ฐ์ง๋์์ต๋๋ค"
+
+ const val ERR_NUMBER_NOT_LEAF = "์ซ์ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํฉ๋๋ค"
+ const val ERR_NUMBER_HAS_CHILDREN = "์ซ์ ๋
ธ๋๋ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค"
+ const val ERR_NUMBER_NOT_LITERAL = "์ซ์ ๋
ธ๋๋ ๋ฆฌํฐ๋ด์ด์ด์ผ ํฉ๋๋ค"
+ const val ERR_NUMBER_IS_OPERATOR = "์ซ์ ๋
ธ๋๋ ์ฐ์ฐ์๊ฐ ๋ ์ ์์ต๋๋ค"
+
+ const val ERR_BOOLEAN_NOT_LEAF = "๋ถ๋ฆฌ์ธ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํฉ๋๋ค"
+ const val ERR_BOOLEAN_HAS_CHILDREN = "๋ถ๋ฆฌ์ธ ๋
ธ๋๋ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค"
+ const val ERR_BOOLEAN_NOT_LITERAL = "๋ถ๋ฆฌ์ธ ๋
ธ๋๋ ๋ฆฌํฐ๋ด์ด์ด์ผ ํฉ๋๋ค"
+
+ const val ERR_VARIABLE_NOT_LEAF = "๋ณ์ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋์ฌ์ผ ํฉ๋๋ค"
+ const val ERR_VARIABLE_HAS_CHILDREN = "๋ณ์ ๋
ธ๋๋ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค"
+ const val ERR_VARIABLE_IS_LITERAL = "๋ณ์ ๋
ธ๋๋ ๋ฆฌํฐ๋ด์ด ๋ ์ ์์ต๋๋ค"
+
+ const val ERR_BINARY_IS_LEAF = "์ดํญ ์ฐ์ฐ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋๊ฐ ๋ ์ ์์ต๋๋ค"
+ const val ERR_BINARY_CHILDREN_COUNT = "์ดํญ ์ฐ์ฐ ๋
ธ๋๋ ์ ํํ 2๊ฐ์ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค"
+ const val ERR_BINARY_NOT_OPERATOR = "์ดํญ ์ฐ์ฐ ๋
ธ๋๋ ์ฐ์ฐ์์ฌ์ผ ํฉ๋๋ค"
+ const val ERR_BINARY_INVALID_PRECEDENCE = "์ดํญ ์ฐ์ฐ์์ ์ฐ์ ์์๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ const val ERR_UNARY_IS_LEAF = "๋จํญ ์ฐ์ฐ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋๊ฐ ๋ ์ ์์ต๋๋ค"
+ const val ERR_UNARY_CHILDREN_COUNT = "๋จํญ ์ฐ์ฐ ๋
ธ๋๋ ์ ํํ 1๊ฐ์ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค"
+ const val ERR_UNARY_NOT_OPERATOR = "๋จํญ ์ฐ์ฐ ๋
ธ๋๋ ์ฐ์ฐ์์ฌ์ผ ํฉ๋๋ค"
+
+ const val ERR_FUNC_LEAF_WITH_ARGS = "์ธ์๊ฐ ์๋ ํจ์ ํธ์ถ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋๊ฐ ๋ ์ ์์ต๋๋ค"
+ const val ERR_FUNC_CHILDREN_COUNT = "ํจ์ ํธ์ถ ๋
ธ๋์ ์์ ๋
ธ๋ ์์ ์ธ์ ์๊ฐ ์ผ์นํ์ง ์์ต๋๋ค"
+ const val ERR_FUNC_NOT_FUNCTION = "ํจ์ ํธ์ถ ๋
ธ๋๋ ํจ์ ํธ์ถ์ด์ด์ผ ํฉ๋๋ค"
+ const val ERR_FUNC_INVALID_SIGNATURE = "ํจ์ ์๊ทธ๋์ฒ๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ const val ERR_IF_IS_LEAF = "์กฐ๊ฑด๋ฌธ ๋
ธ๋๋ ๋ฆฌํ ๋
ธ๋๊ฐ ๋ ์ ์์ต๋๋ค"
+ const val ERR_IF_CHILDREN_COUNT = "์กฐ๊ฑด๋ฌธ ๋
ธ๋๋ ์ ํํ 3๊ฐ์ ์์ ๋
ธ๋๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค"
+ const val ERR_IF_NOT_CONDITIONAL = "์กฐ๊ฑด๋ฌธ ๋
ธ๋๋ ์กฐ๊ฑด๋ฌธ์ด์ด์ผ ํฉ๋๋ค"
+ const val ERR_IF_INVALID_STRUCTURE = "์กฐ๊ฑด๋ฌธ ๊ตฌ์กฐ๊ฐ ์ ํจํ์ง ์์ต๋๋ค"
+
+ const val ERR_ARGS_CHILDREN_COUNT = "์ธ์ ๋ชฉ๋ก ๋
ธ๋์ ์์ ๋
ธ๋ ์์ ์ธ์ ์๊ฐ ์ผ์นํ์ง ์์ต๋๋ค"
+
+ // ๋์ ๋ฉ์์ง ๋น๋
+ fun errDepthExceeded(actual: Int, max: Int) =
+ "๋
ธ๋ ๊ตฌ์กฐ ๊น์ด๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+
+ fun errSizeExceeded(actual: Int, max: Int) =
+ "๋
ธ๋ ๊ตฌ์กฐ ํฌ๊ธฐ๊ฐ ์ต๋๊ฐ์ ์ด๊ณผํฉ๋๋ค: $actual > $max"
+
+ fun errInvalidChildFound(type: String) =
+ "์ ํจํ์ง ์์ ์์ ๋
ธ๋๊ฐ ๋ฐ๊ฒฌ๋์์ต๋๋ค: $type"
+
+ fun errChildValidationException(type: String, message: String) =
+ "์์ ๋
ธ๋ ๊ฒ์ฆ ์ค ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค: $type - $message"
+
+ private const val STRUCTURE_VERIFICATION_SUCCESS = "๊ตฌ์กฐ ๊ฒ์ฆ ์ฑ๊ณต"
+ private const val UNSUPPORTED_NODE_TYPE = "์ง์๋์ง ์๋ ๋
ธ๋ ํ์
์
๋๋ค:"
+ private const val UNKNOWN = "Unknown"
+ }
+
+ // SpecificationContract ๊ตฌํ
+ override fun getName(): String = "AST ๋
ธ๋ ๊ตฌ์กฐ ์ฌ์"
+ override fun getDescription(): String = "AST ๋
ธ๋์ ๊ตฌ์กฐ์ ์ ํฉ์ฑ๊ณผ ์ผ๊ด์ฑ์ ๊ฒ์ฆํ๋ ์ฌ์"
+ override fun getDomain(): String = "ast"
+ override fun getPriority(): Priority = Priority.NORMAL
+}
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt
new file mode 100644
index 00000000..e164abbe
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt
@@ -0,0 +1,124 @@
+package hs.kr.entrydsm.domain.ast.utils
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+
+/**
+ * AST ๊ฒ์ฆ์ ์ํ ๊ณตํต ์ ํธ๋ฆฌํฐ ๋ฉ์๋๋ค์ ์ ๊ณตํฉ๋๋ค.
+ *
+ * ์ฌ๋ฌ ์ ์ฑ
ํด๋์ค์์ ์ค๋ณต์ผ๋ก ์ฌ์ฉ๋๋ ๊ฒ์ฆ ๋ก์ง์ ์ค์ํํ์ฌ
+ * DRY ์์น์ ์ค์ํ๊ณ ์ ์ง๋ณด์์ฑ์ ํฅ์์ํต๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.03
+ */
+object ASTValidationUtils {
+
+ /**
+ * ๋ณ์๋ช
์ด ์ ํจํ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param name ํ์ธํ ๋ณ์๋ช
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isValidVariableName(name: String): Boolean {
+ if (name.isEmpty()) return false
+ if (!name[0].isLetter() && name[0] != '_') return false
+ return name.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ /**
+ * ํจ์๋ช
์ด ์ ํจํ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param name ํ์ธํ ํจ์๋ช
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isValidFunctionName(name: String): Boolean {
+ if (name.isEmpty()) return false
+ if (!name[0].isLetter()) return false
+ return name.drop(1).all { it.isLetterOrDigit() || it == '_' }
+ }
+
+ /**
+ * ์์ฝ์ด์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param name ํ์ธํ ๋ฌธ์์ด
+ * @return ์์ฝ์ด์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isReservedWord(name: String): Boolean {
+ return RESERVED_WORDS.contains(name.lowercase())
+ }
+
+ /**
+ * ์ง์๋๋ ์ดํญ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param operator ํ์ธํ ์ฐ์ฐ์
+ * @return ์ง์๋๋ ์ดํญ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isSupportedBinaryOperator(operator: String): Boolean {
+ return BINARY_OPERATORS.contains(operator)
+ }
+
+ /**
+ * ์ง์๋๋ ๋จํญ ์ฐ์ฐ์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param operator ํ์ธํ ์ฐ์ฐ์
+ * @return ์ง์๋๋ ๋จํญ ์ฐ์ฐ์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isSupportedUnaryOperator(operator: String): Boolean {
+ return UNARY_OPERATORS.contains(operator)
+ }
+
+ /**
+ * ๋
ธ๋๊ฐ 0 ์์์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param node ํ์ธํ AST ๋
ธ๋
+ * @return 0 ์์์ด๋ฉด true, ์๋๋ฉด false
+ */
+ fun isZeroConstant(node: ASTNode): Boolean {
+ return node is hs.kr.entrydsm.domain.ast.entities.NumberNode && node.value == 0.0
+ }
+
+ /**
+ * ์์ฝ์ด ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์์ฝ์ด ์งํฉ
+ */
+ fun getReservedWords(): Set = RESERVED_WORDS.toSet()
+
+ /**
+ * ์ง์๋๋ ์ดํญ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ดํญ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getBinaryOperators(): Set = BINARY_OPERATORS.toSet()
+
+ /**
+ * ์ง์๋๋ ๋จํญ ์ฐ์ฐ์ ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋จํญ ์ฐ์ฐ์ ์งํฉ
+ */
+ fun getUnaryOperators(): Set = UNARY_OPERATORS.toSet()
+
+ /**
+ * ์์ฝ์ด ๋ชฉ๋ก
+ */
+ private val RESERVED_WORDS = setOf(
+ "if", "else", "while", "for", "do", "break", "continue",
+ "function", "return", "var", "let", "const", "true", "false",
+ "null", "undefined", "this", "new", "typeof", "instanceof",
+ "try", "catch", "finally", "throw", "switch", "case", "default"
+ )
+
+ /**
+ * ์ง์๋๋ ์ดํญ ์ฐ์ฐ์ ๋ชฉ๋ก
+ */
+ private val BINARY_OPERATORS = setOf(
+ "+", "-", "*", "/", "%", "^",
+ "==", "!=", "<", "<=", ">", ">=",
+ "&&", "||"
+ )
+
+ /**
+ * ์ง์๋๋ ๋จํญ ์ฐ์ฐ์ ๋ชฉ๋ก
+ */
+ private val UNARY_OPERATORS = setOf("-", "+", "!")
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt
new file mode 100644
index 00000000..158eaf1c
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt
@@ -0,0 +1,152 @@
+package hs.kr.entrydsm.domain.ast.utils
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+
+/**
+ * ํจ์ ํธ์ถ ๊ฒ์ฆ ๊ท์น์ ์ค์์์ ๊ด๋ฆฌํ๋ ์ ํธ๋ฆฌํฐ ํด๋์ค์
๋๋ค.
+ *
+ * ํ๋์ฝ๋ฉ๋ when ๊ตฌ๋ฌธ์ Map ๊ธฐ๋ฐ ๊ตฌ์กฐ๋ก ๋์ฒดํ์ฌ ํ์ฅ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ํฅ์์ํต๋๋ค.
+ *
+ * @author kangeunchan
+ * @since 2025.08.03
+ */
+object FunctionValidationRules {
+
+ /**
+ * ํจ์ ๊ฒ์ฆ ๊ท์น ์ธํฐํ์ด์ค
+ */
+ fun interface ValidationRule {
+ fun validate(args: List): Boolean
+ }
+
+ /**
+ * ํจ์๋ณ ๊ฒ์ฆ ๊ท์น ๋งต
+ */
+ private val VALIDATION_RULES = mapOf(
+ // ์ ํํ ์ธ์ ๊ฐ์๊ฐ ํ์ํ ํจ์๋ค
+ "SQRT" to ValidationRule { args -> args.size == 1 },
+ "POW" to ValidationRule { args -> args.size == 2 },
+ "IF" to ValidationRule { args -> args.size == 3 },
+
+ // ๋จ์ผ ์ธ์ ์ํ ํจ์๋ค
+ "SIN" to ValidationRule { args -> args.size == 1 },
+ "COS" to ValidationRule { args -> args.size == 1 },
+ "TAN" to ValidationRule { args -> args.size == 1 },
+ "ABS" to ValidationRule { args -> args.size == 1 },
+ "LOG" to ValidationRule { args -> args.size == 1 },
+ "EXP" to ValidationRule { args -> args.size == 1 },
+
+ // ๊ฐ๋ณ ์ธ์ ํจ์๋ค (์ต์ 1๊ฐ)
+ "MAX" to ValidationRule { args -> args.isNotEmpty() },
+ "MIN" to ValidationRule { args -> args.isNotEmpty() }
+ )
+
+ /**
+ * ํจ์ ํธ์ถ์ด ์ ํจํ์ง ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @param args ์ธ์ ๋ชฉ๋ก
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isValidFunctionCall(name: String, args: List): Boolean {
+ val rule = VALIDATION_RULES[name.uppercase()]
+ return rule?.validate(args) ?: true // ๋ฑ๋ก๋์ง ์์ ํจ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ฉ
+ }
+
+ /**
+ * ํจ์๋ณ ํ์ํ ์ธ์ ๊ฐ์ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค. (๋๋ฒ๊น
์ฉ)
+ *
+ * @param name ํจ์๋ช
+ * @return ์ธ์ ๊ฐ์ ์ค๋ช
๋ฌธ์์ด
+ */
+ fun getArgumentCountDescription(name: String): String {
+ return when (name.uppercase()) {
+ "SQRT", "SIN", "COS", "TAN", "ABS", "LOG", "EXP" -> "์ ํํ 1๊ฐ"
+ "POW" -> "์ ํํ 2๊ฐ"
+ "IF" -> "์ ํํ 3๊ฐ"
+ "MAX", "MIN" -> "์ต์ 1๊ฐ"
+ else -> "์ ํ ์์"
+ }
+ }
+
+ /**
+ * ํจ์์ ์์ ์ธ์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @return ์์ ์ธ์ ๊ฐ์ (๊ฐ๋ณ ์ธ์์ธ ๊ฒฝ์ฐ -1)
+ */
+ fun getExpectedArgumentCount(name: String): Int {
+ return when (name.uppercase()) {
+ "SQRT", "SIN", "COS", "TAN", "ABS", "LOG", "EXP" -> 1
+ "POW" -> 2
+ "IF" -> 3
+ "MAX", "MIN" -> -1 // ๊ฐ๋ณ ์ธ์
+ else -> -1 // ์ ์ ์์
+ }
+ }
+
+ /**
+ * ๋ฑ๋ก๋ ๋ชจ๋ ํจ์๋ช
์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ฑ๋ก๋ ํจ์๋ช
์งํฉ
+ */
+ fun getRegisteredFunctions(): Set {
+ return VALIDATION_RULES.keys
+ }
+
+ /**
+ * ํจ์๊ฐ ๋ฑ๋ก๋์ด ์๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param name ํจ์๋ช
+ * @return ๋ฑ๋ก๋์ด ์์ผ๋ฉด true, ์๋๋ฉด false
+ */
+ fun isRegisteredFunction(name: String): Boolean {
+ return VALIDATION_RULES.containsKey(name.uppercase())
+ }
+
+ /**
+ * ์๋ก์ด ํจ์ ๊ฒ์ฆ ๊ท์น์ ์ถ๊ฐํฉ๋๋ค. (ํ์ฅ์ฑ์ ์ํ ๋ฉ์๋)
+ *
+ * @param name ํจ์๋ช
+ * @param rule ๊ฒ์ฆ ๊ท์น
+ */
+ fun addValidationRule(name: String, rule: ValidationRule) {
+ // ๋ฐํ์์ ๊ท์น์ ์ถ๊ฐํ ์ ์๋๋ก MutableMap์ผ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ
+ // ํ์ฌ๋ ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ์ค๊ณ๋จ
+ throw ASTException.runtimeRuleNotSupported()
+ }
+
+ /**
+ * ํจ์ ๊ฒ์ฆ ํต๊ณ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ์ ๋ณด ๋งต
+ */
+ fun getValidationStatistics(): Map {
+ val exactArgFunctions = VALIDATION_RULES.filterValues { rule ->
+ // ์ ํํ ๊ฐ์๋ฅผ ์๊ตฌํ๋ ํจ์๋ค (๊ฐ๋จํ ํด๋ฆฌ์คํฑ)
+ listOf(1, 2, 3).any { count ->
+ rule.validate(List(count) { createDummyNode() })
+ }
+ }
+
+ val variableArgFunctions = VALIDATION_RULES.filterValues { rule ->
+ // ๊ฐ๋ณ ์ธ์ ํจ์๋ค
+ rule.validate(listOf(createDummyNode())) && rule.validate(listOf(createDummyNode(), createDummyNode()))
+ }
+
+ return mapOf(
+ "totalFunctions" to VALIDATION_RULES.size,
+ "exactArgFunctions" to exactArgFunctions.size,
+ "variableArgFunctions" to variableArgFunctions.size,
+ "registeredFunctions" to getRegisteredFunctions()
+ )
+ }
+
+ /**
+ * ํ
์คํธ์ฉ ๋๋ฏธ ๋
ธ๋๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ private fun createDummyNode(): ASTNode {
+ return hs.kr.entrydsm.domain.ast.entities.NumberNode(0.0)
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt
new file mode 100644
index 00000000..90be86af
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt
@@ -0,0 +1,24 @@
+package hs.kr.entrydsm.domain.ast.values
+
+import java.time.LocalDateTime
+
+/**
+ * ์ต์ ํ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ํด๋์ค
+ */
+data class ASTOptimizationResult(
+ val originalSize: NodeSize,
+ val optimizedSize: NodeSize,
+ val originalDepth: TreeDepth,
+ val optimizedDepth: TreeDepth,
+ val level: OptimizationLevel,
+ val optimizedAt: LocalDateTime,
+ val astId: String
+) {
+ fun getReductionRatio(): Double {
+ return if (originalSize.value > 0) {
+ (originalSize.value - optimizedSize.value).toDouble() / originalSize.value.toDouble()
+ } else {
+ 0.0
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt
new file mode 100644
index 00000000..13b295ab
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt
@@ -0,0 +1,13 @@
+package hs.kr.entrydsm.domain.ast.values
+
+import java.time.LocalDateTime
+
+/**
+ * ๊ฒ์ฆ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ํด๋์ค
+ */
+data class ASTValidationResult(
+ val isValid: Boolean,
+ val violations: List,
+ val validatedAt: LocalDateTime,
+ val astId: String
+)
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt
new file mode 100644
index 00000000..8a1b993b
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt
@@ -0,0 +1,298 @@
+package hs.kr.entrydsm.domain.ast.values
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+
+/**
+ * AST ๋
ธ๋์ ํฌ๊ธฐ๋ฅผ ๋ํ๋ด๋ ๊ฐ ๊ฐ์ฒด์
๋๋ค.
+ *
+ * ๋
ธ๋์ ํฌ๊ธฐ๋ฅผ ์์ ํ๊ฒ ๊ด๋ฆฌํ๋ฉฐ, ํฌ๊ธฐ ์ ํ๊ณผ
+ * ๊ด๋ จ๋ ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+data class NodeSize private constructor(val value: Int) {
+
+ init {
+ if (value < 0) {
+ throw ASTException.nodeSizeNegative(value)
+ }
+ if (value > MAX_SIZE) {
+ throw ASTException.nodeSizeTooLarge(value, MAX_SIZE)
+ }
+
+ }
+
+ /**
+ * ํฌ๊ธฐ๋ฅผ ์ฆ๊ฐ์ํต๋๋ค.
+ */
+ fun increment(): NodeSize {
+ return of(value + 1)
+ }
+
+ /**
+ * ํฌ๊ธฐ๋ฅผ ๊ฐ์์ํต๋๋ค.
+ */
+ fun decrement(): NodeSize {
+ return if (value > 0) of(value - 1) else this
+ }
+
+ /**
+ * ์ง์ ๋ ๊ฐ๋งํผ ํฌ๊ธฐ๋ฅผ ์ฆ๊ฐ์ํต๋๋ค.
+ */
+ fun plus(amount: Int): NodeSize {
+ return of(value + amount)
+ }
+
+ /**
+ * ์ง์ ๋ ๊ฐ๋งํผ ํฌ๊ธฐ๋ฅผ ๊ฐ์์ํต๋๋ค.
+ */
+ fun minus(amount: Int): NodeSize {
+ return of(maxOf(0, value - amount))
+ }
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋ํฉ๋๋ค.
+ */
+ fun plus(other: NodeSize): NodeSize {
+ return of(value + other.value)
+ }
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋บ๋๋ค.
+ */
+ fun minus(other: NodeSize): NodeSize {
+ return of(maxOf(0, value - other.value))
+ }
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isGreaterThan(other: NodeSize): Boolean = value > other.value
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isLessThan(other: NodeSize): Boolean = value < other.value
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isGreaterOrEqual(other: NodeSize): Boolean = value >= other.value
+
+ /**
+ * ๋ค๋ฅธ ํฌ๊ธฐ์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isLessOrEqual(other: NodeSize): Boolean = value <= other.value
+
+ /**
+ * ๋น ํฌ๊ธฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isEmpty(): Boolean = value == 0
+
+ /**
+ * ๋จ์ผ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isSingle(): Boolean = value == 1
+
+ /**
+ * ์์ ํฌ๊ธฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isSmall(): Boolean = value <= SMALL_SIZE
+
+ /**
+ * ์ค๊ฐ ํฌ๊ธฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isMedium(): Boolean = value in (SMALL_SIZE + 1)..MEDIUM_SIZE
+
+ /**
+ * ํฐ ํฌ๊ธฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isLarge(): Boolean = value in (MEDIUM_SIZE + 1)..LARGE_SIZE
+
+ /**
+ * ๋งค์ฐ ํฐ ํฌ๊ธฐ์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isVeryLarge(): Boolean = value > LARGE_SIZE
+
+ /**
+ * ์ต๋ ํฌ๊ธฐ์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isAtLimit(): Boolean = value >= MAX_SIZE
+
+ /**
+ * ๊ฒฝ๊ณ ์์ค์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isWarningLevel(): Boolean = value >= WARNING_SIZE
+
+ /**
+ * ๋ ํฌ๊ธฐ ์ค ์ต๋๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun max(other: NodeSize): NodeSize {
+ return if (value >= other.value) this else other
+ }
+
+ /**
+ * ๋ ํฌ๊ธฐ ์ค ์ต์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun min(other: NodeSize): NodeSize {
+ return if (value <= other.value) this else other
+ }
+
+ /**
+ * ๋ฐฑ๋ถ์จ๋ก ๋ณํํฉ๋๋ค.
+ */
+ fun toPercentage(): Double = (value.toDouble() / MAX_SIZE) * 100
+
+ /**
+ * ํฌ๊ธฐ ๋ ๋ฒจ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getLevel(): SizeLevel {
+ return when {
+ value == 0 -> SizeLevel.EMPTY
+ value <= SMALL_SIZE -> SizeLevel.SMALL
+ value <= MEDIUM_SIZE -> SizeLevel.MEDIUM
+ value <= LARGE_SIZE -> SizeLevel.LARGE
+ value <= WARNING_SIZE -> SizeLevel.VERY_LARGE
+ value > WARNING_SIZE -> SizeLevel.CRITICAL
+ else -> SizeLevel.EMPTY
+ }
+ }
+
+ /**
+ * ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ถ์ ํฉ๋๋ค (๋ฐ์ดํธ ๋จ์).
+ */
+ fun estimateMemoryUsage(): Long {
+ return value * BYTES_PER_NODE
+ }
+
+ /**
+ * ๋ฌธ์์ด ํํ์ ๋ฐํํฉ๋๋ค.
+ */
+ override fun toString(): String = "NodeSize($value)"
+
+ /**
+ * ํฌ๊ธฐ ๋ ๋ฒจ์ ๋ํ๋ด๋ ์ด๊ฑฐํ
+ */
+ enum class SizeLevel(val description: String) {
+ EMPTY("๋น ํฌ๊ธฐ"),
+ SMALL("์์ ํฌ๊ธฐ"),
+ MEDIUM("์ค๊ฐ ํฌ๊ธฐ"),
+ LARGE("ํฐ ํฌ๊ธฐ"),
+ VERY_LARGE("๋งค์ฐ ํฐ ํฌ๊ธฐ"),
+ CRITICAL("์ํ ํฌ๊ธฐ")
+ }
+
+ companion object {
+ private const val MAX_SIZE = 10000
+ private const val WARNING_SIZE = 8000
+ private const val LARGE_SIZE = 1000
+ private const val MEDIUM_SIZE = 100
+ private const val SMALL_SIZE = 10
+ private const val BYTES_PER_NODE = 64L
+
+ private val ZERO = NodeSize(0)
+ private val ONE = NodeSize(1)
+
+ /**
+ * ๊ฐ์ผ๋ก NodeSize๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ fun of(value: Int): NodeSize {
+ return when (value) {
+ 0 -> ZERO
+ 1 -> ONE
+ else -> NodeSize(value)
+ }
+ }
+
+ /**
+ * 0 ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun zero(): NodeSize = ZERO
+
+ /**
+ * 1 ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun one(): NodeSize = ONE
+
+ /**
+ * ์ต๋ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun max(): NodeSize = of(MAX_SIZE)
+
+ /**
+ * ๊ฒฝ๊ณ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun warning(): NodeSize = of(WARNING_SIZE)
+
+ /**
+ * ์์ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun small(): NodeSize = of(SMALL_SIZE)
+
+ /**
+ * ์ค๊ฐ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun medium(): NodeSize = of(MEDIUM_SIZE)
+
+ /**
+ * ํฐ ํฌ๊ธฐ๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun large(): NodeSize = of(LARGE_SIZE)
+
+ /**
+ * ์ฌ๋ฌ ํฌ๊ธฐ ์ค ์ต๋๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun maxOf(vararg sizes: NodeSize): NodeSize {
+ return sizes.maxByOrNull { it.value } ?: ZERO
+ }
+
+ /**
+ * ์ฌ๋ฌ ํฌ๊ธฐ ์ค ์ต์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun minOf(vararg sizes: NodeSize): NodeSize {
+ return sizes.minByOrNull { it.value } ?: ZERO
+ }
+
+ /**
+ * ์ฌ๋ฌ ํฌ๊ธฐ์ ํฉ์ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun sum(sizes: List): NodeSize {
+ if (sizes.isEmpty()) return ZERO
+ val total = sizes.sumOf { it.value }
+ return of(total)
+ }
+
+ /**
+ * ํ๊ท ํฌ๊ธฐ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun average(sizes: List): NodeSize {
+ if (sizes.isEmpty()) return ZERO
+ val avg = sizes.map { it.value }.average().toInt()
+ return of(avg)
+ }
+
+ /**
+ * ์ ํจํ ํฌ๊ธฐ ๋ฒ์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isValidRange(size: Int): Boolean {
+ return size in 0..MAX_SIZE
+ }
+
+ /**
+ * ์์ ํ ํฌ๊ธฐ ์์ฑ (๋ฒ์ ๊ฒ์ฆ ์์)
+ */
+ fun tryOf(value: Int): NodeSize? {
+ return if (isValidRange(value)) of(value) else null
+ }
+
+ /**
+ * ์ด ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun calculateTotalMemoryUsage(sizes: List): Long {
+ return sizes.sumOf { it.estimateMemoryUsage() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt
new file mode 100644
index 00000000..f8232892
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt
@@ -0,0 +1,166 @@
+package hs.kr.entrydsm.domain.ast.values
+
+/**
+ * AST ๋
ธ๋์ ํ์
์ ๋ํ๋ด๋ ๊ฐ ๊ฐ์ฒด์
๋๋ค.
+ *
+ * AST ๋
ธ๋์ ์ข
๋ฅ๋ฅผ ๊ตฌ๋ถํ๋ฉฐ, ๊ฐ ๋
ธ๋ ํ์
๋ณ ํน์ฑ๊ณผ
+ * ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์ ์ํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+enum class NodeType(
+ val description: String,
+ val isLeaf: Boolean,
+ val isOperator: Boolean,
+ val isLiteral: Boolean,
+ val priority: Int
+) {
+
+ /**
+ * ์ซ์ ๋ฆฌํฐ๋ด ๋
ธ๋
+ */
+ NUMBER("์ซ์ ๋ฆฌํฐ๋ด", true, false, true, 0),
+
+ /**
+ * ๋ถ๋ฆฌ์ธ ๋ฆฌํฐ๋ด ๋
ธ๋
+ */
+ BOOLEAN("๋ถ๋ฆฌ์ธ ๋ฆฌํฐ๋ด", true, false, true, 0),
+
+ /**
+ * ๋ณ์ ๋
ธ๋
+ */
+ VARIABLE("๋ณ์", true, false, false, 0),
+
+ /**
+ * ์ดํญ ์ฐ์ฐ์ ๋
ธ๋
+ */
+ BINARY_OP("์ดํญ ์ฐ์ฐ์", false, true, false, 2),
+
+ /**
+ * ๋จํญ ์ฐ์ฐ์ ๋
ธ๋
+ */
+ UNARY_OP("๋จํญ ์ฐ์ฐ์", false, true, false, 1),
+
+ /**
+ * ํจ์ ํธ์ถ ๋
ธ๋
+ */
+ FUNCTION_CALL("ํจ์ ํธ์ถ", false, false, false, 3),
+
+ /**
+ * ์กฐ๊ฑด๋ฌธ ๋
ธ๋
+ */
+ IF("์กฐ๊ฑด๋ฌธ", false, false, false, 4),
+
+ /**
+ * ์ธ์ ๋ชฉ๋ก ๋
ธ๋
+ */
+ ARGUMENTS("์ธ์ ๋ชฉ๋ก", false, false, false, 1);
+
+ /**
+ * ๋ฆฌํฐ๋ด ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isLiteralNode(): Boolean = isLiteral
+
+ /**
+ * ์ฐ์ฐ์ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isOperatorNode(): Boolean = isOperator
+
+ /**
+ * ๋ฆฌํ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isLeafNode(): Boolean = isLeaf
+
+ /**
+ * ๋ณตํฉ ๋
ธ๋์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isComplexNode(): Boolean = !isLeaf
+
+
+ /**
+ * ํน์ ๋
ธ๋ ํ์
๊ณผ ํธํ๋๋์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isCompatibleWith(other: NodeType): Boolean = when {
+ this.isLiteral && other.isLiteral -> true
+ this.isOperator && other.isOperator -> true
+ (this == VARIABLE && other.isLiteral) || (this.isLiteral && other == VARIABLE) -> true
+ else -> false
+ }
+
+ /**
+ * ๋
ธ๋ ํ์
๋ณ ์์ ์์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getExpectedChildCount(): IntRange {
+ return when (this) {
+ NUMBER, BOOLEAN, VARIABLE -> 0..0
+ UNARY_OP -> 1..1
+ BINARY_OP -> 2..2
+ IF -> 3..3
+ FUNCTION_CALL -> 0..UNLIMITED_CHILD_COUNT
+ ARGUMENTS -> 0..UNLIMITED_CHILD_COUNT
+ }
+ }
+
+ /**
+ * ๋
ธ๋ ํ์
๋ณ ์ต๋ ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getMaxDepth(): Int {
+ return when (this) {
+ NUMBER, BOOLEAN, VARIABLE -> 1
+ UNARY_OP -> 10
+ BINARY_OP -> 15
+ FUNCTION_CALL -> 20
+ IF -> 25
+ ARGUMENTS -> 5
+ }
+ }
+
+ companion object {
+ private const val UNLIMITED_CHILD_COUNT = Int.MAX_VALUE
+
+ /**
+ * ๋ชจ๋ ๋ฆฌํฐ๋ด ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ val literalTypes: Set by lazy {
+ entries.filter { it.isLiteral }.toSet()
+ }
+
+ /**
+ * ๋ชจ๋ ์ฐ์ฐ์ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ val operatorTypes: Set by lazy {
+ entries.filter { it.isOperator }.toSet()
+ }
+
+ /**
+ * ๋ชจ๋ ๋ฆฌํ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ val leafTypes: Set by lazy {
+ entries.filter { it.isLeaf }.toSet()
+ }
+
+ /**
+ * ๋ชจ๋ ๋ณตํฉ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ val complexTypes: Set by lazy {
+ entries.filter { !it.isLeaf }.toSet()
+ }
+
+ /**
+ * ์ฐ์ ์์ ์์ผ๋ก ์ ๋ ฌ๋ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ val sortedByPriority: List by lazy {
+ entries.sortedBy { it.priority }
+ }
+
+ /**
+ * ์ค๋ช
์ผ๋ก ๋
ธ๋ ํ์
์ ์ฐพ์ต๋๋ค.
+ */
+ fun findByDescription(description: String): NodeType? {
+ return entries.find { it.description == description }
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt
new file mode 100644
index 00000000..4f53095c
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt
@@ -0,0 +1,10 @@
+package hs.kr.entrydsm.domain.ast.values
+
+/**
+ * ์ต์ ํ ๋ ๋ฒจ ์ด๊ฑฐํ
+ */
+enum class OptimizationLevel(val description: String) {
+ NONE("์ต์ ํ ์์"),
+ BASIC("๊ธฐ๋ณธ ์ต์ ํ"),
+ FULL("์์ ์ต์ ํ")
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt
new file mode 100644
index 00000000..6edc85b5
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt
@@ -0,0 +1,222 @@
+package hs.kr.entrydsm.domain.ast.values
+
+import hs.kr.entrydsm.domain.ast.exceptions.ASTException
+
+/**
+ * AST ํธ๋ฆฌ์ ๊น์ด๋ฅผ ๋ํ๋ด๋ ๊ฐ ๊ฐ์ฒด์
๋๋ค.
+ *
+ * ํธ๋ฆฌ์ ๊น์ด๋ฅผ ์์ ํ๊ฒ ๊ด๋ฆฌํ๋ฉฐ, ๊น์ด ์ ํ๊ณผ
+ * ๊ด๋ จ๋ ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.16
+ */
+data class TreeDepth private constructor(val value: Int) {
+
+ init {
+ if (value < 0) {
+ throw ASTException.treeDepthNegative(value)
+ }
+
+ if (value > MAX_DEPTH) {
+ throw ASTException.treeDepthTooLarge(value, MAX_DEPTH)
+ }
+ }
+
+ /**
+ * ๊น์ด๋ฅผ ์ฆ๊ฐ์ํต๋๋ค.
+ */
+ fun increment(): TreeDepth {
+ return if (value < MAX_DEPTH) of(value + 1) else this
+ }
+
+ /**
+ * ๊น์ด๋ฅผ ๊ฐ์์ํต๋๋ค.
+ */
+ fun decrement(): TreeDepth {
+ return if (value > 0) of(value - 1) else this
+ }
+
+ /**
+ * ์ง์ ๋ ๊ฐ๋งํผ ๊น์ด๋ฅผ ์ฆ๊ฐ์ํต๋๋ค.
+ */
+ fun plus(amount: Int): TreeDepth {
+ return of(value + amount)
+ }
+
+ /**
+ * ์ง์ ๋ ๊ฐ๋งํผ ๊น์ด๋ฅผ ๊ฐ์์ํต๋๋ค.
+ */
+ fun minus(amount: Int): TreeDepth {
+ return of(maxOf(0, value - amount))
+ }
+
+ /**
+ * ๋ค๋ฅธ ๊น์ด์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isGreaterThan(other: TreeDepth): Boolean = value > other.value
+
+ /**
+ * ๋ค๋ฅธ ๊น์ด์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isLessThan(other: TreeDepth): Boolean = value < other.value
+
+ /**
+ * ๋ค๋ฅธ ๊น์ด์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isGreaterOrEqual(other: TreeDepth): Boolean = value >= other.value
+
+ /**
+ * ๋ค๋ฅธ ๊น์ด์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isLessOrEqual(other: TreeDepth): Boolean = value <= other.value
+
+ /**
+ * ์ต๋ ๊น์ด์ ๋น๊ตํฉ๋๋ค.
+ */
+ fun isAtLimit(): Boolean = value >= MAX_DEPTH
+
+ /**
+ * ๊ฒฝ๊ณ ์์ค์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isWarningLevel(): Boolean = value >= WARNING_DEPTH
+
+ /**
+ * ์์ ๊น์ด์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isShallow(): Boolean = value <= SHALLOW_DEPTH
+
+ /**
+ * ๊น์ ๊น์ด์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isDeep(): Boolean = value >= DEEP_DEPTH
+
+ /**
+ * ๋ ๊น์ด ์ค ์ต๋๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun max(other: TreeDepth): TreeDepth {
+ return if (value >= other.value) this else other
+ }
+
+ /**
+ * ๋ ๊น์ด ์ค ์ต์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun min(other: TreeDepth): TreeDepth {
+ return if (value <= other.value) this else other
+ }
+
+ /**
+ * ๋ฐฑ๋ถ์จ๋ก ๋ณํํฉ๋๋ค.
+ */
+ fun toPercentage(): Double = (value.toDouble() / MAX_DEPTH) * 100
+
+ /**
+ * ๊น์ด ๋ ๋ฒจ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getLevel(): DepthLevel {
+ return when {
+ value <= SHALLOW_DEPTH -> DepthLevel.SHALLOW
+ value <= NORMAL_DEPTH -> DepthLevel.NORMAL
+ value <= DEEP_DEPTH -> DepthLevel.DEEP
+ value <= WARNING_DEPTH -> DepthLevel.WARNING
+ else -> DepthLevel.CRITICAL
+ }
+ }
+
+ /**
+ * ๋ฌธ์์ด ํํ์ ๋ฐํํฉ๋๋ค.
+ */
+ override fun toString(): String = "TreeDepth($value)"
+
+ /**
+ * ๊น์ด ๋ ๋ฒจ์ ๋ํ๋ด๋ ์ด๊ฑฐํ
+ */
+ enum class DepthLevel(val description: String) {
+ SHALLOW("์์ ๊น์ด"),
+ NORMAL("์ผ๋ฐ ๊น์ด"),
+ DEEP("๊น์ ๊น์ด"),
+ WARNING("๊ฒฝ๊ณ ๊น์ด"),
+ CRITICAL("์ํ ๊น์ด")
+ }
+
+ companion object {
+ private const val MAX_DEPTH = 100
+ private const val WARNING_DEPTH = 80
+ private const val DEEP_DEPTH = 60
+ private const val NORMAL_DEPTH = 40
+ private const val SHALLOW_DEPTH = 10
+
+ private val ZERO = TreeDepth(0)
+ private val ONE = TreeDepth(1)
+
+ /**
+ * ๊ฐ์ผ๋ก TreeDepth๋ฅผ ์์ฑํฉ๋๋ค.
+ */
+ fun of(value: Int): TreeDepth {
+ return when (value) {
+ 0 -> ZERO
+ 1 -> ONE
+ else -> TreeDepth(value)
+ }
+ }
+
+ /**
+ * 0 ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun zero(): TreeDepth = ZERO
+
+ /**
+ * 1 ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun one(): TreeDepth = ONE
+
+ /**
+ * ์ต๋ ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun max(): TreeDepth = of(MAX_DEPTH)
+
+ /**
+ * ๊ฒฝ๊ณ ๊น์ด๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun warning(): TreeDepth = of(WARNING_DEPTH)
+
+ /**
+ * ์ฌ๋ฌ ๊น์ด ์ค ์ต๋๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun maxOf(vararg depths: TreeDepth): TreeDepth {
+ return depths.maxByOrNull { it.value } ?: ZERO
+ }
+
+ /**
+ * ์ฌ๋ฌ ๊น์ด ์ค ์ต์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ */
+ fun minOf(vararg depths: TreeDepth): TreeDepth {
+ return depths.minByOrNull { it.value } ?: ZERO
+ }
+
+ /**
+ * ํ๊ท ๊น์ด๋ฅผ ๊ณ์ฐํฉ๋๋ค.
+ */
+ fun average(depths: List): TreeDepth {
+ if (depths.isEmpty()) return ZERO
+ val avg = depths.map { it.value }.average().toInt()
+ return of(avg)
+ }
+
+ /**
+ * ์ ํจํ ๊น์ด ๋ฒ์์ธ์ง ํ์ธํฉ๋๋ค.
+ */
+ fun isValidRange(depth: Int): Boolean {
+ return depth in 0..MAX_DEPTH
+ }
+
+ /**
+ * ์์ ํ ๊น์ด ์์ฑ (๋ฒ์ ๊ฒ์ฆ ์์)
+ */
+ fun tryOf(value: Int): TreeDepth? {
+ return if (isValidRange(value)) of(value) else null
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt
new file mode 100644
index 00000000..42ef18dd
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt
@@ -0,0 +1,45 @@
+package hs.kr.entrydsm.domain.ast.values
+
+import java.time.LocalDateTime
+
+/**
+ * ํธ๋ฆฌ ํต๊ณ ๋ฐ์ดํฐ ํด๋์ค
+ */
+data class TreeStatistics(
+ val nodeCount: NodeSize,
+ val leafCount: NodeSize,
+ val maxDepth: TreeDepth,
+ val averageDepth: TreeDepth,
+ val nodeTypeCounts: Map,
+ val variables: Set,
+ val astId: String,
+ val calculatedAt: LocalDateTime
+) {
+ /**
+ * ๊ฐ์ฅ ๋ง์ ๋
ธ๋ ํ์
์ ๋ฐํํฉ๋๋ค.
+ */
+ fun getMostCommonNodeType(): NodeType? {
+ return nodeTypeCounts.maxByOrNull { it.value }?.key
+ }
+
+ /**
+ * ํน์ ๋
ธ๋ ํ์
์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getNodeCount(type: NodeType): Int {
+ return nodeTypeCounts[type] ?: 0
+ }
+
+ /**
+ * ๋ฆฌํ ๋
ธ๋๋ค์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getLeafNodeCount(): Int {
+ return NodeType.leafTypes.sumOf { getNodeCount(it) }
+ }
+
+ /**
+ * ์ฐ์ฐ์ ๋
ธ๋๋ค์ ๊ฐ์๋ฅผ ๋ฐํํฉ๋๋ค.
+ */
+ fun getOperatorNodeCount(): Int {
+ return NodeType.operatorTypes.sumOf { getNodeCount(it) }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt
new file mode 100644
index 00000000..a98953f4
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt
@@ -0,0 +1,291 @@
+package hs.kr.entrydsm.domain.calculator.aggregates
+
+import hs.kr.entrydsm.domain.ast.entities.ASTNode
+import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException
+import hs.kr.entrydsm.domain.calculator.values.CalculationRequest
+import hs.kr.entrydsm.domain.calculator.values.CalculationResult
+import hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator
+import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate
+import hs.kr.entrydsm.domain.lexer.entities.Token
+import hs.kr.entrydsm.domain.lexer.entities.TokenType
+import hs.kr.entrydsm.domain.parser.aggregates.LRParser
+import hs.kr.entrydsm.domain.parser.values.Grammar
+import hs.kr.entrydsm.global.annotation.aggregates.Aggregate
+import kotlin.system.measureTimeMillis
+
+/**
+ * ๊ณ์ฐ๊ธฐ ๋๋ฉ์ธ์ ํต์ฌ ์งํฉ ๋ฃจํธ์
๋๋ค.
+ *
+ * ์์ ๊ณ์ฐ์ ์ ์ฒด ํ๋ก์ธ์ค๋ฅผ ๊ด๋ฆฌํ๋ฉฐ, ๋ ์ฑ, ํ์ฑ, ํ๊ฐ์ ๋ชจ๋ ๋จ๊ณ๋ฅผ
+ * ์กฐ์จํฉ๋๋ค. ๋ค๋ฅธ ๋๋ฉ์ธ๋ค๊ณผ์ ํ๋ ฅ์ ํตํด ์์ ํ ๊ณ์ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ,
+ * ๋ณ์ ๋ฐ์ธ๋ฉ, ๋ค๋จ๊ณ ๊ณ์ฐ, ์ค๋ฅ ์ฒ๋ฆฌ ๋ฑ์ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
+ *
+ * @property lexer ํ ํฐํ๋ฅผ ๋ด๋นํ๋ ๋ ์
+ * @property maxFormulaLength ์ต๋ ์์ ๊ธธ์ด
+ * @property maxVariables ์ต๋ ๋ณ์ ๊ฐ์
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+@Aggregate(context = "calculator")
+class Calculator(
+ private val lexer: LexerAggregate = LexerAggregate(),
+ private val parser: LRParser = LRParser.createDefault(),
+ private val maxFormulaLength: Int = 5000,
+ private val maxVariables: Int = 100
+) {
+
+ /**
+ * ๋จ์ผ ์์์ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param request ๊ณ์ฐ ์์ฒญ
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ
+ * @throws CalculatorException ๊ณ์ฐ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ fun calculate(request: CalculationRequest): CalculationResult {
+ validateRequest(request)
+
+ var result: Any? = null
+ var ast: ASTNode? = null
+ var tokens: List? = null
+
+ val executionTime = measureTimeMillis {
+ try {
+ // 1. ํ ํฐํ
+ tokens = tokenize(request.formula)
+
+ // 2. ํ์ฑ (๊ฐ๋จํ ํ์ฑ ๋ก์ง - ์ค์ ๋ก๋ LRParser ์ฌ์ฉ)
+ ast = parseTokens(tokens!!)
+
+ // 3. ํ๊ฐ
+ val evaluator = ExpressionEvaluator.create(request.variables)
+ result = evaluator.evaluate(ast!!)
+
+ } catch (e: Exception) {
+ throw CalculatorException.stepExecutionError(1, e)
+ }
+ }
+
+ return CalculationResult(
+ result = result,
+ executionTimeMs = executionTime,
+ formula = request.formula,
+ variables = request.variables,
+ steps = listOf("ํ ํฐํ", "ํ์ฑ", "ํ๊ฐ"),
+ ast = ast
+ )
+ }
+
+ /**
+ * ๋ค๋จ๊ณ ๊ณ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param formulas ๊ณ์ฐํ ์์๋ค
+ * @param variables ๋ณ์ ๋ฐ์ธ๋ฉ
+ * @return ๊ฐ ๋จ๊ณ์ ๊ณ์ฐ ๊ฒฐ๊ณผ
+ * @throws CalculatorException ๊ณ์ฐ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ fun calculateMultiStep(formulas: List, variables: Map = emptyMap()): List {
+ if (formulas.isEmpty()) {
+ throw CalculatorException.emptySteps()
+ }
+
+ if (formulas.size > 50) { // ์ต๋ ๋จ๊ณ ์ ์ ํ
+ throw CalculatorException.tooManySteps(formulas.size, 50)
+ }
+
+ val results = mutableListOf()
+ var currentVariables = variables.toMutableMap()
+
+ formulas.forEachIndexed { index, formula ->
+ try {
+ val request = CalculationRequest(formula, currentVariables)
+ val result = calculate(request)
+ results.add(result)
+
+ currentVariables["${STEP_VARIABLE_PREFIX}${index + 1}"] = result.result ?: 0.0
+
+ } catch (e: Exception) {
+ throw CalculatorException.stepExecutionError(index + 1, e)
+ }
+ }
+
+ return results
+ }
+
+ /**
+ * ์์์ ํ ํฐํํฉ๋๋ค.
+ *
+ * @param formula ํ ํฐํํ ์์
+ * @return ํ ํฐ ๋ฆฌ์คํธ
+ * @throws CalculatorException ํ ํฐํ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ private fun tokenize(formula: String): List {
+ val result = lexer.tokenize(formula)
+ return if (result.isSuccess) result.tokens else emptyList()
+ }
+
+ /**
+ * ํ ํฐ๋ค์ ํ์ฑํ์ฌ AST๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param tokens ํ์ฑํ ํ ํฐ๋ค
+ * @return AST ๋
ธ๋
+ * @throws CalculatorException ํ์ฑ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ private fun parseTokens(tokens: List): ASTNode {
+ return try {
+ parser.parse(tokens)
+ } catch (e: Exception) {
+ throw CalculatorException.stepExecutionError(2, e)
+ }
+ }
+
+ /**
+ * ๊ณ์ฐ ์์ฒญ์ ์ ํจ์ฑ์ ๊ฒ์ฆํฉ๋๋ค.
+ *
+ * @param request ๊ฒ์ฆํ ์์ฒญ
+ * @throws CalculatorException ์ ํจํ์ง ์์ ์์ฒญ์ธ ๊ฒฝ์ฐ
+ */
+ private fun validateRequest(request: CalculationRequest) {
+ if (request.formula.isBlank()) {
+ throw CalculatorException.emptyFormula()
+ }
+
+ if (request.formula.length > maxFormulaLength) {
+ throw CalculatorException.formulaTooLong(request.formula, maxFormulaLength)
+ }
+
+ if (request.variables.size > maxVariables) {
+ throw CalculatorException.tooManyVariables(request.variables.size, maxVariables)
+ }
+ }
+
+ /**
+ * ๊ณ์ฐ๊ธฐ ์ค์ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ค์ ์ ๋ณด ๋งต
+ */
+ fun getConfiguration(): Map = mapOf(
+ "maxFormulaLength" to maxFormulaLength,
+ "maxVariables" to maxVariables,
+ "lexerConfiguration" to lexer.getConfiguration(),
+ "supportedTokenTypes" to ALL_CALCULATOR_TOKENS,
+ "grammarStatistics" to Grammar.getGrammarStatistics()
+ )
+
+ /**
+ * ์์์ ๋ฌธ๋ฒ ์ ํจ์ฑ์ ๊ฒ์ฌํฉ๋๋ค.
+ *
+ * @param formula ๊ฒ์ฌํ ์์
+ * @return ์ ํจํ๋ฉด true, ์๋๋ฉด false
+ * @throws CalculatorException ๊ฒ์ฆ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ fun isValidFormula(formula: String): Boolean = try {
+ tokenize(formula)
+ true
+ } catch (e: Exception) {
+ throw CalculatorException.formulaValidationError(formula, e)
+ }
+
+ /**
+ * ์์์์ ์ฌ์ฉ๋ ๋ณ์๋ค์ ์ถ์ถํฉ๋๋ค.
+ *
+ * @param formula ๋ถ์ํ ์์
+ * @return ๋ณ์ ์ด๋ฆ ์งํฉ
+ * @throws CalculatorException ๋ณ์ ์ถ์ถ ์ค ์ค๋ฅ ๋ฐ์ ์
+ */
+ fun extractVariables(formula: String): Set = try {
+ val tokens = tokenize(formula)
+ tokens.filter { it.type == TokenType.VARIABLE || it.type == TokenType.IDENTIFIER }
+ .map { it.value }
+ .toSet()
+ } catch (e: Exception) {
+ throw CalculatorException.variableExtractionError(formula, e)
+ }
+
+ /**
+ * ๊ณ์ฐ๊ธฐ ํต๊ณ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ์ ๋ณด
+ */
+ fun getStatistics(): Map = mapOf(
+ "configuration" to getConfiguration(),
+ "lexerStats" to lexer.getStatistics(),
+ "grammarStats" to Grammar.getGrammarStatistics()
+ )
+
+ companion object {
+
+ private const val ALL_CALCULATOR_TOKENS = "ALL_CALCULATOR_TOKENS"
+ private const val STEP_VARIABLE_PREFIX = "__entry_calc_step_"
+ /**
+ * ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return Calculator ์ธ์คํด์ค
+ */
+ fun createDefault(): Calculator = Calculator()
+
+ /**
+ * ์ฌ์ฉ์ ์ ์ ์ค์ ์ผ๋ก ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param maxFormulaLength ์ต๋ ์์ ๊ธธ์ด
+ * @param maxVariables ์ต๋ ๋ณ์ ๊ฐ์
+ * @return Calculator ์ธ์คํด์ค
+ */
+ fun create(maxFormulaLength: Int = 5000, maxVariables: Int = 100): Calculator =
+ Calculator(maxFormulaLength = maxFormulaLength, maxVariables = maxVariables)
+
+ /**
+ * ๊ธฐ๋ณธ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ธฐ๋ณธ ๊ณ์ฐ๊ธฐ ์ธ์คํด์ค
+ */
+ fun createBasic(): Calculator = Calculator(
+ maxFormulaLength = 1000,
+ maxVariables = 10
+ )
+
+ /**
+ * ๊ณผํ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณผํ ๊ณ์ฐ๊ธฐ ์ธ์คํด์ค
+ */
+ fun createScientific(): Calculator = Calculator(
+ maxFormulaLength = 5000,
+ maxVariables = 100
+ )
+
+ /**
+ * ํต๊ณ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ๊ณ์ฐ๊ธฐ ์ธ์คํด์ค
+ */
+ fun createStatistical(): Calculator = Calculator(
+ maxFormulaLength = 10000,
+ maxVariables = 500
+ )
+
+ /**
+ * ๊ณตํ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณตํ ๊ณ์ฐ๊ธฐ ์ธ์คํด์ค
+ */
+ fun createEngineering(): Calculator = Calculator(
+ maxFormulaLength = 15000,
+ maxVariables = 1000
+ )
+
+ /**
+ * ์ค์ ๊ณผ ํจ๊ป ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param settings ๊ณ์ฐ๊ธฐ ์ค์
+ * @return ์ค์ ๋ ๊ณ์ฐ๊ธฐ ์ธ์คํด์ค
+ */
+ fun createWithSettings(settings: Map): Calculator {
+ val maxFormula = (settings["maxFormulaLength"] as? Int) ?: 5000
+ val maxVars = (settings["maxVariables"] as? Int) ?: 100
+ return Calculator(maxFormulaLength = maxFormula, maxVariables = maxVars)
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt
new file mode 100644
index 00000000..2bf05f3b
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt
@@ -0,0 +1,394 @@
+package hs.kr.entrydsm.domain.calculator.entities
+
+import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException
+import hs.kr.entrydsm.domain.calculator.values.CalculationResult
+import hs.kr.entrydsm.global.annotation.entities.Entity
+import java.time.Instant
+import java.util.UUID
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * ๊ณ์ฐ ์ธ์
์ ๊ด๋ฆฌํ๋ ์ํฐํฐ์
๋๋ค.
+ *
+ * DDD Entity ํจํด์ ์ ์ฉํ์ฌ ์ฌ์ฉ์์ ๊ณ์ฐ ์ธ์
์ํ์ ์ด๋ ฅ์
+ * ์บก์ํํฉ๋๋ค. ์ฐ์๋ ๊ณ์ฐ๋ค์ ๊ด๋ฆฌํ๊ณ ๊ณ์ฐ ๊ฒฐ๊ณผ์ ์ฌ์ฌ์ฉ๊ณผ
+ * ์ถ์ ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.
+ *
+ * @property sessionId ์ธ์
๊ณ ์ ์๋ณ์
+ * @property userId ์ฌ์ฉ์ ์๋ณ์
+ * @property createdAt ์์ฑ ์๊ฐ
+ * @property lastActivity ๋ง์ง๋ง ํ๋ ์๊ฐ
+ * @property calculations ๊ณ์ฐ ์ด๋ ฅ
+ * @property variables ์ธ์
๋ณ์๋ค
+ * @property settings ์ธ์
์ค์
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.20
+ */
+@Entity(
+ aggregateRoot = hs.kr.entrydsm.domain.calculator.aggregates.Calculator::class,
+ context = "calculator"
+)
+data class CalculationSession(
+ val sessionId: String,
+ val userId: String?,
+ val createdAt: Instant = Instant.now(),
+ val lastActivity: Instant = Instant.now(),
+ val calculations: List = emptyList(),
+ val variables: Map = emptyMap(),
+ val settings: CalculationSettings = CalculationSettings.default(),
+ val metadata: Map = emptyMap()
+) {
+
+ /**
+ * ๊ณ์ฐ ์ค์ ์ ๋ํ๋ด๋ ๋ฐ์ดํฐ ํด๋์ค์
๋๋ค.
+ */
+ data class CalculationSettings(
+ val precision: Int = 10,
+ val angleUnit: AngleUnit = AngleUnit.RADIANS,
+ val numberFormat: NumberFormat = NumberFormat.AUTO,
+ val enableCaching: Boolean = true,
+ val maxHistorySize: Int = 100,
+ val enableOptimization: Boolean = true,
+ val strictMode: Boolean = false
+ ) {
+ enum class AngleUnit { RADIANS, DEGREES }
+ enum class NumberFormat { AUTO, DECIMAL, SCIENTIFIC, ENGINEERING }
+
+ companion object {
+ fun default() = CalculationSettings()
+ }
+ }
+
+ init {
+ if (sessionId.isBlank()) {
+ throw CalculatorException.sessionIdEmpty(sessionId)
+ }
+
+ if (calculations.size > settings.maxHistorySize) {
+ throw CalculatorException.calculationHistoryTooLarge(
+ calculations.size,
+ settings.maxHistorySize
+ )
+ }
+ }
+
+ /**
+ * ์๋ก์ด ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+ *
+ * @param result ์ถ๊ฐํ ๊ณ์ฐ ๊ฒฐ๊ณผ
+ * @return ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun addCalculation(result: CalculationResult): CalculationSession {
+ val newCalculations = if (calculations.size >= settings.maxHistorySize) {
+ calculations.drop(1) + result
+ } else {
+ calculations + result
+ }
+
+ return copy(
+ calculations = newCalculations,
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ๋ณ์๋ฅผ ์ค์ ํฉ๋๋ค.
+ *
+ * @param name ๋ณ์ ์ด๋ฆ
+ * @param value ๋ณ์ ๊ฐ
+ * @return ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun setVariable(name: String, value: Any): CalculationSession {
+ if (name.isBlank()) {
+ throw CalculatorException.variableNameEmpty(name)
+ }
+
+ return copy(
+ variables = variables + (name to value),
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ์ฌ๋ฌ ๋ณ์๋ฅผ ์ผ๊ด ์ค์ ํฉ๋๋ค.
+ *
+ * @param newVariables ์ค์ ํ ๋ณ์๋ค
+ * @return ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun setVariables(newVariables: Map): CalculationSession {
+ return copy(
+ variables = variables + newVariables,
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ๋ณ์๋ฅผ ์ ๊ฑฐํฉ๋๋ค.
+ *
+ * @param name ์ ๊ฑฐํ ๋ณ์ ์ด๋ฆ
+ * @return ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun removeVariable(name: String): CalculationSession {
+ return copy(
+ variables = variables - name,
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ๋ณ์ ๊ฐ์ ์กฐํํฉ๋๋ค.
+ *
+ * @param name ๋ณ์ ์ด๋ฆ
+ * @return ๋ณ์ ๊ฐ (์์ผ๋ฉด null)
+ */
+ fun getVariable(name: String): Any? = variables[name]
+
+ /**
+ * ๋ณ์๊ฐ ์กด์ฌํ๋์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param name ๋ณ์ ์ด๋ฆ
+ * @return ์กด์ฌํ๋ฉด true
+ */
+ fun hasVariable(name: String): Boolean = name in variables
+
+ /**
+ * ๋ง์ง๋ง ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๋ง์ง๋ง ๊ณ์ฐ ๊ฒฐ๊ณผ (์์ผ๋ฉด null)
+ */
+ fun getLastResult(): CalculationResult? = calculations.lastOrNull()
+
+ /**
+ * ํน์ ์ธ๋ฑ์ค์ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @param index ์ธ๋ฑ์ค (0๋ถํฐ ์์)
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ (์ธ๋ฑ์ค๊ฐ ์ ํจํ์ง ์์ผ๋ฉด null)
+ */
+ fun getCalculation(index: Int): CalculationResult? {
+ return if (index in calculations.indices) calculations[index] else null
+ }
+
+ /**
+ * ์ฑ๊ณตํ ๊ณ์ฐ๋ค๋ง ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ฑ๊ณตํ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ค
+ */
+ fun getSuccessfulCalculations(): List {
+ return calculations.filter { it.isSuccess() }
+ }
+
+ /**
+ * ์คํจํ ๊ณ์ฐ๋ค๋ง ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์คํจํ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ค
+ */
+ fun getFailedCalculations(): List {
+ return calculations.filter { !it.isSuccess() }
+ }
+
+ /**
+ * ๊ณ์ฐ ์ด๋ ฅ์ ํด๋ฆฌ์ดํฉ๋๋ค.
+ *
+ * @return ์ด๋ ฅ์ด ํด๋ฆฌ์ด๋ ์ธ์
+ */
+ fun clearHistory(): CalculationSession {
+ return copy(
+ calculations = emptyList(),
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ๋ณ์๋ค์ ํด๋ฆฌ์ดํฉ๋๋ค.
+ *
+ * @return ๋ณ์๊ฐ ํด๋ฆฌ์ด๋ ์ธ์
+ */
+ fun clearVariables(): CalculationSession {
+ return copy(
+ variables = emptyMap(),
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ์ธ์
์ ์์ ํ ์ด๊ธฐํํฉ๋๋ค.
+ *
+ * @return ์ด๊ธฐํ๋ ์ธ์
+ */
+ fun reset(): CalculationSession {
+ return copy(
+ calculations = emptyList(),
+ variables = emptyMap(),
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ์ธ์
์ค์ ์ ์
๋ฐ์ดํธํฉ๋๋ค.
+ *
+ * @param newSettings ์๋ก์ด ์ค์
+ * @return ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun updateSettings(newSettings: CalculationSettings): CalculationSession {
+ val adjustedCalculations = if (newSettings.maxHistorySize < calculations.size) {
+ calculations.takeLast(newSettings.maxHistorySize)
+ } else {
+ calculations
+ }
+
+ return copy(
+ settings = newSettings,
+ calculations = adjustedCalculations,
+ lastActivity = Instant.now()
+ )
+ }
+
+ /**
+ * ์ธ์
์ด ํ์ฑ ์ํ์ธ์ง ํ์ธํฉ๋๋ค.
+ *
+ * @param timeoutMinutes ํ์์์ ์๊ฐ (๋ถ)
+ * @return ํ์ฑ ์ํ์ด๋ฉด true
+ */
+ fun isActive(timeoutMinutes: Long = 30): Boolean {
+ val now = Instant.now()
+ val timeoutInstant = lastActivity.plusSeconds(timeoutMinutes * 60)
+ return now.isBefore(timeoutInstant)
+ }
+
+ /**
+ * ์ธ์
์ ์ด ๊ณ์ฐ ์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ด ๊ณ์ฐ ์๊ฐ (๋ฐ๋ฆฌ์ด)
+ */
+ fun getTotalCalculationTime(): Long {
+ return calculations.sumOf { it.executionTimeMs }
+ }
+
+ /**
+ * ํ๊ท ๊ณ์ฐ ์๊ฐ์ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํ๊ท ๊ณ์ฐ ์๊ฐ (๋ฐ๋ฆฌ์ด)
+ */
+ fun getAverageCalculationTime(): Double {
+ return if (calculations.isEmpty()) 0.0 else getTotalCalculationTime().toDouble() / calculations.size
+ }
+
+ /**
+ * ์ฑ๊ณต๋ฅ ์ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @return ์ฑ๊ณต๋ฅ (0.0 ~ 1.0)
+ */
+ fun getSuccessRate(): Double {
+ return if (calculations.isEmpty()) 0.0 else getSuccessfulCalculations().size.toDouble() / calculations.size
+ }
+
+ /**
+ * ์ธ์
์ ํต๊ณ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ์ ๋ณด ๋งต
+ */
+ fun getStatistics(): Map = mapOf(
+ "sessionId" to sessionId,
+ "userId" to (userId ?: "anonymous"),
+ "totalCalculations" to calculations.size,
+ "successfulCalculations" to getSuccessfulCalculations().size,
+ "failedCalculations" to getFailedCalculations().size,
+ "successRate" to getSuccessRate(),
+ "totalVariables" to variables.size,
+ "totalCalculationTime" to getTotalCalculationTime(),
+ "averageCalculationTime" to getAverageCalculationTime(),
+ "sessionDuration" to (lastActivity.epochSecond - createdAt.epochSecond),
+ "isActive" to isActive(),
+ "settings" to settings
+ )
+
+ /**
+ * ์ธ์
์ ์์ฝํฉ๋๋ค.
+ *
+ * @return ์ธ์
์์ฝ ๋ฌธ์์ด
+ */
+ fun summarize(): String = buildString {
+ appendLine("=== ๊ณ์ฐ ์ธ์
์์ฝ ===")
+ appendLine("์ธ์
ID: $sessionId")
+ appendLine("์ฌ์ฉ์: ${userId ?: "์ต๋ช
"}")
+ appendLine("์ด ๊ณ์ฐ: ${calculations.size}๊ฐ")
+ appendLine("์ฑ๊ณต๋ฅ : ${"%.1f".format(getSuccessRate() * 100)}%")
+ appendLine("๋ณ์: ${variables.size}๊ฐ")
+ appendLine("์ด ๊ณ์ฐ ์๊ฐ: ${getTotalCalculationTime()}ms")
+ appendLine("ํ๊ท ๊ณ์ฐ ์๊ฐ: ${"%.2f".format(getAverageCalculationTime())}ms")
+ appendLine("์ธ์
์ง์ ์๊ฐ: ${(lastActivity.epochSecond - createdAt.epochSecond)}์ด")
+ appendLine("ํ์ฑ ์ํ: ${if (isActive()) "์" else "์๋์ค"}")
+ }
+
+ companion object {
+ /**
+ * ๋์์ฑ ํ๊ฒฝ์์ ๊ณ ์ ํ ์ธ์
ID ์์ฑ์ ์ํ atomic counter
+ */
+ private val sessionCounter = AtomicLong(0)
+
+ /**
+ * ๋์์ฑ ํ๊ฒฝ์์ ๊ณ ์ ํ ์ธ์
ID๋ฅผ ์์ฑํฉ๋๋ค.
+ * ํ์์คํฌํ, atomic counter, UUID๋ฅผ ์กฐํฉํ์ฌ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ ์ต์ํํฉ๋๋ค.
+ *
+ * @param prefix ์ธ์
ID ์ ๋์ฌ
+ * @param includeUuid UUID ํฌํจ ์ฌ๋ถ (๊ธฐ๋ณธ๊ฐ: true)
+ * @return ๊ณ ์ ํ ์ธ์
ID
+ */
+ private fun generateUniqueSessionId(prefix: String, includeUuid: Boolean = true): String {
+ val timestamp = System.currentTimeMillis()
+ val counter = sessionCounter.incrementAndGet()
+
+ return if (includeUuid) {
+ val uuid = UUID.randomUUID().toString().take(8)
+ "${prefix}_${timestamp}_${counter}_${uuid}"
+ } else {
+ "${prefix}_${timestamp}_${counter}"
+ }
+ }
+
+ /**
+ * ์๋ก์ด ์ธ์
์ ์์ฑํฉ๋๋ค.
+ *
+ * @param sessionId ์ธ์
ID
+ * @param userId ์ฌ์ฉ์ ID
+ * @return ์๋ก์ด ์ธ์
+ */
+ fun create(sessionId: String, userId: String? = null): CalculationSession {
+ if (sessionId.isBlank()) {
+ throw CalculatorException.sessionIdEmpty(sessionId)
+ }
+
+ return CalculationSession(sessionId = sessionId, userId = userId)
+ }
+
+ /**
+ * ์์ ์ธ์
์ ์์ฑํฉ๋๋ค.
+ * ๋์์ฑ ํ๊ฒฝ์์ ๊ณ ์ ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด ํ์์คํฌํ, UUID, atomic counter๋ฅผ ์กฐํฉํฉ๋๋ค.
+ *
+ * @return ์์ ์ธ์
+ */
+ fun createTemporary(): CalculationSession {
+ val sessionId = generateUniqueSessionId("temp", includeUuid = true)
+ return create(sessionId)
+ }
+
+ /**
+ * ์ฌ์ฉ์ ์ธ์
์ ์์ฑํฉ๋๋ค.
+ * ๋์์ฑ ํ๊ฒฝ์์ ๊ณ ์ ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด ํ์์คํฌํ, atomic counter๋ฅผ ์กฐํฉํฉ๋๋ค.
+ *
+ * @param userId ์ฌ์ฉ์ ID
+ * @return ์ฌ์ฉ์ ์ธ์
+ */
+ fun createForUser(userId: String): CalculationSession {
+ if (userId.isBlank()) {
+ throw CalculatorException.userIdEmpty(userId)
+ }
+
+ val sessionId = generateUniqueSessionId("user_${userId}", includeUuid = false)
+ return create(sessionId, userId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt
new file mode 100644
index 00000000..651f1b8a
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt
@@ -0,0 +1,548 @@
+package hs.kr.entrydsm.domain.calculator.exceptions
+
+import hs.kr.entrydsm.global.exception.DomainException
+import hs.kr.entrydsm.global.exception.ErrorCode
+
+/**
+ * Calculator ๋๋ฉ์ธ์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ํด๋์ค์
๋๋ค.
+ *
+ * ์์ ์ฒ๋ฆฌ, ๊ณ์ฐ ๋จ๊ณ ์คํ, ๋ณ์ ๊ด๋ฆฌ ๋ฑ์ ๊ณ์ฐ๊ธฐ ํต์ฌ ๊ธฐ๋ฅ์์
+ * ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
+ *
+ * @property formula ์ค๋ฅ์ ๊ด๋ จ๋ ์์ (์ ํ์ฌํญ)
+ * @property step ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ณ์ฐ ๋จ๊ณ (์ ํ์ฌํญ)
+ * @property variableCount ๋ณ์ ๊ฐ์ (์ ํ์ฌํญ)
+ * @property maxAllowed ํ์ฉ๋ ์ต๋๊ฐ (์ ํ์ฌํญ)
+ * @property missingVariables ๋๋ฝ๋ ๋ณ์ ๋ฆฌ์คํธ (์ ํ์ฌํญ)
+ * @property reason ์ฌ์ (์ ํ์ฌํญ)
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.15
+ */
+class CalculatorException(
+ errorCode: ErrorCode,
+ val formula: String? = null,
+ val step: Int? = null,
+ val variableCount: Int? = null,
+ val maxAllowed: Int? = null,
+ val missingVariables: List = emptyList(),
+ val reason: String? = null,
+ message: String = buildCalculatorMessage(errorCode, formula, step, variableCount, maxAllowed, missingVariables, reason),
+ cause: Throwable? = null
+) : DomainException(errorCode, message, cause) {
+
+ companion object {
+ /**
+ * Calculator ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
+ *
+ * @param errorCode ์ค๋ฅ ์ฝ๋
+ * @param formula ์์
+ * @param step ๊ณ์ฐ ๋จ๊ณ
+ * @param variableCount ๋ณ์ ๊ฐ์
+ * @param maxAllowed ์ต๋ ํ์ฉ๊ฐ
+ * @param missingVariables ๋๋ฝ๋ ๋ณ์๋ค
+ * @param reason ์ฌ์
+ * @return ๊ตฌ์ฑ๋ ๋ฉ์์ง
+ */
+ private fun buildCalculatorMessage(
+ errorCode: ErrorCode,
+ formula: String?,
+ step: Int?,
+ variableCount: Int?,
+ maxAllowed: Int?,
+ missingVariables: List,
+ reason: String?
+ ): String {
+ val baseMessage = errorCode.description
+ val details = mutableListOf()
+
+ formula?.let { details.add("์์: $it") }
+ step?.let { details.add("๋จ๊ณ: $it") }
+ reason?.let { details.add("์ฌ์ : $it") }
+
+ if (variableCount != null && maxAllowed != null) {
+ details.add("๋ณ์: $variableCount (์ต๋: $maxAllowed)")
+ } else {
+ variableCount?.let { details.add("๋ณ์๊ฐ์: $it") }
+ maxAllowed?.let { details.add("์ต๋ํ์ฉ: $it") }
+ }
+ if (missingVariables.isNotEmpty()) {
+ details.add("๋๋ฝ๋ณ์: ${missingVariables.joinToString(", ")}")
+ }
+
+ return if (details.isNotEmpty()) {
+ "$baseMessage (${details.joinToString(", ")})"
+ } else {
+ baseMessage
+ }
+ }
+
+ /**
+ * ๋น ์์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun emptyFormula(): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.EMPTY_FORMULA
+ )
+ }
+
+ /**
+ * ์์์ด ๋๋ฌด ๊ธด ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param formula ๋๋ฌด ๊ธด ์์
+ * @param maxLength ์ต๋ ํ์ฉ ๊ธธ์ด
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun formulaTooLong(formula: String, maxLength: Int): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.FORMULA_TOO_LONG,
+ formula = formula.take(50) + if (formula.length > 50) "..." else "",
+ maxAllowed = maxLength
+ )
+ }
+
+ /**
+ * ๋น ๊ณ์ฐ ๋จ๊ณ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun emptySteps(): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.EMPTY_STEPS
+ )
+ }
+
+ /**
+ * ๊ณ์ฐ ๋จ๊ณ๊ฐ ๋๋ฌด ๋ง์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param stepCount ์ค์ ๋จ๊ณ ์
+ * @param maxSteps ์ต๋ ํ์ฉ ๋จ๊ณ ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun tooManySteps(stepCount: Int, maxSteps: Int): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.TOO_MANY_STEPS,
+ variableCount = stepCount,
+ maxAllowed = maxSteps
+ )
+ }
+
+ /**
+ * ๋ณ์๊ฐ ๋๋ฌด ๋ง์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param variableCount ์ค์ ๋ณ์ ๊ฐ์
+ * @param maxVariables ์ต๋ ํ์ฉ ๋ณ์ ๊ฐ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun tooManyVariables(variableCount: Int, maxVariables: Int): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.TOO_MANY_VARIABLES,
+ variableCount = variableCount,
+ maxAllowed = maxVariables
+ )
+ }
+
+ /**
+ * ํ์ ๋ณ์ ๋๋ฝ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param missingVariables ๋๋ฝ๋ ๋ณ์๋ค
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun missingVariables(missingVariables: List): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.MISSING_VARIABLES,
+ missingVariables = missingVariables
+ )
+ }
+
+ /**
+ * ๋จ๊ณ ์คํ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param step ์คํ ์คํจํ ๋จ๊ณ
+ * @param cause ์์ธ ์์ธ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepExecutionError(step: Int, cause: Throwable? = null): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.STEP_EXECUTION_ERROR,
+ step = step,
+ cause = cause
+ )
+ }
+
+ /**
+ * ์์ ๊ฒ์ฆ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param formula ๊ฒ์ฆ ์คํจํ ์์
+ * @param cause ์์ธ ์์ธ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun formulaValidationError(formula: String, cause: Throwable? = null): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.FORMULA_VALIDATION_ERROR,
+ formula = formula,
+ cause = cause
+ )
+ }
+
+ /**
+ * ๋ณ์ ์ถ์ถ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param formula ๋ณ์ ์ถ์ถ ์คํจํ ์์
+ * @param cause ์์ธ ์์ธ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun variableExtractionError(formula: String, cause: Throwable? = null): CalculatorException {
+ return CalculatorException(
+ errorCode = ErrorCode.VARIABLE_EXTRACTION_ERROR,
+ formula = formula,
+ cause = cause
+ )
+ }
+
+ /**
+ * ์ธ์
ID๊ฐ ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ์ธ์
ID
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun sessionIdEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.SESSION_ID_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ๊ณ์ฐ ์ด๋ ฅ ๊ฐ์๊ฐ ์ต๋ ํฌ๊ธฐ๋ฅผ ์ด๊ณผํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ํ์ฌ ๊ณ์ฐ ์ด๋ ฅ ํฌ๊ธฐ
+ * @param max ํ์ฉ๋๋ ์ต๋ ํฌ๊ธฐ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun calculationHistoryTooLarge(actual: Int, max: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.CALCULATION_HISTORY_TOO_LARGE,
+ reason = "actual=$actual, max=$max"
+ )
+
+ /**
+ * ๋ณ์ ์ด๋ฆ์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋ณ์๋ช
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun variableNameEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.VARIABLE_NAME_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ์ฌ์ฉ์ ID๊ฐ ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ์ฌ์ฉ์ ID
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun userIdEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.USER_ID_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ์์ ๋ชฉ๋ก์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun emptyExpressions(): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.EMPTY_EXPRESSIONS
+ )
+
+ /**
+ * ๊ณ์ฐ ๋จ๊ณ๊ฐ ์ ์ฒด ํ์์์์ ์ด๊ณผํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param stepIndex ์ด๊ณผํ ๋จ๊ณ ์ธ๋ฑ์ค
+ * @param remainingTime ๋จ์ ์๊ฐ(ms)
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepTimeoutExceeded(stepIndex: Int, remainingTime: Long): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.STEP_TIMEOUT_EXCEEDED,
+ reason = "stepIndex=$stepIndex, remainingTime=$remainingTime"
+ )
+
+ /**
+ * ๊ณ์ฐ ์์ฒญ ๋ชฉ๋ก์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun requestListEmpty(): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.REQUEST_LIST_EMPTY
+ )
+
+ /**
+ * ๋์์ฑ ์์ค์ด 0 ์ดํ์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋์์ฑ ์์ค
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun invalidConcurrencyLevel(actual: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.INVALID_CONCURRENCY_LEVEL,
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ๋ฒํผ ํฌ๊ธฐ๊ฐ 0 ์ดํ์ผ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋ฒํผ ํฌ๊ธฐ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun invalidBufferSize(actual: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.INVALID_BUFFER_SIZE,
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ์ต๋ ์ฌ์๋ ํ์๋ฅผ ์ด๊ณผํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun maxRetryExceeded(): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.MAX_RETRY_EXCEEDED
+ )
+
+ /**
+ * ์ ํจํ์ง ์์ AST ๋
ธ๋ ํ์
์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualType ์
๋ ฅ๋ AST ๋
ธ๋ ํ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun invalidAstNodeType(actualType: String): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.INVALID_AST_NODE_TYPE,
+ reason = "actualType=$actualType"
+ )
+
+ /**
+ * ์ต์
ํค๊ฐ ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ์ต์
ํค
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun optionKeyEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.OPTION_KEY_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ์คํ ์๊ฐ์ด 0๋ณด๋ค ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ์คํ ์๊ฐ(ms)
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun executionTimeNegative(actual: Long): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.EXECUTION_TIME_NEGATIVE,
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ๋ณํฉํ ๊ฒฐ๊ณผ๊ฐ ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun mergeResultsEmpty(): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.MERGE_RESULTS_EMPTY
+ )
+
+ /**
+ * ๋จ๊ณ ์ด๋ฆ์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋จ๊ณ ์ด๋ฆ
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepNameEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.STEP_NAME_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ๋จ๊ณ ์ด๋ฆ์ด ๋๋ฌด ๊ธธ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actualLength ์ค์ ๊ธธ์ด
+ * @param maxLength ํ์ฉ๋๋ ์ต๋ ๊ธธ์ด
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepNameTooLong(actualLength: Int, maxLength: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.STEP_NAME_TOO_LONG,
+ reason = "length=$actualLength, max=$maxLength"
+ )
+
+ /**
+ * ๊ฒฐ๊ณผ ๋ณ์๋ช
์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋ณ์๋ช
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun resultVariableNameEmpty(actual: String?): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.RESULT_VARIABLE_NAME_EMPTY,
+ reason = "actual=${actual ?: "null"}"
+ )
+
+ /**
+ * ๊ฒฐ๊ณผ ๋ณ์๋ช
์ด ์ ํจํ์ง ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์
๋ ฅ๋ ๋ณ์๋ช
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun resultVariableNameInvalid(actual: String): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.RESULT_VARIABLE_NAME_INVALID,
+ reason = "actual=$actual"
+ )
+
+ /**
+ * ๊ณ์ฐ ๋จ๊ณ ๋ชฉ๋ก์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepsEmpty(): CalculatorException =
+ CalculatorException(errorCode = ErrorCode.STEPS_EMPTY)
+
+ /**
+ * ๊ณ์ฐ ๋จ๊ณ ๊ฐ์๊ฐ ์ต๋ ํ์ฉ์น๋ฅผ ์ด๊ณผํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์ค์ ๊ณ์ฐ ๋จ๊ณ ๊ฐ์
+ * @param max ํ์ฉ๋๋ ์ต๋ ๊ณ์ฐ ๋จ๊ณ ๊ฐ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepsTooMany(actual: Int, max: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.STEPS_TOO_MANY,
+ reason = "actual=$actual, max=$max"
+ )
+
+ /**
+ * ๋ณ์ ๊ฐ์๊ฐ ์ต๋ ํ์ฉ์น๋ฅผ ์ด๊ณผํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์ค์ ๋ณ์ ๊ฐ์
+ * @param max ํ์ฉ๋๋ ์ต๋ ๋ณ์ ๊ฐ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun variablesTooMany(actual: Int, max: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.VARIABLES_TOO_MANY,
+ reason = "actual=$actual, max=$max"
+ )
+
+ /**
+ * ํน์ ๋จ๊ณ์ ์์์ด ๋น์ด ์์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param stepIndex ์์์ด ๋น์ด ์๋ ๋จ๊ณ์ ์ธ๋ฑ์ค (0๋ถํฐ ์์)
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun stepFormulaEmpty(stepIndex: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.STEP_FORMULA_EMPTY,
+ reason = "stepIndex=$stepIndex"
+ )
+
+ /**
+ * ์ธ๋ฑ์ค๊ฐ [0..maxInclusive] ๋ฒ์๋ฅผ ๋ฒ์ด๋ฌ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์ ๋ฌ๋ ์ธ๋ฑ์ค ๊ฐ
+ * @param maxInclusive ํ์ฉ๋๋ ์ต๋ ์ธ๋ฑ์ค ๊ฐ (ํฌํจ)
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun indexOutOfRangeInclusive(actual: Int, maxInclusive: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.INDEX_OUT_OF_RANGE_INCLUSIVE,
+ reason = "index=$actual, range=0..$maxInclusive"
+ )
+
+ /**
+ * ์ธ๋ฑ์ค๊ฐ [0..maxExclusive) ๋ฒ์๋ฅผ ๋ฒ์ด๋ฌ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์ ๋ฌ๋ ์ธ๋ฑ์ค ๊ฐ
+ * @param maxExclusive ํ์ฉ๋๋ ์ต๋ ์ธ๋ฑ์ค ๊ฐ (๋ฏธํฌํจ)
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun indexOutOfRangeExclusive(actual: Int, maxExclusive: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.INDEX_OUT_OF_RANGE_EXCLUSIVE,
+ reason = "index=$actual, range=0..${maxExclusive - 1}"
+ )
+
+ /**
+ * ์ต์ ๋จ๊ณ ๊ฐ์ ์๊ฑด์ ์ถฉ์กฑํ์ง ๋ชปํ์ ๋์ ์ค๋ฅ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param actual ์ค์ ๋จ๊ณ ๊ฐ์
+ * @param min ์๊ตฌ๋๋ ์ต์ ๋จ๊ณ ๊ฐ์
+ * @return CalculatorException ์ธ์คํด์ค
+ */
+ fun minStepsRequired(actual: Int, min: Int): CalculatorException =
+ CalculatorException(
+ errorCode = ErrorCode.MIN_STEPS_REQUIRED,
+ reason = "actual=$actual, min=$min"
+ )
+ }
+
+ /**
+ * Calculator ์ค๋ฅ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํ๋ ๋งต์ผ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์์, ๋จ๊ณ, ๋ณ์ ์ ๋ณด๊ฐ ํฌํจ๋ ๋งต
+ */
+ fun getCalculatorInfo(): Map {
+ val info = mutableMapOf()
+
+ formula?.let { info["formula"] = it }
+ step?.let { info["step"] = it }
+ variableCount?.let { info["variableCount"] = it }
+ maxAllowed?.let { info["maxAllowed"] = it }
+ if (missingVariables.isNotEmpty()) { info["missingVariables"] = missingVariables }
+
+ return info
+ }
+
+ /**
+ * ์ ์ฒด ์ค๋ฅ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํ๋ ๋งต์ผ๋ก ๋ฐํํฉ๋๋ค.
+ *
+ * @return ๊ธฐ๋ณธ ์ค๋ฅ ์ ๋ณด์ Calculator ์ ๋ณด๊ฐ ๊ฒฐํฉ๋ ๋งต
+ */
+ fun getFullErrorInfo(): Map {
+ val baseInfo = super.toErrorInfo().toMutableMap()
+ val calculatorInfo = getCalculatorInfo()
+
+ calculatorInfo.forEach { (key, value) ->
+ when (value) {
+ is List<*> -> baseInfo[key] = value.joinToString(", ")
+ else -> baseInfo[key] = value?.toString() ?: ""
+ }
+ }
+
+ return baseInfo
+ }
+
+ override fun toString(): String {
+ val calculatorDetails = getCalculatorInfo()
+ return if (calculatorDetails.isNotEmpty()) {
+ "${super.toString()}, calculator=${calculatorDetails}"
+ } else {
+ super.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt
new file mode 100644
index 00000000..c0ee908f
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt
@@ -0,0 +1,421 @@
+package hs.kr.entrydsm.domain.calculator.factories
+
+import hs.kr.entrydsm.domain.calculator.aggregates.Calculator
+import hs.kr.entrydsm.domain.calculator.entities.CalculationSession
+import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException
+import hs.kr.entrydsm.domain.calculator.values.CalculationRequest
+import hs.kr.entrydsm.domain.calculator.values.CalculationResult
+import hs.kr.entrydsm.domain.factories.EnvironmentFactory
+import hs.kr.entrydsm.global.annotation.factory.Factory
+import hs.kr.entrydsm.global.annotation.factory.type.Complexity
+import hs.kr.entrydsm.global.annotation.specification.type.Priority
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * Calculator ๋๋ฉ์ธ ๊ฐ์ฒด๋ค์ ์์ฑํ๋ ํฉํ ๋ฆฌ์
๋๋ค.
+ *
+ * DDD Factory ํจํด์ ์ ์ฉํ์ฌ ๊ณ์ฐ๊ธฐ์ ๊ด๋ จ๋ ๊ฐ์ฒด๋ค์ ์์ฑ๊ณผ ๊ตฌ์ฑ์
+ * ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ค์ํ ์ ํ์ ๊ณ์ฐ๊ธฐ์ ์ธ์
์ ์์ฑํ๊ณ
+ * ์ ์ ํ ์ค์ ๊ณผ ์ ์ฑ
์ ์ ์ฉํ์ฌ ์ผ๊ด๋ ๊ฐ์ฒด ์์ฑ์ ๋ณด์ฅํฉ๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.07.20
+ */
+@Factory(
+ context = "calculator",
+ complexity = Complexity.HIGH,
+ cache = true
+)
+class CalculatorFactory {
+
+ companion object {
+ private val createdCalculatorCount = AtomicLong(0L)
+ private val createdSessionCount = AtomicLong(0L)
+ private val createdRequestCount = AtomicLong(0L)
+
+ @Volatile
+ private var instance: CalculatorFactory? = null
+
+ fun getInstance(): CalculatorFactory {
+ return instance ?: synchronized(this) {
+ instance ?: CalculatorFactory().also { instance = it }
+ }
+ }
+
+ // ํธ์ ๋ฉ์๋๋ค
+ fun quickCreateBasicCalculator(): Calculator = getInstance().createBasicCalculator()
+ fun quickCreateScientificCalculator(): Calculator = getInstance().createScientificCalculator()
+ fun quickCreateSession(userId: String? = null): CalculationSession = getInstance().createSession(userId)
+ fun quickCreateRequest(expression: String, variables: Map = emptyMap()): CalculationRequest =
+ getInstance().createRequest(expression, variables)
+ }
+
+ /**
+ * ๊ธฐ๋ณธ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ธฐ๋ณธ ์ค์ ์ ๊ณ์ฐ๊ธฐ
+ */
+ fun createBasicCalculator(): Calculator {
+ createdCalculatorCount.incrementAndGet()
+ return Calculator.createBasic()
+ }
+
+ /**
+ * ๊ณผํ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณผํ ๊ณ์ฐ ๊ธฐ๋ฅ์ด ํฌํจ๋ ๊ณ์ฐ๊ธฐ
+ */
+ fun createScientificCalculator(): Calculator {
+ createdCalculatorCount.incrementAndGet()
+ return Calculator.createScientific()
+ }
+
+ /**
+ * ํต๊ณ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ํจ์๊ฐ ํฌํจ๋ ๊ณ์ฐ๊ธฐ
+ */
+ fun createStatisticalCalculator(): Calculator {
+ createdCalculatorCount.incrementAndGet()
+ return Calculator.createStatistical()
+ }
+
+ /**
+ * ๊ณตํ์ฉ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณตํ ๊ณ์ฐ ๊ธฐ๋ฅ์ด ํฌํจ๋ ๊ณ์ฐ๊ธฐ
+ */
+ fun createEngineeringCalculator(): Calculator {
+ createdCalculatorCount.incrementAndGet()
+ return Calculator.createEngineering()
+ }
+
+ /**
+ * ์ฌ์ฉ์ ์ ์ ์ค์ ์ผ๋ก ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param precision ์ ๋ฐ๋
+ * @param angleUnit ๊ฐ๋ ๋จ์
+ * @param enableCaching ์บ์ฑ ํ์ฑํ ์ฌ๋ถ
+ * @param enableOptimization ์ต์ ํ ํ์ฑํ ์ฌ๋ถ
+ * @return ์ฌ์ฉ์ ์ ์ ๊ณ์ฐ๊ธฐ
+ */
+ fun createCustomCalculator(
+ precision: Int = 10,
+ angleUnit: CalculationSession.CalculationSettings.AngleUnit = CalculationSession.CalculationSettings.AngleUnit.RADIANS,
+ enableCaching: Boolean = true,
+ enableOptimization: Boolean = true
+ ): Calculator {
+ createdCalculatorCount.incrementAndGet()
+
+ val settingsMap = mapOf(
+ "precision" to precision,
+ "angleUnit" to angleUnit,
+ "enableCaching" to enableCaching,
+ "enableOptimization" to enableOptimization
+ )
+
+ return Calculator.createWithSettings(settingsMap)
+ }
+
+ /**
+ * ์๋ก์ด ๊ณ์ฐ ์ธ์
์ ์์ฑํฉ๋๋ค.
+ *
+ * @param userId ์ฌ์ฉ์ ID (์ ํ์ )
+ * @return ์๋ก์ด ๊ณ์ฐ ์ธ์
+ */
+ fun createSession(userId: String? = null): CalculationSession {
+ createdSessionCount.incrementAndGet()
+ return if (userId != null) {
+ CalculationSession.createForUser(userId)
+ } else {
+ CalculationSession.createTemporary()
+ }
+ }
+
+ /**
+ * ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ์ฌ์ฉ์ ์ธ์
์ ์์ฑํฉ๋๋ค.
+ *
+ * @param userId ์ฌ์ฉ์ ID
+ * @return ์ฌ์ฉ์ ์ธ์
+ */
+ fun createUserSession(userId: String): CalculationSession {
+ if (userId.isBlank()) {
+ throw CalculatorException.userIdEmpty(userId)
+ }
+
+ createdSessionCount.incrementAndGet()
+ return CalculationSession.createForUser(userId)
+ }
+
+ /**
+ * ์์ ์ธ์
์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ์์ ์ธ์
+ */
+ fun createTemporarySession(): CalculationSession {
+ createdSessionCount.incrementAndGet()
+ return CalculationSession.createTemporary()
+ }
+
+ /**
+ * ์ฌ์ฉ์ ์ ์ ์ค์ ์ผ๋ก ์ธ์
์ ์์ฑํฉ๋๋ค.
+ *
+ * @param sessionId ์ธ์
ID
+ * @param userId ์ฌ์ฉ์ ID
+ * @param settings ๊ณ์ฐ ์ค์
+ * @param variables ์ด๊ธฐ ๋ณ์๋ค
+ * @return ์ฌ์ฉ์ ์ ์ ์ธ์
+ */
+ fun createCustomSession(
+ sessionId: String,
+ userId: String? = null,
+ settings: CalculationSession.CalculationSettings = CalculationSession.CalculationSettings.default(),
+ variables: Map = emptyMap()
+ ): CalculationSession {
+ createdSessionCount.incrementAndGet()
+ return CalculationSession(
+ sessionId = sessionId,
+ userId = userId,
+ variables = variables,
+ settings = settings
+ )
+ }
+
+ /**
+ * ๊ณ์ฐ ์์ฒญ์ ์์ฑํฉ๋๋ค.
+ *
+ * @param formula ์์
+ * @param variables ๋ณ์๋ค (์ ํ์ )
+ * @return ๊ณ์ฐ ์์ฒญ
+ */
+ fun createRequest(
+ formula: String,
+ variables: Map = emptyMap()
+ ): CalculationRequest {
+ if (formula.isBlank()) {
+ throw CalculatorException.emptyFormula()
+ }
+ createdRequestCount.incrementAndGet()
+
+ return CalculationRequest(
+ formula = formula,
+ variables = variables
+ )
+ }
+
+ /**
+ * ์ฐ์ ์์๊ฐ ์๋ ๊ณ์ฐ ์์ฒญ์ ์์ฑํฉ๋๋ค.
+ *
+ * @param formula ์์
+ * @param priority ์ฐ์ ์์
+ * @param variables ๋ณ์๋ค
+ * @return ์ฐ์ ์์ ๊ณ์ฐ ์์ฒญ
+ */
+ fun createPriorityRequest(
+ formula: String,
+ priority: Priority,
+ variables: Map = emptyMap()
+ ): CalculationRequest {
+ createdRequestCount.incrementAndGet()
+ val options = mapOf("priority" to priority.name)
+ return CalculationRequest(
+ formula = formula,
+ variables = variables,
+ options = options
+ )
+ }
+
+ /**
+ * ์ผ๊ด ๊ณ์ฐ ์์ฒญ๋ค์ ์์ฑํฉ๋๋ค.
+ *
+ * @param expressions ์์๋ค
+ * @param variables ๊ณตํต ๋ณ์๋ค
+ * @return ๊ณ์ฐ ์์ฒญ ๋ชฉ๋ก
+ */
+ fun createBatchRequests(
+ expressions: List,
+ variables: Map = emptyMap()
+ ): List {
+ if (expressions.isEmpty()) {
+ throw CalculatorException.emptyExpressions()
+ }
+
+ return expressions.map { expression ->
+ createRequest(expression, variables)
+ }
+ }
+
+ /**
+ * ์ฑ๊ณตํ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param requestId ์์ฒญ ID
+ * @param result ๊ณ์ฐ ๊ฒฐ๊ณผ ๊ฐ
+ * @param executionTime ์คํ ์๊ฐ
+ * @return ์ฑ๊ณต ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun createSuccessResult(
+ formula: String,
+ result: Any,
+ executionTimeMs: Long = 0
+ ): CalculationResult {
+ return CalculationResult(
+ result = result,
+ executionTimeMs = executionTimeMs,
+ formula = formula
+ )
+ }
+
+ /**
+ * ์คํจํ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param requestId ์์ฒญ ID
+ * @param error ์ค๋ฅ ์ ๋ณด
+ * @param executionTime ์คํ ์๊ฐ
+ * @return ์คํจ ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun createFailureResult(
+ formula: String,
+ error: String,
+ executionTimeMs: Long = 0
+ ): CalculationResult {
+ return CalculationResult(
+ result = null,
+ executionTimeMs = executionTimeMs,
+ formula = formula,
+ errors = listOf(error)
+ )
+ }
+
+ /**
+ * ์์ธ๋ก๋ถํฐ ์คํจ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param requestId ์์ฒญ ID
+ * @param exception ์์ธ
+ * @param executionTime ์คํ ์๊ฐ
+ * @return ์คํจ ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun createFailureFromException(
+ formula: String,
+ exception: Exception,
+ executionTimeMs: Long = 0
+ ): CalculationResult {
+ return CalculationResult(
+ result = null,
+ executionTimeMs = executionTimeMs,
+ formula = formula,
+ errors = listOf("๊ณ์ฐ ์ค๋ฅ: ${exception.message}")
+ )
+ }
+
+ /**
+ * ๊ธฐ๋ณธ ๋ณ์๋ค์ ๊ฐ์ง ํ๊ฒฝ์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ธฐ๋ณธ ๋ณ์ ๋งต
+ */
+ fun createDefaultEnvironment(): Map {
+ return EnvironmentFactory.createBasicEnvironment().mapValues { it.value }
+ }
+
+ /**
+ * ๊ณผํ ๊ณ์ฐ์ฉ ํ๊ฒฝ์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณผํ ์์๊ฐ ํฌํจ๋ ๋ณ์ ๋งต
+ */
+ fun createScientificEnvironment(): Map {
+ return EnvironmentFactory.createScientificEnvironment().mapValues { it.value }
+ }
+
+ /**
+ * ๊ณตํ์ฉ ํ๊ฒฝ์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๊ณตํ ์์๊ฐ ํฌํจ๋ ๋ณ์ ๋งต
+ */
+ fun createEngineeringEnvironment(): Map {
+ return EnvironmentFactory.createEngineeringEnvironment().mapValues { it.value }
+ }
+
+ /**
+ * ํต๊ณ์ฉ ํ๊ฒฝ์ ์์ฑํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ์์๊ฐ ํฌํจ๋ ๋ณ์ ๋งต
+ */
+ fun createStatisticalEnvironment(): Map {
+ return EnvironmentFactory.createStatisticalEnvironment().mapValues { it.value }
+ }
+
+ /**
+ * ๊ณ ์ฑ๋ฅ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @param maxConcurrency ์ต๋ ๋์ ๊ณ์ฐ ์
+ * @param cacheSize ์บ์ ํฌ๊ธฐ
+ * @return ๊ณ ์ฑ๋ฅ ๊ณ์ฐ๊ธฐ
+ */
+ fun createHighPerformanceCalculator(
+ maxConcurrency: Int = 10,
+ cacheSize: Int = 1000
+ ): Calculator {
+ createdCalculatorCount.incrementAndGet()
+
+ val settingsMap = mapOf(
+ "precision" to 15,
+ "enableCaching" to true,
+ "enableOptimization" to true,
+ "maxHistorySize" to cacheSize
+ )
+
+ return Calculator.createWithSettings(settingsMap)
+ }
+
+ /**
+ * ๋ณด์ ๊ฐํ ๊ณ์ฐ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.
+ *
+ * @return ๋ณด์ ์ค์ ์ด ๊ฐํ๋ ๊ณ์ฐ๊ธฐ
+ */
+ fun createSecureCalculator(): Calculator {
+ createdCalculatorCount.incrementAndGet()
+
+ val settingsMap = mapOf(
+ "precision" to 10,
+ "strictMode" to true,
+ "enableCaching" to false, // ๋ณด์์ ์ํด ์บ์ฑ ๋นํ์ฑํ
+ "enableOptimization" to false, // ์์ธก ๊ฐ๋ฅํ ๋์์ ์ํด ์ต์ ํ ๋นํ์ฑํ
+ "maxHistorySize" to 10
+ )
+
+ return Calculator.createWithSettings(settingsMap)
+ }
+
+
+ /**
+ * ํฉํ ๋ฆฌ์ ํต๊ณ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ํต๊ณ ์ ๋ณด ๋งต
+ */
+ fun getStatistics(): Map = mapOf(
+ "factoryName" to "CalculatorFactory",
+ "createdCalculators" to createdCalculatorCount.get(),
+ "createdSessions" to createdSessionCount.get(),
+ "createdRequests" to createdRequestCount.get(),
+ "supportedCalculatorTypes" to listOf("basic", "scientific", "statistical", "engineering", "custom"),
+ "supportedEnvironments" to listOf("default", "scientific", "engineering", "statistical"),
+ "cacheEnabled" to true,
+ "complexityLevel" to Complexity.HIGH.name
+ )
+
+ /**
+ * ํฉํ ๋ฆฌ์ ์ค์ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
+ *
+ * @return ์ค์ ์ ๋ณด ๋งต
+ */
+ fun getConfiguration(): Map = mapOf(
+ "defaultPrecision" to 10,
+ "defaultAngleUnit" to "RADIANS",
+ "defaultCachingEnabled" to true,
+ "defaultOptimizationEnabled" to true,
+ "maxConcurrency" to 10,
+ "defaultCacheSize" to 1000,
+ "securityMode" to "standard"
+ )
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt
new file mode 100644
index 00000000..09eb8bc9
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt
@@ -0,0 +1,34 @@
+package hs.kr.entrydsm.domain.calculator.interfaces
+
+import hs.kr.entrydsm.domain.calculator.values.CalculationResult
+import hs.kr.entrydsm.domain.calculator.values.CalculationRequest
+
+/**
+ * ์ผ๊ด ์ฒ๋ฆฌ ๋ฐ ๋น๋๊ธฐ ๊ณ์ฐ ๊ธฐ๋ฅ์ ์ ์ํ๋ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * Interface Segregation Principle์ ์ ์ฉํ์ฌ CalculatorContract์์
+ * ์ผ๊ด ์ฒ๋ฆฌ ๋ฐ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ด๋ จ ๋ฉ์๋๋ค๋ง ๋ถ๋ฆฌํ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.08.05
+ */
+interface BatchContract {
+
+ /**
+ * ์ผ๊ด ๊ณ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param requests ๊ณ์ฐ ์์ฒญ๋ค
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ๋ค
+ */
+ fun calculateBatch(requests: List): List
+
+ /**
+ * ๋น๋๊ธฐ ๊ณ์ฐ์ ์ํํฉ๋๋ค.
+ *
+ * @param request ๊ณ์ฐ ์์ฒญ
+ * @param callback ์๋ฃ ์ฝ๋ฐฑ
+ */
+ fun calculateAsync(request: CalculationRequest, callback: (CalculationResult) -> Unit)
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt
new file mode 100644
index 00000000..2186437e
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt
@@ -0,0 +1,53 @@
+package hs.kr.entrydsm.domain.calculator.interfaces
+
+import hs.kr.entrydsm.domain.calculator.entities.CalculationSession
+import hs.kr.entrydsm.domain.calculator.values.CalculationResult
+import hs.kr.entrydsm.domain.calculator.values.CalculationRequest
+
+/**
+ * ๊ธฐ๋ณธ ๊ณ์ฐ ๊ธฐ๋ฅ์ ์ ์ํ๋ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * Interface Segregation Principle์ ์ ์ฉํ์ฌ CalculatorContract์์
+ * ๊ธฐ๋ณธ ๊ณ์ฐ ๊ด๋ จ ๋ฉ์๋๋ค๋ง ๋ถ๋ฆฌํ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * @see ์ฝ๋ ์ฌ๋ก๋ก ๋ณด๋ Domain-Driven ํฅ์ฌ๊ณ ๋ ์ํคํ
์ฒ
+ *
+ * @author kangeunchan
+ * @since 2025.08.05
+ */
+interface CalculationContract {
+
+ /**
+ * ์์์ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param request ๊ณ์ฐ ์์ฒญ
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun calculate(request: CalculationRequest): CalculationResult
+
+ /**
+ * ์์ ๋ฌธ์์ด์ ์ง์ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param expression ์์ ๋ฌธ์์ด
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun calculate(expression: String): CalculationResult
+
+ /**
+ * ๋ณ์์ ํจ๊ป ์์์ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param expression ์์ ๋ฌธ์์ด
+ * @param variables ๋ณ์ ๋งต
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ
+ */
+ fun calculate(expression: String, variables: Map): CalculationResult
+
+ /**
+ * ์ธ์
์ ์ฌ์ฉํ์ฌ ๊ณ์ฐํฉ๋๋ค.
+ *
+ * @param request ๊ณ์ฐ ์์ฒญ
+ * @param session ๊ณ์ฐ ์ธ์
+ * @return ๊ณ์ฐ ๊ฒฐ๊ณผ์ ์
๋ฐ์ดํธ๋ ์ธ์
+ */
+ fun calculateWithSession(request: CalculationRequest, session: CalculationSession): Pair
+}
\ No newline at end of file
diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt
new file mode 100644
index 00000000..3ba8d605
--- /dev/null
+++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt
@@ -0,0 +1,26 @@
+package hs.kr.entrydsm.domain.calculator.interfaces
+
+/**
+ * ๊ณ์ฐ๊ธฐ์ ํต์ฌ ๊ณ์ฝ์ ์ ์ํ๋ ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * Interface Segregation Principle์ ์ ์ฉํ์ฌ ๊ธฐ๋ฅ๋ณ๋ก ๋ถ๋ฆฌ๋ ์ธํฐํ์ด์ค๋ค์
+ * ๋ชจ๋ ์์ํ๋ ํตํฉ ์ธํฐํ์ด์ค์
๋๋ค. Anti-Corruption Layer ์ญํ ์ ์ํํ์ฌ
+ * ๋ค์ํ ๊ณ์ฐ๊ธฐ ๊ตฌํ์ฒด๋ค ๊ฐ์ ํธํ์ฑ์ ๋ณด์ฅํ๋ฉฐ, ๊ณ์ฐ๊ธฐ์ ํต์ฌ ๊ธฐ๋ฅ์
+ * ํ์คํ๋ ๋ฐฉ์์ผ๋ก ์ ๊ณตํฉ๋๋ค.
+ *
+ * ํด๋ผ์ด์ธํธ๋ ํ์์ ๋ฐ๋ผ ๊ฐ๋ณ ์ธํฐํ์ด์ค๋ฅผ ์ง์ ์ฌ์ฉํ๊ฑฐ๋
+ * ๋ชจ๋ ๊ธฐ๋ฅ์ด ํ์ํ ๊ฒฝ์ฐ ์ด ํตํฉ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
+ *
+ * @see