[framework] TxCore → common-framework #4
9 changed files with 481 additions and 18 deletions
3
.forge/ACM-FW-001-attempt-2-run-764de03d4a1a.md
Normal file
3
.forge/ACM-FW-001-attempt-2-run-764de03d4a1a.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ACM-FW-001-attempt-2-run-764de03d4a1a
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/ACM-FW-001-attempt-2-run-764de03d4a1a`.
|
||||
|
|
@ -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 <T> TxResult<T> execute(TxServiceRegistry.TxOperation<T> operation) {
|
||||
return txTemplate.execute(serviceName, operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxContext beginTransaction() {
|
||||
return txTemplate.txBegin(serviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxResult<Void> commit(TxContext context) {
|
||||
return txTemplate.txCommit(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxResult<Void> rollback(TxContext context, String reason) {
|
||||
return txTemplate.txRollback(context, reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 대체 자리표시자.
|
||||
*
|
||||
* <p>실 전환에서는 요청/응답 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<String, Object> 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T> 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 <T> TxResult<T> success(T data, TxContext context) {
|
||||
return new TxResult<>(true, data, null, null, context);
|
||||
}
|
||||
|
||||
public static <T> TxResult<T> success(T data) {
|
||||
return success(data, null);
|
||||
}
|
||||
|
||||
public static <T> TxResult<T> failure(String errorCode, String errorMessage) {
|
||||
return new TxResult<>(false, null, errorCode, errorMessage, null);
|
||||
}
|
||||
|
||||
public static <T> TxResult<T> 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; }
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
<T> TxResult<T> execute(TxServiceRegistry.TxOperation<T> operation);
|
||||
|
||||
TxContext beginTransaction();
|
||||
|
||||
TxResult<Void> commit(TxContext context);
|
||||
|
||||
TxResult<Void> rollback(TxContext context, String reason);
|
||||
}
|
||||
|
|
@ -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<String, TxService> 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 <T> TxResult<T> executeInTransaction(String serviceName, TxOperation<T> 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> {
|
||||
T execute(TxContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <T> TxResult<T> execute(TxServiceRegistry.TxOperation<T> 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 <T> TxResult<T> execute(String serviceName, TxServiceRegistry.TxOperation<T> 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<Void> 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<Void> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> result = TxResult.success("data");
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals("data", result.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTxResultFailure() {
|
||||
TxResult<String> 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<String> result = registry.executeInTransaction("svc", ctx -> "ok");
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals("ok", result.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testServiceRegistryUnknownService() {
|
||||
TxServiceRegistry registry = new TxServiceRegistry();
|
||||
TxResult<String> 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<Void> result = template.txCommit(ctx);
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals(TxContext.TransactionStatus.COMMITTED, ctx.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTxTemplateTxCommitNullContext() {
|
||||
TxTemplate template = new TxTemplate();
|
||||
TxResult<Void> result = template.txCommit(null);
|
||||
assertFalse(result.isSuccess());
|
||||
assertEquals("INVALID_CONTEXT", result.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTxTemplateTxRollback() {
|
||||
TxTemplate template = new TxTemplate();
|
||||
TxContext ctx = template.txBegin();
|
||||
TxResult<Void> 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<Void> result = svc.commit(ctx);
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultTxServiceRollback() {
|
||||
DefaultTxService svc = new DefaultTxService("svc");
|
||||
TxContext ctx = svc.beginTransaction();
|
||||
TxResult<Void> 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());
|
||||
}
|
||||
}
|
||||
Reference in a new issue