포스트

Mybatis의 비동기 쿼리 사용

목차

  1. 개요
  2. 핵심 차이점
  3. DAO 계층 변경
  4. Service 계층 변경
  5. 여러 쿼리 동시 실행하기
  6. Mono / Flux 처리 시 알아두면 좋은 것들
  7. 핵심 패턴 요약
  8. 참고 자료

1. 개요

Spring WebFlux프로젝트를 진행하며, DAO와 Service 계층의 코드 내 Mybatis를 비동기로 실햏하는 방법과 Spring MVC 프로젝트와의 차이점에 대해 설명합니다.


2. 핵심 차이점

구분Spring MVCSpring WebFlux
프로그래밍 모델Blocking (동기)Non-blocking (비동기)
반환 타입User, List<User>Mono<User>, Flux<User>
DB 접근JDBC (Blocking)R2DBC (Non-blocking) 또는 JDBC + CompletableFuture
HTTP 클라이언트RestTemplate (Blocking)WebClient (Non-blocking)
스레드 모델Thread per requestEvent Loop (적은 스레드로 많은 요청 처리)

3. DAO 계층 변경

기존 MyBatis나 JDBC는 블로킹 방식이므로, CompletableFuture로 감싸서 비동기로 실행합니다.

3.1 Spring MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Repository
@RequiredArgsConstructor
public class MemberDAO {
    private final SqlSessionTemplate sqlSessionTemplate;

    // 동기 방식 - 메서드가 완료될 때까지 스레드 블로킹
    public MemberVM getMemberInfo(SigninReq param) {
        return sqlSessionTemplate.selectOne("memberSQL.getMemberInfo", param);
    }

    public boolean insertMember(MemberVM param) {
        return sqlSessionTemplate.insert("memberSQL.insertMember", param) > 0;
    }

    public int existsByAddress(String address) {
        return sqlSessionTemplate.selectOne("memberSQL.existsByAddress", address);
    }

    public List<MemberVM> getAllMembers() {
        return sqlSessionTemplate.selectList("memberSQL.getAllMembers");
    }
}

3.2 Spring WebFlux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Repository
@RequiredArgsConstructor
public class MemberDAO {
    // 실제 테스트한 프로젝트의 경우, 2개의 JDBC 연결 테스트도 진행하여 "@Qualifier" 사용
    @Qualifier("sqlSessionTemplate")
    private final SqlSessionTemplate sqlSessionTemplate;

    /**
     * CompletableFuture로 감싸서 비동기 실행
     * Service 계층에서 Mono.fromFuture()로 변환하여 사용
     */
    public CompletableFuture<MemberVM> getMemberInfo(SigninReq param) {
        return CompletableFuture.supplyAsync(() -> {
            return sqlSessionTemplate.selectOne("memberSQL.getMemberInfo", param);
        });
    }

    public CompletableFuture<Boolean> insertMember(MemberVM param) {
        return CompletableFuture.supplyAsync(() -> {
            return sqlSessionTemplate.insert("memberSQL.insertMember", param) > 0;
        });
    }

    /**
     * 간단한 조회는 동기로 유지 (선택적)
     * - 인덱스 기반 단일 행 조회
     * - 빠른 응답이 보장되는 경우
     */
    public int existsByAddress(String address) {
        return sqlSessionTemplate.selectOne("memberSQL.existsByAddress", address);
    }

    /**
     * 리스트 조회는 CompletableFuture<List<T>> 반환
     * Service에서 Flux로 변환 가능
     */
    public CompletableFuture<List<MemberVM>> getAllMembers() {
        return CompletableFuture.supplyAsync(() -> {
            return sqlSessionTemplate.selectList("memberSQL.getAllMembers");
        });
    }
}

주요 변경 사항:

  1. 반환 타입: T → CompletableFuture<T>
  2. CompletableFuture.supplyAsync(() -> { ... }) 로 블로킹 코드 감싸기
  3. 간단한 조회는 동기로 유지 가능 (선택적)

4. Service 계층 변경

4.1 Spring MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberDAO memberDAO;
    private final TokenProvider tokenProvider;

    // 동기 방식 - 메서드가 완료될 때까지 블로킹
    public SigninRes login(SigninReq reqDto) {
        // 유저 정보 암호화
        String password = SecurityUtil.encryptHash(reqDto.getPassword());
        reqDto.setPassword(password);

        // DB 조회 (블로킹)
        MemberVM member = memberDAO.getMemberInfo(reqDto);

        if (member == null) {
            throw new GatewayException(ErrorCode.USER_NOT_FOUND);
        }

        // JWT 토큰 생성
        String token = tokenProvider.createToken(member);

        SigninRes res = new SigninRes();
        res.setName(member.getName());
        res.setRole(member.getRole());
        res.setToken(token);

        return res;
    }
}

