๐Ÿš€ Spring Boot @Async ํ™œ์šฉ

yeolyeolยท2025๋…„ 4์›” 5์ผ
0

til

๋ชฉ๋ก ๋ณด๊ธฐ
29/30
post-thumbnail

์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์„œ์•ฝ์„œ๋ฅผ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋กœ ๋ฐฐ๋กœ ํ•˜๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ, ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋งŒ๋“ค์–ด์ง„ ์„œ์•ฝ์„œ๋ฅผ DB์— ๋„ฃ์€ ๋’ค ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ๋กœ ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“ค์–ด ๋“ฑ๋กํ•˜๋Š” ๊ณผ์ •์—์„œ 15์ดˆ๋‚˜ ๊ฑธ๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

'์•„, ์ด ์ž‘์—… ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š”๋ฐ... ์‚ฌ์šฉ์ž ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•  ์ˆœ ์—†์ง€!๐Ÿค”' ๋ผ๋Š” ์ƒ๊ฐ์œผ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ™œํ•˜๊ฒŒ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋น„๋™๊ธฐ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ Spring Boot์˜ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ @Async๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ–ˆ๋˜ ๊ฒฝํ—˜๊ณผ, ๊ทธ ๊ณผ์ •์—์„œ ํ™œ์šฉํ–ˆ๋˜ CompletableFuture, Mockito ํ…Œ์ŠคํŠธ ๊ฒฝํ—˜๐Ÿ˜…, ๊ทธ๋ฆฌ๊ณ  ๊ฐ€์žฅ ์ค‘์š”ํ–ˆ๋˜ @Transactional๊ณผ์˜ ๊ด€๊ณ„๐Ÿšจ๊นŒ์ง€ ์ƒ์„ธํžˆ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.


1๋‹จ๊ณ„: @ASYNC ๊ธฐ๋ณธ ์ ์šฉ

๊ฐ€์žฅ ๋จผ์ €, Spring Boot์— "๋น„๋™๊ธฐ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค"๊ณ  ์•Œ๋ ค์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์ธ ํด๋ž˜์Šค๋‚˜ @Configuration ํด๋ž˜์Šค์— @EnableAsync๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ค€๋น„๊ฐ€ ๋๋‚ฉ๋‹ˆ๋‹ค.

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync // ์ถ”๊ฐ€!
public class MyAwesomeApplication {
    // ... main ...
}

๊ทธ๋ฆฌ๊ณ  ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ๋ฉ”์„œ๋“œ์— @Async๋งŒ ๋ถ™์—ฌ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. (๋‹จ, public ๋ฉ”์„œ๋“œ์—ฌ์•ผ ํ•˜๊ณ  Spring Bean ์•ˆ์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.)

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MySlowService {

    @Async // ์ด๋ ‡๊ฒŒ ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
    public void doSomethingSlow() {
        System.out.println("๋А๋ฆฐ ์ž‘์—… ์‹œ์ž‘... (๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ)");
        // ... ์‹œ๊ฐ„ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—… ...
        System.out.println("๋А๋ฆฐ ์ž‘์—… ๋!");
    }
}

2๋‹จ๊ณ„: "๊ทธ๋ž˜์„œ... ์„ฑ๊ณตํ–ˆ๋Š”๊ฐ€?" ๐Ÿค”

๋‹จ์ˆœํžˆ '์‹คํ–‰ํ•˜๊ณ  ์žŠ์–ด๋ฒ„๋ฆฌ๋Š”(Fire-and-Forget)' ์ž‘์—…์ด๋ผ๋ฉด ์œ„์ฒ˜๋Ÿผ void๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ์ œ ๊ฒฝ์šฐ์—๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋“ฑ๋ก์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ์‹คํŒจํ–ˆ๋Š”์ง€ ์•Œ์•„์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿด ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ CompletableFuture!

๋น„๋™๊ธฐ ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜์ค‘์— ๋ฐ›์•„๋ณด๊ฑฐ๋‚˜, ์„ฑ๊ณต/์‹คํŒจ์— ๋”ฐ๋ฅธ ํ›„์† ์ฒ˜๋ฆฌ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

์ €ํฌ๊ฐ€ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ์˜ ํŠธ๋žœ์žญ์…˜์„ ๋“ฑ๋กํ•˜๋Š” addContract๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ‘‡

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;

