diff --git a/.forge/ACM-FW-001-attempt-2-run-764de03d4a1a.md b/.forge/ACM-FW-001-attempt-2-run-764de03d4a1a.md new file mode 100644 index 0000000..974d684 --- /dev/null +++ b/.forge/ACM-FW-001-attempt-2-run-764de03d4a1a.md @@ -0,0 +1,3 @@ +# ACM-FW-001-attempt-2-run-764de03d4a1a + +Forge 이슈 작업 브랜치 `forge/ACM-FW-001-attempt-2-run-764de03d4a1a`. diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/DefaultTxService.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/DefaultTxService.java new file mode 100644 index 0000000..dc907ee --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/DefaultTxService.java @@ -0,0 +1,48 @@ +package com.klaro.acquirecore.framework; + +import org.springframework.stereotype.Service; + +/** + * Default implementation of TxService. + * Provides base transaction service functionality. + */ +@Service +public class DefaultTxService implements TxService { + + private final String serviceName; + private final TxTemplate txTemplate; + + public DefaultTxService() { + this("default"); + } + + public DefaultTxService(String serviceName) { + this.serviceName = serviceName; + this.txTemplate = new TxTemplate(); + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public TxResult execute(TxServiceRegistry.TxOperation operation) { + return txTemplate.execute(serviceName, operation); + } + + @Override + public TxContext beginTransaction() { + return txTemplate.txBegin(serviceName); + } + + @Override + public TxResult commit(TxContext context) { + return txTemplate.txCommit(context); + } + + @Override + public TxResult rollback(TxContext context, String reason) { + return txTemplate.txRollback(context, reason); + } +} 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..ba2748b 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,49 @@ package com.klaro.acquirecore.framework; -import java.util.HashMap; -import java.util.Map; +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; /** - * 레거시 TxCore {@code TXBUF} (고정 슬롯 키/값 버퍼) 의 Spring 대체 자리표시자. - * - *