4.2 Spring WebFlux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberDAO memberDAO;
    private final TokenProvider tokenProvider;

    /**
     * 반환 타입: SigninRes → Mono<SigninRes>
     * 비동기 체이닝으로 non-blocking 처리
     */
    public Mono<SigninRes> login(SigninReq reqDto) {
        // 1. 비밀번호 암호화 (동기 작업 - 빠름)
        String password = SecurityUtil.encryptHash(reqDto.getPassword());
        reqDto.setPassword(password);

        // 2. CompletableFuture를 Mono로 변환
        Mono<MemberVM> queryResult = Mono.fromFuture(memberDAO.getMemberInfo(reqDto));

        // 3. Reactive 체이닝
        return queryResult.flatMap(member -> {
            // JWT 토큰 생성 (동기 작업)
            String token = tokenProvider.createToken(member);

            // 응답 생성
            SigninRes res = new SigninRes();
            res.setName(member.getName());
            res.setRole(member.getRole());
            res.setToken(token);

            return Mono.just(res);
        })
        // member가 null이면 에러
        .switchIfEmpty(Mono.error(new GatewayException(ErrorCode.USER_NOT_FOUND)));
    }
}

주요 변경 사항:

  1. 반환 타입: SigninRes → Mono<SigninRes>
  2. DAO 호출 결과를 Mono.fromFuture()로 변환
  3. flatMap()으로 비동기 변환 처리
  4. 예외는 throw 대신 Mono.error() 사용
  5. null 체크는 switchIfEmpty() 사용

5. 여러 쿼리 동시 실행하기

5.1 Spring MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
public DashboardVM getDashboard(Long userId) {
    // 순차적으로 실행 (각각 블로킹)
    MemberVM member = memberDAO.getMemberInfo(userId);
    List<Order> orders = orderDAO.getOrdersByUserId(userId);
    Stats stats = statsDAO.getStats(userId);

    DashboardVM dashboard = new DashboardVM();
    dashboard.setMember(member);
    dashboard.setOrders(orders);
    dashboard.setStats(stats);

    return dashboard;
}

5.2 Spring WebFlux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Mono<DashboardVM> getDashboard(Long userId) {
    // 3개 쿼리를 병렬로 실행
    Mono<MemberVM> memberMono = Mono.fromFuture(memberDAO.getMemberInfo(userId));
    Mono<List<Order>> ordersMono = Mono.fromFuture(orderDAO.getOrdersByUserId(userId));
    Mono<Stats> statsMono = Mono.fromFuture(statsDAO.getStats(userId));

    // zip()으로 3개 결과를 결합
    return Mono.zip(memberMono, ordersMono, statsMono)
        .map(tuple -> {
            DashboardVM dashboard = new DashboardVM();
            dashboard.setMember(tuple.getT1());
            dashboard.setOrders(tuple.getT2());
            dashboard.setStats(tuple.getT3());
            return dashboard;
        });
}

성능 개선:

  • MVC: 3개 쿼리가 순차 실행 (총 300ms = 100ms × 3)
  • WebFlux: 3개 쿼리가 병렬 실행 (총 100ms = max(100ms, 100ms, 100ms))

6. Mono / Flux 처리 시 알아두면 좋은 것들

6.1 flatMap() - 비동기 변환

1
2
3
4
5
6
7
8
9
10
11
// Mono<User>를 Mono<UserDTO>로 변환 (비동기 작업 포함)
public Mono<UserDTO> getUserDTO(Long userId) {
    return Mono.fromFuture(userDAO.getUser(userId))
        .flatMap(user -> {
            // 사용자의 권한 정보를 비동기로 조회
            return Mono.fromFuture(roleDAO.getRoleByUserId(user.getId()))
                .map(role -> {
                    return new UserDTO(user, role);
                });
        });
}

6.2 switchIfEmpty() - 빈 결과 처리

1
2
3
4
public Mono<User> getUser(Long userId) {
    return Mono.fromFuture(userDAO.getUser(userId))
        .switchIfEmpty(Mono.error(new NotFoundException("User not found")));
}

6.3 zip() - 여러 Mono 병렬 실행

1
2
3
4
5
6
7
public Mono<Dashboard> getDashboard(Long userId) {
    Mono<User> userMono = Mono.fromFuture(userDAO.getUser(userId));
    Mono<List<Order>> ordersMono = Mono.fromFuture(orderDAO.getOrders(userId));

    return Mono.zip(userMono, ordersMono)
        .map(tuple -> new Dashboard(tuple.getT1(), tuple.getT2()));
}

7. 핵심 패턴 요약

패턴사용 시점예제
Mono.fromFuture()CompletableFuture → Mono 변환Mono.fromFuture(dao.getUser())
Mono.fromCallable()블로킹 코드 래핑JDBC, 파일 I/O
flatMap()비동기 변환 (중첩 Mono)쿼리 결과로 다른 쿼리 실행
map()동기 변환DTO 변환, 계산
zip()병렬 실행여러 쿼리 동시 실행
switchIfEmpty()빈 결과 대체null 체크, 기본값
onErrorResume()에러 복구Fallback 로직
Mono.just()동기 값을 Mono로성공 결과 반환
Mono.error()에러 전파예외 처리

8. 참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.