@Component
public class ContractHandler {
    // ... (์ƒ์„ฑ์ž ๋“ฑ) ...

    @Async // ๋น„๋™๊ธฐ ์‹คํ–‰!
    public CompletableFuture<Boolean> addContract(ContractInput input) { // ๋ฐ˜ํ™˜ ํƒ€์ž… ๋ณ€๊ฒฝ!
        try {
            // ... (๋ธ”๋ก์ฒด์ธ๊ณผ ํ†ต์‹ ํ•˜๋Š” ๋กœ์ง) ...
            boolean isSuccess = blockchainTransaction.isStatusOK(); // ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
            log.info("โœ… ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋“ฑ๋ก ์„ฑ๊ณต!");
            return CompletableFuture.completedFuture(isSuccess); // ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์€ ์™„๋ฃŒ๋œ Future ๋ฐ˜ํ™˜
        } catch (Exception e) {
            log.error("โ— ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋“ฑ๋ก ์‹คํŒจ!: {}", e);
            return CompletableFuture.completedFuture(false); // ์‹คํŒจ ์‹œ false ๋ฐ˜ํ™˜
        }
    }
}

ํ˜ธ์ถœํ•˜๋Š” ์ชฝ์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ๊ฒฐ๊ณผ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ‘

CompletableFuture<Boolean> future = contractHandler.addContract(input);

future.thenAccept(success -> { // ์„ฑ๊ณตํ–ˆ์„ ๋•Œ (๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋จ)
    if (success) {
        log.info("โœจ ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ๋“ฑ๋ก ์„ฑ๊ณต ์•Œ๋ฆผ ๋ณด๋‚ด์•ผ์ง€!");
    } else {
        log.error("๐Ÿ’ง ์‹คํŒจ ์•Œ๋ฆผ ๋ณด๋‚ด์•ผ๊ฒ ๋‹ค...");
    }
}).exceptionally(ex -> { // ์˜ˆ์™ธ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ (๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋จ)
    log.error("๐Ÿ”ฅ ์˜ˆ์™ธ ๋ฐœ์ƒ!", ex);
    return null;
});

log.info("๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋‹ค์Œ ์ผ์„ ํ•˜๋Ÿฌ ๊ฐ‘๋‹ˆ๋‹ค.");

3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

์ž, ์ด์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค! @Async ๋ฉ”์„œ๋“œ๋ฅผ Mockito๋กœ Mockingํ•˜๋Š”๋ฐ... ์—ฌ๊ธฐ์„œ ์ œ๊ฐ€ ์ž ์‹œ ์–ด๋ ค์›€์„ ๊ฒช์—ˆ๋˜ ๋ถ€๋ถ„์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ˜…

๐Ÿšจ ์ž˜๋ชป๋œ ์‹œ๋„ (๋ฌดํ•œ ๋Œ€๊ธฐ) ๐Ÿšจ

// given
when(mockContractHandler.addContract(any()))
    .thenReturn(new CompletableFuture<>()); // ํ…… ๋นˆ Future ๋ฐ˜ํ™˜...

// when
CompletableFuture<Boolean> future = service.callAddContract();

// then
assertTrue(future.join()); // ์—ฌ๊ธฐ์„œ ์˜์›ํžˆ ๋Œ€๊ธฐ... ๐Ÿ˜ต

new CompletableFuture<>()๋Š” ์™„๋ฃŒ๋˜์ง€ ์•Š์€ Future๋ฅผ ๋งŒ๋“ค์–ด์„œ join()์ด ๊ณ„์† ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

โœจ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• โœจ

ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋งž๊ฒŒ ์ด๋ฏธ ์™„๋ฃŒ๋œ Future๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

โœ…์„ฑ๊ณต ์‹œ๋‚˜๋ฆฌ์˜ค

when(mockContractHandler.addContract(any()))
    .thenReturn(CompletableFuture.completedFuture(true)); // ์„ฑ๊ณต(true)์œผ๋กœ ์™„๋ฃŒ๋œ Future!

โŒ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค

when(mockContractHandler.addContract(any()))
    .thenReturn(CompletableFuture.completedFuture(false)); // ์‹คํŒจ(false)๋กœ ์™„๋ฃŒ๋œ Future!

์‚ฌ์šฉ๋œ TEST ์ฝ”๋“œ

