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." + ); + } +}