Skip to content

Commit 5fd0596

Browse files
committed
append asynchronous env guide docs
1 parent 04ca108 commit 5fd0596

6 files changed

+573
-22
lines changed

README-EN.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# 🖥️ Multi-DataSource Query Counter
2+
3+
[한국어](README.md) | **English**
4+
5+
## 🖥️ Introduction
6+
7+
This project is a simple tool to measuring query counts in a JDBC API(Spring Data JPA(Hibernate), MyBatis, ...) environment.
8+
You can seamlessly measure query counts also in a Multi-DataSource environment.
9+
10+
```
11+
Hibernate:
12+
select
13+
id,
14+
name
15+
from
16+
example
17+
18+
Hibernate:
19+
select
20+
count(id)
21+
from
22+
example
23+
24+
ERROR --- 'GET /examples' - totalQueryCount: 2, totalSpendTime: 7ms
25+
```
26+
27+
<br>
28+
29+
## 🖥️ How to use?
30+
31+
### 1. Add dependency
32+
33+
> [!IMPORTANT]
34+
> Spring Boot 3.x uses `jakarta.*` packages while Spring Boot 2.x uses `javax.*` packages. Make sure to select the compatible library version for your project.
35+
36+
Choose the appropriate version based on your Spring Boot version:
37+
38+
| Spring Boot Version | Library Version |
39+
|---------------------|---------------------|
40+
| Spring Boot 2.x | 2.x.x-spring-boot-2 |
41+
| Spring Boot 3.x | 2.x.x-spring-boot-3 |
42+
43+
### In Java Gradle(Groovy DSL)
44+
45+
```groovy
46+
repositories {
47+
mavenCentral()
48+
maven { url 'https://jitpack.io' }
49+
}
50+
51+
dependencies {
52+
// For Spring Boot 2.x
53+
implementation 'com.github.Hyeon9mak:multi-datasource-query-counter:2.0.2-spring-boot-2'
54+
55+
// For Spring Boot 3.x
56+
// implementation 'com.github.Hyeon9mak:multi-datasource-query-counter:2.0.2-spring-boot-3'
57+
}
58+
```
59+
60+
### In Kotlin Gradle(Kotlin DSL)
61+
62+
```kotlin
63+
repositories {
64+
mavenCentral()
65+
maven { url = uri("https://jitpack.io") }
66+
}
67+
68+
dependencies {
69+
// For Spring Boot 2.x
70+
implementation("com.github.Hyeon9mak:multi-datasource-query-counter:2.0.2-spring-boot-2")
71+
72+
// For Spring Boot 3.x
73+
// implementation("com.github.Hyeon9mak:multi-datasource-query-counter:2.0.2-spring-boot-3")
74+
}
75+
```
76+
77+
### 2. Add logging options
78+
79+
The priority order is as follows: `error` > `warn` > `info`.
80+
The default value for `enable` is `false`, and the default value for `count` is `1`.
81+
82+
```yaml
83+
query-counter.logging.level:
84+
error:
85+
enable: true
86+
count: 5
87+
warn:
88+
enable: true
89+
count: 2
90+
info:
91+
enable: false
92+
```
93+
94+
### 3. Specify the target API
95+
96+
`@CountQueries` annotation is used to specify the API you want to measure.
97+
98+
```java
99+
@CountQueries
100+
@GetMapping("/examples")
101+
public List<Example> getExamples() {
102+
return repository.getExamples();
103+
}
104+
```
105+
106+
### 4. Check the log
107+
108+
Start the application and check the log.
109+
110+
```
111+
ERROR --- 'GET /examples' - totalQueryCount: 2, totalSpendTime: 7ms
112+
```
113+
114+
Enjoy it! 🎉
115+
116+
<br>
117+
118+
## 🖥️ In asynchronous environments
119+
120+
- [Click here to read the guide for Java Asynchronous environments.](README-java-asynchronous-environments-EN.md)
121+
- [Click here to read the guide for Kotlin Coroutine environments.](README-kotlin-coroutine-environments-EN.md)
122+
123+
<br>
124+
125+
## 🖥️ Core concepts
126+
127+
![image](core_concept.png)
128+
129+
It's based on the JDBC API and Spring AOP, CGLib proxy.
130+
... That's it!
131+
132+
<br>
133+
134+
## 🖥️ Recommended articles
135+
136+
- [API 요청 당 쿼리 개수를 알고 싶어 라이브러리까지 만든 이야기 — 라이브러리 제작](https://medium.com/@hyeon9mak/api-%EC%9A%94%EC%B2%AD-%EB%8B%B9-%EC%BF%BC%EB%A6%AC-%EA%B0%9C%EC%88%98%EB%A5%BC-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%96%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EA%B9%8C%EC%A7%80-%EB%A7%8C%EB%93%A0-%EC%9D%B4%EC%95%BC%EA%B8%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%A0%9C%EC%9E%91-de39f0d27351)
137+
- [Hibernate 의 ‘불편한’ 편의 기능들](https://medium.com/monday-9-pm/hibernate-%EC%9D%98-%EB%B6%88%ED%8E%B8%ED%95%9C-%ED%8E%B8%EC%9D%98-%EA%B8%B0%EB%8A%A5%EB%93%A4-06a1fbc7492a)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# 🖥️ Sharing QueryCountPerRequest Across Threads in Asynchronous Environments
2+
3+
[한국어](README-java-asynchronous-environments.md) | **English**
4+
5+
## 🖥️ Overview
6+
7+
The Multi-Datasource-Query-Counter library counts database queries per API request. By default, it uses `@RequestScope` to ensure each request has its own counter.
8+
However, in asynchronous environments (`CompletableFuture`, `@Async`, WebFlux, etc.), execution switches to new threads, which cannot access the original request context, resulting in missed query counts.
9+
10+
This guide explains how to properly share the `QueryCountPerRequest` object across threads in asynchronous environments.
11+
12+
<br>
13+
14+
## 🖥️ The Problem
15+
16+
In a typical Spring application, a request is processed within a single thread.
17+
However, when using asynchronous code:
18+
19+
1. Execution switches to a new thread.
20+
2. The new thread has a new context.
21+
22+
So, the new thread cannot access the original request context.
23+
24+
<br>
25+
26+
## 🖥️ Solution: Using TaskDecorator
27+
28+
Spring provides `TaskDecorator` which allows copying the request context to new threads before executing asynchronous tasks.
29+
30+
### 1. Configure ThreadPoolTaskExecutor
31+
32+
```java
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.scheduling.annotation.EnableAsync;
36+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
37+
import org.springframework.web.context.request.RequestAttributes;
38+
import org.springframework.web.context.request.RequestContextHolder;
39+
40+
import java.util.concurrent.Executor;
41+
42+
@EnableAsync
43+
@Configuration
44+
public class AsyncConfig {
45+
46+
@Bean(name = "asyncExecutor")
47+
public Executor asyncExecutor() {
48+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
49+
50+
// Configure thread pool settings according to your application needs... (skip)
51+
52+
// Set TaskDecorator to copy the request context to the new thread!
53+
executor.setTaskDecorator(task -> {
54+
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
55+
return () -> {
56+
try {
57+
RequestContextHolder.setRequestAttributes(requestAttributes);
58+
task.run();
59+
} finally {
60+
RequestContextHolder.resetRequestAttributes();
61+
}
62+
};
63+
});
64+
65+
executor.initialize();
66+
return executor;
67+
}
68+
}
69+
```
70+
71+
### 2. Using CompletableFuture
72+
73+
When using CompletableFuture directly, use the custom Executor:
74+
75+
```java
76+
@Autowired
77+
@Qualifier("asyncExecutor")
78+
private Executor asyncExecutor;
79+
80+
public CompletableFuture<List<User>> findAllUsersAsync() {
81+
return CompletableFuture.supplyAsync(() -> {
82+
return userRepository.findAll(); // Query count point.
83+
}, asyncExecutor); // Specify the custom executor.
84+
}
85+
```
86+
87+
### 3. Using @Async Annotation
88+
89+
When using the @Async annotation, explicitly specify the configured Executor:
90+
91+
```java
92+
@Async("asyncExecutor") // Specify the bean name explicitly.
93+
public CompletableFuture<User> findUserByIdAsync(Long id) {
94+
return CompletableFuture.completedFuture(
95+
userRepository.findById(id).orElse(null) // Query count point.
96+
);
97+
}
98+
```
99+
100+
<br>
101+
102+
## 🖥️ WebFlux/Reactor Environment
103+
104+
(WIP)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# 🖥️ 비동기 환경에서 스레드 간 QueryCountPerRequest 공유하기
2+
3+
**한국어** | [English](README-java-asynchronous-environments-EN.md)
4+
5+
## 🖥️ 개요
6+
7+
Multi-Datasource-Query-Counter 라이브러리는 API 요청별로 데이터베이스 쿼리 수를 계산합니다.
8+
라이브러리는 기본적으로 `@RequestScope`를 활용하여 각 API 요청마다 카운터를 갖도록 하고 있습니다.
9+
그러나 비동기 환경(`CompletableFuture`, `@Async`, WebFlux 등)에서 별다른 설정 없이 새 스레드로 전환이 진행되면 서로 다른 카운터를 갖게 되므로 올바른 쿼리 개수 측정이 불가능합니다.
10+
11+
이 가이드는 비동기 환경에서 스레드 간 `QueryCountPerRequest` 객체를 올바르게 공유하는 방법을 설명합니다.
12+
13+
<br>
14+
15+
## 🖥️ 문제점
16+
17+
일반적인 Spring 애플리케이션에서 API 요청은 단일 스레드 내에서 처리됩니다.
18+
그러나 비동기 코드를 사용할 때는 아래와 같은 순서를 가집니다.
19+
20+
1. 새 스레드로 분기
21+
2. 새 스레드는 새로운 컨텍스트를 갖는다.
22+
23+
때문에 이전 스레드의 컨텍스트에 접근할 수 없습니다.
24+
25+
<br>
26+
27+
## 🖥️ 해결책: TaskDecorator 사용하기
28+
29+
Spring 은 비동기 작업을 실행하기 전에 새 스레드에 컨텍스트를 복사할 수 있는 `TaskDecorator` 를 제공합니다.
30+
31+
### 1-1. ThreadPoolTaskExecutor 구성하기
32+
33+
```java
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.scheduling.annotation.EnableAsync;
37+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
38+
import org.springframework.web.context.request.RequestAttributes;
39+
import org.springframework.web.context.request.RequestContextHolder;
40+
41+
import java.util.concurrent.Executor;
42+
43+
@EnableAsync
44+
@Configuration
45+
public class AsyncConfig {
46+
47+
@Bean(name = "asyncExecutor")
48+
public Executor asyncExecutor() {
49+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
50+
51+
// 애플리케이션 요구사항에 맞게 스레드 풀 구성... (생략)
52+
53+
// 새 스레드에 요청 컨텍스트를 복사하기 위한 TaskDecorator 설정!
54+
executor.setTaskDecorator(task -> {
55+
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
56+
return () -> {
57+
try {
58+
RequestContextHolder.setRequestAttributes(requestAttributes);
59+
task.run();
60+
} finally {
61+
RequestContextHolder.resetRequestAttributes();
62+
}
63+
};
64+
});
65+
66+
executor.initialize();
67+
return executor;
68+
}
69+
}
70+
```
71+
72+
### 1-2. CompletableFuture 사용하기
73+
74+
CompletableFuture 를 직접 사용할 때는 커스텀 Executor 를 활용합니다.
75+
76+
```java
77+
@Autowired
78+
@Qualifier("asyncExecutor")
79+
private Executor asyncExecutor;
80+
81+
public CompletableFuture<List<User>> findAllUsersAsync() {
82+
return CompletableFuture.supplyAsync(() -> {
83+
return userRepository.findAll(); // 쿼리 카운트 지점.
84+
}, asyncExecutor); // 커스텀 Executor 지정.
85+
}
86+
```
87+
88+
### 1-3. @Async 어노테이션 사용하기
89+
90+
`@Async` 어노테이션을 사용할 때는 구성된 Executor 를 명시적으로 지정합니다.
91+
92+
```java
93+
@Async("asyncExecutor") // Bean 이름을 명시적으로 지정.
94+
public CompletableFuture<User> findUserByIdAsync(Long id) {
95+
return CompletableFuture.completedFuture(
96+
userRepository.findById(id).orElse(null) // 쿼리 카운트 지점.
97+
);
98+
}
99+
```
100+
101+
<br>
102+
103+
## 🖥️ WebFlux/Reactor 환경
104+
105+
(WIP)

0 commit comments

Comments
 (0)