package com.ssafy.chaing.blockchain.handler;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.ssafy.chaing.blockchain.config.Web3jConnectionManager;
import com.ssafy.chaing.blockchain.handler.contract.ContractHandler;
import com.ssafy.chaing.blockchain.handler.contract.input.ContractInput;
import com.ssafy.chaing.blockchain.handler.contract.input.LiveAccountInput;
import com.ssafy.chaing.blockchain.handler.contract.input.PaymentInfoInput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractOutput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractOverviewOutput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractRentOutput;
import com.ssafy.chaing.blockchain.web3j.ContractManager;
import com.ssafy.chaing.blockchain.web3j.ContractManager.PaymentInfo;
import java.math.BigInteger;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tuples.generated.Tuple13;
import org.web3j.tuples.generated.Tuple3;
import org.web3j.tuples.generated.Tuple6;
// TransactionManager๋Š” ์ด์ œ ์ง์ ‘ ์ฃผ์ž…๋ฐ›์ง€ ์•Š์Œ

@ExtendWith(MockitoExtension.class) // JUnit5 ์™€ Mockito ์—ฐ๋™
class ContractHandlerTest {

    private final String TEST_CONTRACT_ADDRESS = "0x๋กœ ์‹œ์ž‘ํ•˜๋Š” ์Šค๋งˆํŠธ ์ปจํŠธ๋ž™ํŠธ ์ฃผ์†Œ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ๋ฉ๋‹ˆ๋‹ค.";
    private final long TEST_CHAIN_ID = 137L; // ํ…Œ์ŠคํŠธ์šฉ ์ฒด์ธ ID

    @Mock
    private Web3jConnectionManager mockConnectionManager; // ์ˆ˜์ •: ConnectionManager Mock
    @Mock
    private Credentials mockCredentials; // ์ˆ˜์ •: Credentials Mock
    @Mock
    private ContractManager mockContractManager; // ์ˆ˜์ •: ContractManager๋Š” ์—ฌ์ „ํžˆ Mock ํ•„์š”
    @Mock
    private Web3j mockWeb3j; // ์ˆ˜์ •: execute ์ฝœ๋ฐฑ์— ์ „๋‹ฌ๋  Web3j Mock

    // @InjectMocks ์‚ฌ์šฉ ์‹œ Mockito๊ฐ€ ์ƒ์„ฑ์ž์— Mock ๊ฐ์ฒด๋“ค์„ ์ฃผ์ž… ์‹œ๋„
    // ๋‹จ, ์ƒ์„ฑ์ž ์ฃผ์ž… ์™ธ @Value ๋“ฑ์ด ์žˆ์œผ๋ฉด ์ง์ ‘ ์ƒ์„ฑํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Œ
    private ContractHandler contractHandler;

    // --- Helper for mocking connectionManager.execute ---
    // ์ด Answer๋Š” connectionManager.execute๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ
    // 1. ContractManager.load๊ฐ€ mockContractManager๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•˜๊ณ  (์ •์  ๋ฉ”์„œ๋“œ ๋ชจํ‚น ํ•„์š”)
    // 2. ์ „๋‹ฌ๋œ ๋žŒ๋‹ค(callable)๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.
    // ์ •์  ๋ฉ”์„œ๋“œ ๋ชจํ‚น ๋Œ€์‹ , ๋žŒ๋‹ค๊ฐ€ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•  ์ตœ์ข… ๊ฒฐ๊ณผ๋งŒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ๋” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.
    private <T> Answer<T> simulateExecution(T expectedResult) {
        return invocation -> {
            // 1. invocation์—์„œ ๋žŒ๋‹ค(Web3jCallable) ๊ฐ€์ ธ์˜ค๊ธฐ (์„ ํƒ์ )
            // Web3jConnectionManager.Web3jCallable<T> callable = invocation.getArgument(0);

            // 2. ๋žŒ๋‹ค๊ฐ€ ์‹คํ–‰๋  ๋•Œ ๋ฐ˜ํ™˜๋  ๊ฒฐ๊ณผ (๊ฐ€์žฅ ์ค‘์š”)
            //    ์‹ค์ œ ๋žŒ๋‹ค ์‹คํ–‰ ๋Œ€์‹ , ๋žŒ๋‹ค ์‹คํ–‰์˜ *๊ฒฐ๊ณผ*๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
            //    ์ด ๊ฒฐ๊ณผ๋Š” ๋ณดํ†ต ContractManager์˜ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ(.send()) ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
            //    ๋”ฐ๋ผ์„œ ๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์—์„œ mockContractManager์˜ ๋™์ž‘์„ ๋ฏธ๋ฆฌ ์„ค์ •ํ•ด๋‘์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
            return expectedResult;
        };
    }