실 전환에서는 요청/응답 DTO(record) 가 정형 필드를 담고, 이 컨텍스트는 - * 서비스 체인({@code tx_call}) 간 전달되는 느슨한 키/값 상태만 보관한다. + * Transaction context DTO - represents the state of a transaction. + * Migrated from txcore.h TX_CONTEXT. */ -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 TransactionStatus status; + private Instant startedAt; + private Instant committedAt; + private String serviceName; + private String errorMessage; + + public TxContext() { + this.transactionId = UUID.randomUUID().toString(); + this.status = TransactionStatus.INITIALIZED; + 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 TransactionStatus getStatus() { return status; } + public void setStatus(TransactionStatus 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 String getServiceName() { return serviceName; } + public void setServiceName(String serviceName) { this.serviceName = serviceName; } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public enum TransactionStatus { + INITIALIZED, ACTIVE, COMMITTED, ROLLED_BACK, FAILED } } diff --git a/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxExecutionException.java b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxExecutionException.java new file mode 100644 index 0000000..f785873 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxExecutionException.java @@ -0,0 +1,35 @@ +package com.klaro.acquirecore.framework; + +/** + * Exception thrown when transaction execution fails. + * Migrated from txcore.c error handling patterns. + */ +public class TxExecutionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private final TxContext context; + + public TxExecutionException(String message) { + super(message); + this.context = null; + } + + public TxExecutionException(String message, Throwable cause) { + super(message, cause); + this.context = null; + } + + public TxExecutionException(String message, Throwable cause, TxContext context) { + super(message, cause); + this.context = context; + } + + public TxExecutionException(String message, TxContext context) { + super(message); + this.context = context; + } + + public TxContext getContext() { + return context; + } +} 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..ef14935 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxResult.java @@ -0,0 +1,55 @@ +package com.klaro.acquirecore.framework; + +import java.io.Serializable; + +/** + * Transaction result DTO - wraps operation outcomes. + * Migrated from txcore.h TX_RESULT. + */ +public class TxResult implements Serializable { + + private static final long serialVersionUID = 1L; + + private boolean success; + private T data; + private String errorCode; + private String errorMessage; + private TxContext context; + + public TxResult() {} + + private TxResult(boolean success, T data, String errorCode, String errorMessage, TxContext context) { + this.success = success; + this.data = data; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.context = context; + } + + public static TxResult success(T data, TxContext context) { + return new TxResult<>(true, data, null, null, context); + } + + public static TxResult success(T data) { + return success(data, null); + } + + public static TxResult failure(String errorCode, String errorMessage) { + return new TxResult<>(false, null, errorCode, errorMessage, null); + } + + public static TxResult failure(String errorCode, String errorMessage, TxContext context) { + return new TxResult<>(false, null, errorCode, errorMessage, context); + } + + 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; } + public TxContext getContext() { return context; } + public void setContext(TxContext context) { this.context = context; } +} 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..8d1ae05 --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxService.java @@ -0,0 +1,18 @@ +package com.klaro.acquirecore.framework; + +/** + * Interface for transaction-aware services. + * Migrated from txcore.h TX_SERVICE callback structure. + */ +public interface TxService { + + String getServiceName(); + + TxResult execute(TxServiceRegistry.TxOperation operation); + + TxContext beginTransaction(); + + TxResult commit(TxContext context); + + TxResult rollback(TxContext context, String reason); +} 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..7708f4a --- /dev/null +++ b/boot/common-framework/src/main/java/com/klaro/acquirecore/framework/TxServiceRegistry.java @@ -0,0 +1,57 @@ +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 for transaction-aware services. + * Migrated from txcore.c TX_SERVICE registry functionality. + * Replaces C function pointers and registry with Spring @Service DI. + */ +@Service +public class TxServiceRegistry { + + private final Map services = new ConcurrentHashMap<>(); + + @PostConstruct + public void initialize() {} + + public void registerService(String name, TxService service) { + services.put(name, service); + } + + public void unregisterService(String name) { + services.remove(name); + } + + public TxService getService(String name) { + return services.get(name); + } + + public boolean hasService(String name) { + return services.containsKey(name); + } + + public TxResult executeInTransaction(String serviceName, TxOperation operation) { + TxService service = services.get(serviceName); + if (service == null) { + return TxResult.failure("SERVICE_NOT_FOUND", "Service not registered: " + serviceName); + } + return service.execute(operation); + } + + public String[] getRegisteredServiceNames() { + return services.keySet().toArray(new String[0]); + } + + public void clear() { + services.clear(); + } + + @FunctionalInterface + public interface TxOperation { + T execute(TxContext context); + } +} 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..e57dda7 --- /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; +import java.time.Instant; + +/** + * Template for executing transactional operations. + * Migrated from txcore.c tx_begin/tx_commit functions. + * Provides @Transactional equivalent functionality. + */ +@Component +public class TxTemplate { + + @Transactional + public TxResult execute(TxServiceRegistry.TxOperation operation) { + TxContext context = new TxContext(); + context.setStatus(TxContext.TransactionStatus.ACTIVE); + try { + T result = operation.execute(context); + context.setStatus(TxContext.TransactionStatus.COMMITTED); + context.setCommittedAt(Instant.now()); + return TxResult.success(result, context); + } catch (Exception e) { + context.setStatus(TxContext.TransactionStatus.ROLLED_BACK); + context.setErrorMessage(e.getMessage()); + throw new TxExecutionException("Transaction failed", e, context); + } + } + + @Transactional + public TxResult execute(String serviceName, TxServiceRegistry.TxOperation operation) { + TxContext context = new TxContext(serviceName); + context.setStatus(TxContext.TransactionStatus.ACTIVE); + try { + T result = operation.execute(context); + context.setStatus(TxContext.TransactionStatus.COMMITTED); + context.setCommittedAt(Instant.now()); + return TxResult.success(result, context); + } catch (Exception e) { + context.setStatus(TxContext.TransactionStatus.ROLLED_BACK); + context.setErrorMessage(e.getMessage()); + throw new TxExecutionException("Transaction failed for service: " + serviceName, e, context); + } + } + + public TxContext txBegin() { + return new TxContext(); + } + + public TxContext txBegin(String serviceName) { + return new TxContext(serviceName); + } + + @Transactional + public TxResult txCommit(TxContext context) { + if (context == null) { + return TxResult.failure("INVALID_CONTEXT", "Transaction context is null"); + } + context.setStatus(TxContext.TransactionStatus.COMMITTED); + context.setCommittedAt(Instant.now()); + return TxResult.success(null, context); + } + + @Transactional + public TxResult txRollback(TxContext context, String reason) { + if (context == null) { + return TxResult.failure("INVALID_CONTEXT", "Transaction context is null"); + } + context.setStatus(TxContext.TransactionStatus.ROLLED_BACK); + context.setErrorMessage(reason); + return TxResult.success(null, context); + } +} diff --git a/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/FrameworkTest.java b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/FrameworkTest.java new file mode 100644 index 0000000..1f3fa82 --- /dev/null +++ b/boot/common-framework/src/test/java/com/klaro/acquirecore/framework/FrameworkTest.java @@ -0,0 +1,155 @@ +package com.klaro.acquirecore.framework; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Consolidated unit tests for TxCore migration framework. + */ +class FrameworkTest { + + // TxContext tests + @Test + void testTxContextDefaultConstructor() { + TxContext ctx = new TxContext(); + assertNotNull(ctx.getTransactionId()); + assertEquals(TxContext.TransactionStatus.INITIALIZED, ctx.getStatus()); + } + + @Test + void testTxContextWithServiceName() { + TxContext ctx = new TxContext("testService"); + assertEquals("testService", ctx.getServiceName()); + } + + // TxResult tests + @Test + void testTxResultSuccess() { + TxResult result = TxResult.success("data"); + assertTrue(result.isSuccess()); + assertEquals("data", result.getData()); + } + + @Test + void testTxResultFailure() { + TxResult result = TxResult.failure("ERR", "message"); + assertFalse(result.isSuccess()); + assertEquals("ERR", result.getErrorCode()); + } + + // TxServiceRegistry tests + @Test + void testServiceRegistryRegisterAndGet() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxService service = new DefaultTxService("svc"); + registry.registerService("svc", service); + assertTrue(registry.hasService("svc")); + assertEquals(service, registry.getService("svc")); + } + + @Test + void testServiceRegistryUnregister() { + TxServiceRegistry registry = new TxServiceRegistry(); + registry.registerService("svc", new DefaultTxService("svc")); + registry.unregisterService("svc"); + assertFalse(registry.hasService("svc")); + } + + @Test + void testServiceRegistryExecuteInTransaction() { + TxServiceRegistry registry = new TxServiceRegistry(); + registry.registerService("svc", new DefaultTxService("svc")); + TxResult result = registry.executeInTransaction("svc", ctx -> "ok"); + assertTrue(result.isSuccess()); + assertEquals("ok", result.getData()); + } + + @Test + void testServiceRegistryUnknownService() { + TxServiceRegistry registry = new TxServiceRegistry(); + TxResult result = registry.executeInTransaction("unknown", ctx -> "ok"); + assertFalse(result.isSuccess()); + assertEquals("SERVICE_NOT_FOUND", result.getErrorCode()); + } + + // TxTemplate tests + @Test + void testTxTemplateTxBegin() { + TxTemplate template = new TxTemplate(); + TxContext ctx = template.txBegin("svc"); + assertEquals("svc", ctx.getServiceName()); + } + + @Test + void testTxTemplateTxCommit() { + TxTemplate template = new TxTemplate(); + TxContext ctx = template.txBegin(); + TxResult result = template.txCommit(ctx); + assertTrue(result.isSuccess()); + assertEquals(TxContext.TransactionStatus.COMMITTED, ctx.getStatus()); + } + + @Test + void testTxTemplateTxCommitNullContext() { + TxTemplate template = new TxTemplate(); + TxResult result = template.txCommit(null); + assertFalse(result.isSuccess()); + assertEquals("INVALID_CONTEXT", result.getErrorCode()); + } + + @Test + void testTxTemplateTxRollback() { + TxTemplate template = new TxTemplate(); + TxContext ctx = template.txBegin(); + TxResult result = template.txRollback(ctx, "reason"); + assertTrue(result.isSuccess()); + assertEquals(TxContext.TransactionStatus.ROLLED_BACK, ctx.getStatus()); + assertEquals("reason", ctx.getErrorMessage()); + } + + // DefaultTxService tests + @Test + void testDefaultTxServiceName() { + DefaultTxService svc = new DefaultTxService("myService"); + assertEquals("myService", svc.getServiceName()); + } + + @Test + void testDefaultTxServiceBeginTransaction() { + DefaultTxService svc = new DefaultTxService("svc"); + TxContext ctx = svc.beginTransaction(); + assertEquals("svc", ctx.getServiceName()); + } + + @Test + void testDefaultTxServiceCommit() { + DefaultTxService svc = new DefaultTxService("svc"); + TxContext ctx = svc.beginTransaction(); + TxResult result = svc.commit(ctx); + assertTrue(result.isSuccess()); + } + + @Test + void testDefaultTxServiceRollback() { + DefaultTxService svc = new DefaultTxService("svc"); + TxContext ctx = svc.beginTransaction(); + TxResult result = svc.rollback(ctx, "test"); + assertTrue(result.isSuccess()); + } + + // TxExecutionException tests + @Test + void testTxExecutionExceptionMessage() { + TxExecutionException ex = new TxExecutionException("msg"); + assertEquals("msg", ex.getMessage()); + assertNull(ex.getContext()); + } + + @Test + void testTxExecutionExceptionWithContext() { + TxContext ctx = new TxContext("svc"); + TxExecutionException ex = new TxExecutionException("msg", ctx); + assertEquals("msg", ex.getMessage()); + assertEquals(ctx, ex.getContext()); + } +}