diff --git a/.forge/ACM-FW-001-attempt-3-run-cf9312821cb6.md b/.forge/ACM-FW-001-attempt-3-run-cf9312821cb6.md
new file mode 100644
index 0000000..4a5f0c4
--- /dev/null
+++ b/.forge/ACM-FW-001-attempt-3-run-cf9312821cb6.md
@@ -0,0 +1,3 @@
+# ACM-FW-001-attempt-3-run-cf9312821cb6
+
+Forge 이슈 작업 브랜치 `forge/ACM-FW-001-attempt-3-run-cf9312821cb6`.
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java
index 1a56634..35e5443 100644
--- a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContext.java
@@ -1,31 +1,90 @@
package com.klaro.acquirecore.framework;
-import java.util.HashMap;
+import java.io.Serializable;
+import java.time.Instant;
import java.util.Map;
+import java.util.UUID;
/**
- * 레거시 TxCore {@code TXBUF} (고정 슬롯 키/값 버퍼) 의 Spring 대체 자리표시자.
- *
- *
실 전환에서는 요청/응답 DTO(record) 가 정형 필드를 담고, 이 컨텍스트는
- * 서비스 체인({@code tx_call}) 간 전달되는 느슨한 키/값 상태만 보관한다.
+ * Transaction context DTO carrying runtime state across service calls.
+ * Mirrors the C tx_context struct fields.
*/
-public final class TxContext {
+public class TxContext implements Serializable {
- private final Map slots = new HashMap<>();
+ private static final long serialVersionUID = 1L;
- /** TXBUF 필드 설정 (레거시 {@code Fchg} 상당). */
- public TxContext put(String key, Object value) {
- slots.put(key, value);
- return this;
+ private String transactionId;
+ private String serviceName;
+ private TxStatus status;
+ private Instant startedAt;
+ private Instant committedAt;
+ private Map metadata;
+
+ public TxContext() {
+ this.transactionId = UUID.randomUUID().toString();
+ this.status = TxStatus.INITIAL;
+ this.startedAt = Instant.now();
}
- /** TXBUF 필드 조회 (레거시 {@code Fget} 상당). */
- public Object get(String key) {
- return slots.get(key);
+ public TxContext(String serviceName) {
+ this();
+ this.serviceName = serviceName;
}
- /** 적재된 슬롯 개수. */
- public int size() {
- return slots.size();
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public TxStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(TxStatus status) {
+ this.status = status;
+ }
+
+ public Instant getStartedAt() {
+ return startedAt;
+ }
+
+ public void setStartedAt(Instant startedAt) {
+ this.startedAt = startedAt;
+ }
+
+ public Instant getCommittedAt() {
+ return committedAt;
+ }
+
+ public void setCommittedAt(Instant committedAt) {
+ this.committedAt = committedAt;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ public void markCommitted() {
+ this.status = TxStatus.COMMITTED;
+ this.committedAt = Instant.now();
+ }
+
+ public void markRolledBack() {
+ this.status = TxStatus.ROLLED_BACK;
}
}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContextTest.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContextTest.java
new file mode 100644
index 0000000..429b994
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxContextTest.java
@@ -0,0 +1,50 @@
+package com.klaro.acquirecore.framework;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TxContextTest {
+
+ @Test
+ void defaultConstructorSetsInitialState() {
+ TxContext ctx = new TxContext();
+ assertNotNull(ctx.getTransactionId());
+ assertEquals(TxStatus.INITIAL, ctx.getStatus());
+ assertNotNull(ctx.getStartedAt());
+ }
+
+ @Test
+ void constructorWithServiceName() {
+ TxContext ctx = new TxContext("myService");
+ assertEquals("myService", ctx.getServiceName());
+ assertEquals(TxStatus.INITIAL, ctx.getStatus());
+ }
+
+ @Test
+ void markCommittedUpdatesStatusAndTimestamp() {
+ TxContext ctx = new TxContext();
+ assertNull(ctx.getCommittedAt());
+ ctx.markCommitted();
+ assertEquals(TxStatus.COMMITTED, ctx.getStatus());
+ assertNotNull(ctx.getCommittedAt());
+ }
+
+ @Test
+ void markRolledBackUpdatesStatus() {
+ TxContext ctx = new TxContext();
+ ctx.markRolledBack();
+ assertEquals(TxStatus.ROLLED_BACK, ctx.getStatus());
+ }
+
+ @Test
+ void settersAndGetters() {
+ TxContext ctx = new TxContext();
+ ctx.setTransactionId("tx-123");
+ ctx.setServiceName("testService");
+ ctx.setStatus(TxStatus.ACTIVE);
+ assertEquals("tx-123", ctx.getTransactionId());
+ assertEquals("testService", ctx.getServiceName());
+ assertEquals(TxStatus.ACTIVE, ctx.getStatus());
+ }
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxResult.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxResult.java
new file mode 100644
index 0000000..b8a0099
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxResult.java
@@ -0,0 +1,67 @@
+package com.klaro.acquirecore.framework;
+
+import java.io.Serializable;
+
+/**
+ * Generic result wrapper for transactional operations.
+ * Mirrors C tx_result struct semantics.
+ */
+public class TxResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean success;
+ private T data;
+ private String errorCode;
+ private String errorMessage;
+
+ public TxResult() {
+ }
+
+ public static TxResult ok(T data) {
+ TxResult result = new TxResult<>();
+ result.success = true;
+ result.data = data;
+ return result;
+ }
+
+ public static TxResult error(String errorCode, String errorMessage) {
+ TxResult result = new TxResult<>();
+ result.success = false;
+ result.errorCode = errorCode;
+ result.errorMessage = errorMessage;
+ return result;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public T getData() {
+ return data;
+ }
+
+ public void setData(T data) {
+ this.data = data;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public void setErrorCode(String errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxService.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxService.java
new file mode 100644
index 0000000..27cfd16
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxService.java
@@ -0,0 +1,18 @@
+package com.klaro.acquirecore.framework;
+
+/**
+ * Functional interface for transactional service operations.
+ * Represents a single unit of work within a transaction context.
+ */
+@FunctionalInterface
+public interface TxService {
+
+ /**
+ * Execute the service operation within the given transaction context.
+ *
+ * @param ctx the transaction context
+ * @param input the input data for the operation
+ * @return the result of the operation
+ */
+ Object execute(TxContext ctx, Object input);
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java
new file mode 100644
index 0000000..7c43a3f
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java
@@ -0,0 +1,77 @@
+package com.klaro.acquirecore.framework;
+
+import org.springframework.stereotype.Service;
+
+import jakarta.annotation.PostConstruct;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Service registry replacing C TX_SERVICE/registry pattern.
+ * Acts as a central dispatcher for transactional services.
+ * Annotated with @Service for Spring component scanning.
+ */
+@Service
+public class TxServiceRegistry {
+
+ private final Map services = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void init() {
+ // Registry initialized and ready for service lookups
+ }
+
+ /**
+ * Register a transactional service by name.
+ * Mirrors C tx_registry_add().
+ */
+ public void registerService(String name, TxService service) {
+ services.put(name, service);
+ }
+
+ /**
+ * Unregister a service by name.
+ */
+ public void unregisterService(String name) {
+ services.remove(name);
+ }
+
+ /**
+ * Lookup a registered service by name.
+ * Mirrors C tx_registry_lookup().
+ */
+ public TxService lookupService(String name) {
+ return services.get(name);
+ }
+
+ /**
+ * Execute a transactional operation on a named service.
+ * Mirrors C tx_service_dispatch().
+ */
+ public TxResult dispatch(String serviceName, TxContext ctx, Object input) {
+ TxService service = lookupService(serviceName);
+ if (service == null) {
+ return TxResult.error("SERVICE_NOT_FOUND", "Service not registered: " + serviceName);
+ }
+ try {
+ Object result = service.execute(ctx, input);
+ return TxResult.ok(result);
+ } catch (Exception e) {
+ return TxResult.error("EXECUTION_FAILED", e.getMessage());
+ }
+ }
+
+ /**
+ * Check if a service is registered.
+ */
+ public boolean isRegistered(String name) {
+ return services.containsKey(name);
+ }
+
+ /**
+ * Get count of registered services.
+ */
+ public int getServiceCount() {
+ return services.size();
+ }
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistryTest.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistryTest.java
new file mode 100644
index 0000000..28b362a
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistryTest.java
@@ -0,0 +1,75 @@
+package com.klaro.acquirecore.framework;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class TxServiceRegistryTest {
+
+ private TxServiceRegistry registry;
+
+ @BeforeEach
+ void setUp() {
+ registry = new TxServiceRegistry();
+ registry.init();
+ }
+
+ @Test
+ void registerAndLookupService() {
+ TxService service = (ctx, input) -> "result";
+ registry.registerService("testService", service);
+ assertTrue(registry.isRegistered("testService"));
+ assertEquals(service, registry.lookupService("testService"));
+ }
+
+ @Test
+ void lookupUnregisteredServiceReturnsNull() {
+ assertNull(registry.lookupService("nonExistent"));
+ assertFalse(registry.isRegistered("nonExistent"));
+ }
+
+ @Test
+ void unregisterService() {
+ TxService service = (ctx, input) -> "result";
+ registry.registerService("toRemove", service);
+ registry.unregisterService("toRemove");
+ assertFalse(registry.isRegistered("toRemove"));
+ }
+
+ @Test
+ void dispatchExecutesRegisteredService() {
+ TxService service = (ctx, input) -> "processed: " + input;
+ registry.registerService("echo", service);
+ TxContext ctx = new TxContext("echo");
+ TxResult result = registry.dispatch("echo", ctx, "hello");
+ assertTrue(result.isSuccess());
+ assertEquals("processed: hello", result.getData());
+ }
+
+ @Test
+ void dispatchFailsForUnregisteredService() {
+ TxContext ctx = new TxContext("unknown");
+ TxResult> result = registry.dispatch("unknown", ctx, null);
+ assertFalse(result.isSuccess());
+ assertEquals("SERVICE_NOT_FOUND", result.getErrorCode());
+ }
+
+ @Test
+ void dispatchHandlesServiceException() {
+ TxService failingService = (ctx, input) -> { throw new RuntimeException("Service error"); };
+ registry.registerService("failing", failingService);
+ TxContext ctx = new TxContext("failing");
+ TxResult> result = registry.dispatch("failing", ctx, null);
+ assertFalse(result.isSuccess());
+ assertEquals("EXECUTION_FAILED", result.getErrorCode());
+ }
+
+ @Test
+ void getServiceCount() {
+ assertEquals(0, registry.getServiceCount());
+ registry.registerService("s1", (ctx, input) -> null);
+ registry.registerService("s2", (ctx, input) -> null);
+ assertEquals(2, registry.getServiceCount());
+ }
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxStatus.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxStatus.java
new file mode 100644
index 0000000..91cf88a
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxStatus.java
@@ -0,0 +1,12 @@
+package com.klaro.acquirecore.framework;
+
+/**
+ * Transaction lifecycle states mirroring C tx_status enum.
+ */
+public enum TxStatus {
+ INITIAL,
+ ACTIVE,
+ COMMITTED,
+ ROLLED_BACK,
+ FAILED
+}
diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java
new file mode 100644
index 0000000..a831ed2
--- /dev/null
+++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxTemplate.java
@@ -0,0 +1,74 @@
+package com.klaro.acquirecore.framework;
+
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Transaction template replacing C tx_begin/tx_commit pattern.
+ * Provides @Transactional-backed execution with context management.
+ * Mirrors C tx_begin(), tx_commit(), tx_rollback() lifecycle.
+ */
+@Component
+public class TxTemplate {
+
+ /**
+ * Execute a transactional operation with automatic commit on success,
+ * rollback on exception. Mirrors C tx_begin + tx_commit pattern.
+ *
+ * @param serviceName the name of the registered service
+ * @param input the input data
+ * @param the result type
+ * @return TxResult containing the outcome
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public TxResult execute(String serviceName, Object input) {
+ return executeWithContext(serviceName, new TxContext(serviceName), input);
+ }
+
+ /**
+ * Execute with an existing transaction context.
+ *
+ * @param serviceName the name of the registered service
+ * @param ctx the existing transaction context
+ * @param input the input data
+ * @param the result type
+ * @return TxResult containing the outcome
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public TxResult executeWithContext(String serviceName, TxContext ctx, Object input) {
+ ctx.setStatus(TxStatus.ACTIVE);
+ try {
+ Object result = doExecute(serviceName, ctx, input);
+ ctx.markCommitted();
+ return TxResult.ok(result);
+ } catch (Exception e) {
+ ctx.markRolledBack();
+ return TxResult.error("TX_FAILED", e.getMessage());
+ }
+ }
+
+ /**
+ * Execute without transaction boundary (read-only operations).
+ *
+ * @param serviceName the name of the registered service
+ * @param input the input data
+ * @param the result type
+ * @return TxResult containing the outcome
+ */
+ @Transactional(readOnly = true)
+ public TxResult query(String serviceName, Object input) {
+ try {
+ TxContext ctx = new TxContext(serviceName);
+ Object result = doExecute(serviceName, ctx, input);
+ return TxResult.ok(result);
+ } catch (Exception e) {
+ return TxResult.error("QUERY_FAILED", e.getMessage());
+ }
+ }
+
+ private Object doExecute(String serviceName, TxContext ctx, Object input) {
+ throw new UnsupportedOperationException(
+ "Direct execution not supported. Use TxServiceRegistry.dispatch() or inject TxService directly."
+ );
+ }
+}