    // execute๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋„๋ก ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” Answer
    private <T> Answer<T> simulateExecutionWithError(Exception exceptionToThrow) {
        return invocation -> {
            throw exceptionToThrow;
        };
    }


    @BeforeEach
    void setUp() {
        // MockitoAnnotations.openMocks(this) ๋Œ€์‹  @ExtendWith(MockitoExtension.class) ์‚ฌ์šฉ
        // @InjectMocks๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑ์ž ํ˜ธ์ถœ
        contractHandler = new ContractHandler(
                mockConnectionManager,
                mockCredentials,
                TEST_CHAIN_ID,
                TEST_CONTRACT_ADDRESS
        );
        // CustomGasProvider๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ new๋กœ ์ƒ์„ฑ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ฃผ์ž… ๋ถˆํ•„์š”
        // ContractManager๋Š” loadContractManager ํ—ฌํผ ๋‚ด์—์„œ ๋กœ๋“œ๋˜๋ฏ€๋กœ ํ•„๋“œ ์ฃผ์ž… ๋ถˆํ•„์š”
    }

    @Test
    void testAddContract_Success() throws Exception { // CompletableFuture ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด throws Exception ์ถ”๊ฐ€
        // --- Input Data ---
        PaymentInfoInput paymentInfo1 = new PaymentInfoInput(BigInteger.valueOf(1), BigInteger.valueOf(2100000),
                BigInteger.valueOf(7));
        // ... (๋‹ค๋ฅธ PaymentInfoInput)
        ContractInput input = new ContractInput(/* ... input data ์„ค์ • ... */);
        input.setId(BigInteger.ONE);
        input.setPaymentInfos(List.of(paymentInfo1)); // ์˜ˆ์‹œ

        // --- Mocking ---
        // 1. ์ตœ์ข… ๊ฒฐ๊ณผ์ธ TransactionReceipt Mock ์„ค์ •
        TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
        when(mockReceipt.isStatusOK()).thenReturn(true);

        // 2. connectionManager.execute๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด mockReceipt๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •
        //    any()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ด๋–ค Web3jCallable์ด๋“  ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋„๋ก ์„ค์ •
        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(mockReceipt)); // ์„ฑ๊ณต ์‹œ Receipt ๋ฐ˜ํ™˜

        // --- Execution ---
        CompletableFuture<Boolean> futureResult = contractHandler.addContract(input);

        // --- Verification ---
        assertTrue(futureResult.join(), "Contract should be added successfully");

        // connectionManager.execute๊ฐ€ ์ •ํ™•ํžˆ 1๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ (์„ ํƒ์ )
        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }

    @Test
    void testAddContract_Failure_ExceptionDuringExecution() throws Exception {
        // --- Input Data ---
        ContractInput input = new ContractInput(/* ... input data ์„ค์ • ... */);
        input.setId(BigInteger.TWO);

        // --- Mocking ---
        // connectionManager.execute๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ RuntimeException์„ ๋˜์ง€๋„๋ก ์„ค์ •
        RuntimeException simulatedException = new RuntimeException("Blockchain connection failed");
        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecutionWithError(simulatedException));

        // --- Execution ---
        CompletableFuture<Boolean> futureResult = contractHandler.addContract(input);

        // --- Verification ---
        assertFalse(futureResult.join(), "Should return false when an exception occurs");

        // connectionManager.execute๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ
        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }

    @Test
    void testGetContract() throws Exception {
        // --- Mocking Data ---
        BigInteger contractId = BigInteger.ONE;
        Tuple13<BigInteger, String, String, BigInteger, BigInteger, String, String, BigInteger, List<PaymentInfo>, String, Boolean, BigInteger, BigInteger> dummyTuple =
                new Tuple13<>(
                        contractId, "2025-01-01Z", "2025-12-31Z", BigInteger.valueOf(3000000), BigInteger.valueOf(5),
                        "112233445566", "998877665544", BigInteger.TEN,
                        List.of(new PaymentInfo(new Uint256(1), new Uint256(2100000), new Uint256(7))),
                        "123456789012", true, BigInteger.valueOf(3), BigInteger.valueOf(123)
                );

        // --- Mocking ---
        // connectionManager.execute๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ์ตœ์ข… ๊ฒฐ๊ณผ์ธ dummyTuple์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •
        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(dummyTuple));

        // --- Execution ---
        ContractOutput result = contractHandler.getContract(contractId);

        // --- Verification ---
        assertNotNull(result);
        assertEquals(contractId, result.getId());
        assertEquals("2025-01-01Z", result.getStartDate());
        // ... (๋‹ค๋ฅธ ํ•„๋“œ ๊ฒ€์ฆ)

        // connectionManager.execute๊ฐ€ 1๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆ
        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
        // ์ค‘์š”: contractManager ์ž์ฒด์˜ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์„ ์ง์ ‘ ๊ฒ€์ฆํ•˜๋Š” ๋Œ€์‹ ,
        // connectionManager.execute์˜ ํ˜ธ์ถœ๊ณผ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
    }

    @Test
    void testGetContractOverview() throws Exception {
        // --- Mocking Data ---
        BigInteger contractId = BigInteger.ONE;
        Tuple3<BigInteger, String, String> dummyTuple =
                new Tuple3<>(contractId, "2025-01-01", "2025-12-31");

        // --- Mocking ---
        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(dummyTuple));

        // --- Execution ---
        ContractOverviewOutput result = contractHandler.getContractOverview(contractId);

        // --- Verification ---
        assertNotNull(result);
        assertEquals(contractId, result.getId());
        assertEquals("2025-01-01", result.getStartDate());

        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }

    // ... (getPaymentInfoCount, getRentData ๋“ฑ ๋‹ค๋ฅธ ์ฝ๊ธฐ ํ…Œ์ŠคํŠธ๋„ ์œ ์‚ฌํ•˜๊ฒŒ ์ˆ˜์ •) ...
    // ์˜ˆ์‹œ: getRentData
    @Test
    void testGetRentData() throws Exception {
        // --- Mocking Data ---
        BigInteger contractId = BigInteger.ONE;
        Tuple6<BigInteger, BigInteger, String, String, BigInteger, BigInteger> dummyTuple =
                new Tuple6<>(
                        BigInteger.valueOf(3000000), BigInteger.valueOf(5), "112233445566",
                        "665544332211", BigInteger.TEN, BigInteger.valueOf(3)
                );

        // --- Mocking ---
        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(dummyTuple));

        // --- Execution ---
        ContractRentOutput result = contractHandler.getRentData(contractId);

        // --- Verification ---
        assertNotNull(result);
        assertEquals("112233445566", result.getRentAccountNo());

        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }


    @Test
    void testAddLiveAccount_Success() throws Exception {
        // --- Input ---
        BigInteger contractId = BigInteger.ONE;
        LiveAccountInput liveAccountInput = new LiveAccountInput("validAccountNo");

        // --- Mocking ---
        TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
        when(mockReceipt.isStatusOK()).thenReturn(true);

        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(mockReceipt));

        // --- Execution ---
        boolean result = contractHandler.addLiveAccount(contractId, liveAccountInput);

        // --- Verification ---
        assertTrue(result, "Adding live account should succeed");
        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }

    @Test
    void testAddLiveAccount_Failure_ReceiptNotOk() throws Exception {
        // --- Input ---
        BigInteger contractId = BigInteger.ONE;
        LiveAccountInput liveAccountInput = new LiveAccountInput("validAccountNo");

        // --- Mocking ---
        TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
        when(mockReceipt.isStatusOK()).thenReturn(false); // ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

        when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
                .thenAnswer(simulateExecution(mockReceipt));

        // --- Execution ---
        boolean result = contractHandler.addLiveAccount(contractId, liveAccountInput);

        // --- Verification ---
        assertFalse(result, "Should return false when transaction receipt is not OK");
        verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
    }

}
profile
ํ•œ ๊ฑธ์Œ์”ฉ ๊พธ์ค€ํžˆ

0๊ฐœ์˜ ๋Œ“๊ธ€