๊ตฌํ ๊ณ๊ธฐ
์ด์ ์ ์งํํ "๋ชจ๋์ ๋ด์ค"๋ผ๋ ํ๋ก์ ํธ์ ํ์ด์ง๋ค์ด์ ๋ฑ ์กฐํ API ๊ตฌํ์ ๋ด๋นํ ์ ์ด ์์ต๋๋ค.
์ด๋ฐํ ์ผ์ ํ์ "์ผ๋จ ๋์๊ฐ๊ฒ ํ์"๋ผ๋ ์๊ฐ์ผ๋ก ํจ์จ์ ์ธ ์ฟผ๋ฆฌ ์ค๊ณ๋ ๊น๊ฒ ๊ณ ๋ฏผํ์ง ์๊ณ ๊ตฌํํ์ต๋๋ค.
๊ทธ ํ ๋ฆฌํฉํ ๋ง ์์ ์์ ์ง๋์น๊ฒ ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํ๊ณ , N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ์ ์ง์คํ์ต๋๋ค.



๋น์ ์ ๋ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ฌ ํธ์ถ๋๋ ์ฟผ๋ฆฌ ํ์๋ฅผ ์ค์ธ๋ค๋ฉด 'DB ๋ถํ๊ฐ ์ค์ด๋๋ ๋น์ฐํ ์ฑ๋ฅ ๊ฐ์ ์ด ๋๊ฒ ์ง'๋ผ๊ณ ๋ง์ฐํ ์๊ฐํ์ต๋๋ค.
๋ฌผ๋ก ํ๋ฆฐ ๋ง์ ์๋๋๋ค๋ง, "๊ทธ๋์ ์ค์ ๋ก ์ฑ๋ฅ์ด ์ผ๋ง๋ ๊ฐ์ ๋๋๋ฐ?"๋ผ๋ ์ง๋ฌธ์ด ๋ฐ๋ผ์ฌ ์ ์์ต๋๋ค.

๋ง์นจ ์ฝ๋ ๋ฆฌ๋ทฐ ๊ณผ์ ์์ ํ์๋ถ์ด “์คํ ์๊ฐ์ ์ง์ ์ธก์ ํด๋ณด๋ฉด ๊ฐ์ ํจ๊ณผ๋ฅผ ์์น๋ก ํ์ธํ ์ ์์ ๊ฒ ๊ฐ๋ค”๋ ํผ๋๋ฐฑ์ ์ฃผ์ จ์ต๋๋ค.
๊ทธ๋๊น์ง ์ ๋ ‘N+1 ๋ฌธ์ ’๋ผ๋ ๊ฐ๋ ์์ฒด์๋ง ์ง์คํด์ ์คํ ์๊ฐ์ ๊ณ ๋ คํ์ง ์๊ณ , ๋จ์ํ ์ฟผ๋ฆฌ ํ์๋ง ์ธ๊ณ ์๋ ์ํฉ์ด์์ต๋๋ค.
๊ทธ๊ฒ๋ ์ผ์ผ์ด... ๋ก๊ทธ์์ ํ๋ ํ๋ ์ ์ต๋๋ค..
spring:
jpa:
show-sql: false # SQL ๋ก๊ทธ ์ถ๋ ฅ ์ค์ -> logging ๋ ๋ฒจ ์ค์ ๋๋ฌธ์ off
properties:
hibernate:
format_sql: true # SQL ํฌ๋งทํ
dialect: org.hibernate.dialect.PostgreSQLDialect
logging:
level:
root: info
org.hibernate.SQL: debug # SQL ์ฟผ๋ฆฌ ๋ก๊ทธ ํ์
org.hibernate.orm.jdbc.bind: trace # ๋ฐ์ธ๋ฉ๊ฐ๋ ํจ๊ป ํ์
์ ๊ฐ ๋งก์ ํ์ด์ง๋ค์ด์ API๊ฐ ํ๋ ๊ฐ๊ฐ ์๋๋ผ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉฐ ์ฟผ๋ฆฌ๋ฅผ ์ผ์ผ์ด ์ธ๋ ๊ฑด ๋งค์ฐ ๋นํจ์จ์ ์ด์์ต๋๋ค.
๊ทธ๋์ ๊ฐ๋จํ๊ฒ ์ฟผ๋ฆฌ ํ์ ์ ๋ฐฉ๋ฒ ์๋ + ์คํ ์๊ฐ๋ ์ธก์ ํ ๋ฐฉ๋ฒ ์๋ํ๋ ์๊ฐ์ด ๋ค์์ต๋๋ค.
์ AOP๋ฅผ ์ ํํ๋๊ฐ?
์ฟผ๋ฆฌ ์คํ ์์ ์คํ ์๊ฐ์ ์๋์ผ๋ก ๊ธฐ๋กํ๋ ค๋ฉด,
API ์์ฒญ์ด ๋ค์ด์ฌ ๋๋ง๋ค ์ฟผ๋ฆฌ ์คํ ์ ๋ณด๋ฅผ ์์งํ๊ณ , ์์ฒญ์ด ๋๋๋ฉด ์ด๋ฅผ ๋ก๊ทธ๋ก ๋จ๊ธฐ๋ ๊ตฌ์กฐ๊ฐ ํ์ํฉ๋๋ค.
๋จ์ํ ์๋น์ค ๋ก์ง ์์
`์์ฒญ ์์ ์๊ฐ ๊ธฐ๋ก → ๋น์ฆ๋์ค ๋ก์ง ์ํ → ์ข ๋ฃ ํ ๋ก๊ทธ ์ถ๋ ฅ` ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ์์
์ด๋ฐ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
- ๋ชจ๋ API ๋ฉ์๋์ ์ค๋ณต์ ์ธ ์ฝ๋๊ฐ ๋ค์ด๊ฐ๋ค
- ์ฑ๋ฅ ์ธก์ ์ด๋ผ๋ ๋ถ๊ฐ์ ์ธ ๋ชฉ์ ์ ์ฝ๋๊ฐ ๋น์ฆ๋์ค ๋ก์ง ์ฝ๋์ ์์ธ๋ค
์ด๋ฌํ ์๊ฐ์์ ํก๋จ ๊ด์ฌ์ฌ ๊ฐ๋ ์ ๋ ์ฌ๋ฆฌ๊ฒ ๋์ต๋๋ค.
์ฟผ๋ฆฌ ์คํ ํ์์ ์๊ฐ์ ์ฌ๋ ๊ฒ์ ์ฌ๋ฌ ๋ฉ์๋์ ๊ณตํต์ ์ผ๋ก ์ ์ฉ๋์ง๋ง ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ฌด๊ดํ ๊ด์ฌ์ฌ์ด๊ธฐ ๋๋ฌธ์
Spring์์๋ ์ด๋ฅผ AOP(Aspect Oriented Programming)์ผ๋ก ๋ถ๋ฆฌํด ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
์ฆ,
- ๋น์ฆ๋์ค ๋ก์ง์ ์์ ํ์ง ์์๋ ๋๊ณ
- ์ฌ๋ฌ API์ ์ผ๊ด๋๊ฒ ์ ์ฉํ๊ธฐ ์ํด
Spring AOP๋ก ๊ตฌํํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
์ค๊ณ ๊ณผ์
ํต์ฌ ๋ชฉํ๋ ๋ค์ ์ธ ๊ฐ์ง์ ๋๋ค.
- ๋น์ฆ๋์ค ๋ก์ง์ ์์ ํ์ง ์๊ณ ์๋ ์ธก์ ํ ๊ฒ
- API ์์ฒญ ๋จ์๋ง๋ค ๋ ๋ฆฝ์ ์ผ๋ก ์ธก์ ํ ๊ฒ
- ํ๋์ API๊ฐ ์คํ๋๋ ๋์ ํธ์ถ๋ ์ฟผ๋ฆฌ ํ์์ ์ฟผ๋ฆฌ์ ์ด ์คํ ์๊ฐ์ ๊ตฌํ ๊ฒ + ์ฝ๊ฒ ๊ตฌ๋ถํ๊ธฐ ์ํด API ๊ฒฝ๋ก๋ ํจ๊ป ์ถ๋ ฅ
๐ @RequestScope: API ์์ฒญ ๋จ์๋ง๋ค ๋ ๋ฆฝ์ ์ธ ํต๊ณ ๊ฐ์ฒด ์์ฑํ๊ธฐ
API ์์ฒญ ํ ๋ฒ์ด ๋ค์ด์ฌ ๋๋ง๋ค ๊ทธ ์์ฒญ ์์์ ์คํ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๋ค์ ํต๊ณ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํด์ผ ํฉ๋๋ค.
์๋ฅผ ๋ค์ด `/articles` API๊ฐ ์คํ๋๋ฉด ํด๋น ์์ฒญ์์ ๋ฐ์ํ ์ฟผ๋ฆฌ๋ง ์ถ์ ํ๊ณ ,
๋ค์ ์์ฒญ `/users`๊ฐ ๋ค์ด์ค๋ฉด ์๋ก์ด ํต๊ณ ๊ฐ์ฒด๋ก ์ธก์ ์ ์์ํด์ผ ํฉ๋๋ค.
์ด๋ฐ ์๊ตฌ๋ฅผ ๋ง์กฑ์ํค๊ธฐ ์ํด `@RequestScope`๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
`@RequestScope`๋ Spring์ด ๊ฐ HTTP ์์ฒญ๋ง๋ค ์๋ก์ด ๋น(bean) ์ธ์คํด์ค๋ฅผ ์์ฑํด์ฃผ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
์ค์ฝํ(Scope)๋ ๋น์ด ์ผ๋ง๋ ์ค๋, ์ด๋ค ๋ฒ์์์ ์ ์ง๋๋๊ฐ๋ฅผ ๋งํฉ๋๋ค.
Spring์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฑ๊ธํค ์ค์ฝํ๋ผ๊ณ ํ์ฌ, ํ๋์ ๋น ์ ์ ๋น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์์ ํ๋์ ๋น ์ธ์คํด์ค๋ง ์์ฑํ๊ณ
IoC ์ปจํ ์ด๋ ๋ด์์ ์ด ํ๋์ ๋น ์ธ์คํด์ค๊ฐ ๊ณต์ ๋์ด ์ฌ์ฉ๋ฉ๋๋ค.
๋ํ ์น ํ๊ฒฝ์์ ์ฌ์ฉํ ์ ์๋ ํน๋ณํ ์ค์ฝํ๋ฅผ ์ถ๊ฐ๋ก ์ ๊ณตํ๋๋ฐ, ์ด๋ฅผ ์น ์ค์ฝํ๋ผ๊ณ ํฉ๋๋ค.
์ด ์ค ๋ฆฌํ์คํธ ์ค์ฝํ๋ ๋น์ ์๋ช ์ฃผ๊ธฐ๊ฐ HTTP ์์ฒญ 1ํ ๋์๋ง์ผ๋ก ์ ํ๋์ด ์์ฒญ์ด ๋ค์ด์ฌ ๋ ์์ฑ๋๊ณ , ์๋ต ํ์ ์๋ฉธ๋ฉ๋๋ค.
HTTP ์์ฒญ ๋จ์๋ก ๋ ๋ฆฝ์ ์ธ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ๊ณ ์ถ์ ๋ ์ฌ์ฉํ๋ ์ค์ฝํ์ ๋๋ค.
`@ReauestScope` ์ด๋ ธํ ์ด์ ์ผ๋ก ๋ฆฌํ์คํธ ์ค์ฝํ ๋น์ ์ฝ๊ฒ ์ ์ธํ ์ ์์ต๋๋ค.
HTTP ์์ฒญ์ด ๋ค์ด์ฌ ๋๋ง๋ค `QueryStatistics`๋ผ๋ ์๋ก์ด ์ธ์คํด์ค๋ฅผ ์์ฑํ์ฌ ํด๋น ์์ฒญ์ ๋ํ ์ฟผ๋ฆฌ ์คํ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ฒ ์ต๋๋ค.
@Component
@RequestScope
public class QueryStatistics { ... }
+ ์๋ฃ๋ฅผ ์ฐพ์๋ณด๋ฉด์ `@RequestScope` ์ธ์ ThreadLocal์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ๋ดค์ต๋๋ค.
ThreadLocal์ ํ์ฌ ์ค๋ ๋ ๋ด๋ถ์์๋ง ์ ๊ทผ ๊ฐ๋ฅํ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํ๋ ๋ฐฉ๋ฒ์ผ๋ก,
์ผ๋ฐ์ ์ผ๋ก ํ๋์ ์์ฒญ์ ํ๋์ ์ค๋ ๋์์ ์ฒ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ๊ฐ ์์ฒญ(=๊ฐ ์ค๋ ๋) ๋จ์๋ก ์ฟผ๋ฆฌ ์คํ ํ์๋ฅผ ์ ์ ์๋ ๋ฐฉ๋ฒ์ ๋๋ค.
ํ์ง๋ง ๋ง์ฝ ๋น๋๊ธฐ API๊ฐ ์๋ค๋ฉด ์์ฒญ ์ฒ๋ฆฌ ์ค ๋ค๋ฅธ ์ค๋ ๋๋ก ๋์ด๊ฐ ์๋ ์๊ณ ,
Spring์ด ์์์ ๊ฐ์ฒด ์๋ฉธ๊ณผ ๋ฉ๋ชจ๋ฆฌ ์ ๋ฆฌ๋ฅผ ํด์ฃผ๋ `@RequestScope`์ ๋ฌ๋ฆฌ ๋ช ์์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ ๋ฆฌ๋ฅผ ํด์ค์ผํ๋ ๋ถํธํจ ๋๋ฌธ์ ์ ๋ `@RequestScope`๋ฅผ ์ ํํ์ต๋๋ค.
๐ ์ฟผ๋ฆฌ ํธ์ถ ๊ฐ๋ก์ฑ๊ธฐ
์ด `QueryStatistics` ๊ฐ์ฒด์ ๊ฐ์ ์ฑ์๋ฃ๊ธฐ ์ํด์๋ ์ฟผ๋ฆฌ ์คํ ์์ ์ ๊ฐ๋ก์ฑ์ผ ํฉ๋๋ค.
์ด๋ค ์์ฒญ๋ง๋ค DB ์ฟผ๋ฆฌ๊ฐ ๋ช ๋ฒ ์คํ๋์๊ณ , ๊ฐ๊ฐ ์ผ๋ง๋ ๊ฑธ๋ ธ๋๊ฐ๋ฅผ ์์๋ด๊ธฐ ์ํด
์ฟผ๋ฆฌ ์คํ ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋๋ง๋ค ์ด๋ฅผ ๊ฐ์งํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
Spring ์ ํ๋ฆฌ์ผ์ด์ ์ด DB์ ํต์ ํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์คํํ ๋ ๊ณผ์ ์
๋ค์๊ณผ ๊ฐ์ด ์์ฝํ ์ ์์ต๋๋ค.
- `DataSource` ๊ฐ์ฒด์์ `Connection` ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์จ๋ค
- `Connection` ๊ฐ์ฒด์์ `PreparedStatement` ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค
- `PreparedStatement`์์ `executeQuery()` ๋ฑ ์ฟผ๋ฆฌ ์คํ ๋ฉ์๋๋ฅผ ํธ์ถํด ์ค์ ์ฟผ๋ฆฌ๋ฅผ DB์ ๋ณด๋ธ๋ค
| ๊ฐ์ฒด | ์ญํ |
| DataSource | DB ์ฐ๊ฒฐ์ ๊ด๋ฆฌ `getConnection()` ๋ฉ์๋๋ก Connection ๊ฐ์ฒด๋ฅผ ์ป์ ex) DB URL, ๋น๋ฐ๋ฒํธ ๋ฑ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌ |
| Connection | ์ค์ DB์์ ์ฐ๊ฒฐ ๋ด๋น Connection์ ํตํด SQL ์ฟผ๋ฆฌ ์คํ ์ค๋น / ํธ๋์ญ์ ๊ด๋ฆฌ |
| PreparedStatement | SQL ์ฟผ๋ฆฌ๋ฅผ ์ค์ ๋ก ์คํํ๋ ๊ฐ์ฒด |
์ด ์ค `PreparedStatement`์์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ฏ๋ก ํด๋น ์์ ์ ๊ฐ๋ก์ฑ๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ทธ๋ฌ๋ Spring AOP๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์๋ ๊ฐ๋ก์ฑ๋ ค๋ ๋ฉ์๋๊ฐ ์ ์๋ ํด๋์ค๊ฐ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋์ด ์์ด์ผ ํฉ๋๋ค.
ํ์ง๋ง `Connection`๊ณผ `PreparedStatement` ๋ชจ๋ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋์ด ์์ง ์๊ณ ,
`DataSource` ์ธํฐํ์ด์ค๋ง ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋์ด ์์ต๋๋ค.
๋ฐ๋ผ์ ์ฐ๋ฆฌ๋ `DataSource`์์ `getConnection()` ๋ฉ์๋๋ก `Connection` ๊ฐ์ฒด๋ฅผ ์ป์ ๋ ๋ผ์ด๋ค์ด AOP๋ฅผ ์ ์ฉํ๊ณ
์ฐ๋ฆฌ๊ฐ ๋ฐ๊พผ `Connection`, `PreparedStatement` ํ๋ก์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋๋ก ํฉ๋๋ค.

AOP๋ก `DataSource.getConnection()`์ ๊ฐ๋ก์ฑ๋๋ค.
`getConnection()`์ผ๋ก Connection ๊ฐ์ฒด๋ฅผ ๋ถ๋ฅผ ๋ ํ๋ก์ ๊ฐ์ฒด์ธ `ConnectionProxyHandler`๋ฅผ ๋ฐํํ๋๋ก ํฉ๋๋ค.
// AOP ์ ์ ํด๋์ค
@Aspect
@Component
@RequiredArgsConstructor
public class QueryStatisticsAop {
@Around("execution(* javax.sql.DataSource.getConnection())")
public Object getConnection(ProceedingJoinPoint joinPoint) throws Throwable {
Object connection = joinPoint.proceed();
return Proxy.newProxyInstance(
connection.getClass().getClassLoader(),
connection.getClass().getInterfaces(),
new ConnectionProxyHandler(connection, queryStatistics)
);
}
ํ๋ก์ ๊ฐ์ฒด์ธ `ConnectonProxyHandler`์์๋ `preparedStatement()` ํธ์ถ์ ๊ฐ์งํด์
๊ทธ ๊ฒฐ๊ณผ๋ก ๋ฐํ๋๋ ๊ฐ์ฒด๋ฅผ ๋ค์ ํ ๋ฒ ํ๋ก์๋ก ๊ฐ์๋๋ค.
`PreparedStatement` ๋์ ํ๋ก์ ๊ฐ์ฒด `PreparedStatementProxyHandler`๋ฅผ ๋ฐํํฉ๋๋ค.
return Proxy.newProxyInstance(
invokeResult.getClass().getClassLoader(),
invokeResult.getClass().getInterfaces(),
new PreparedStatementProxyHandler(invokeResult, queryStatistics)
);
`PreparedStatementProxyHandler`์์๋ ์ฟผ๋ฆฌ๋ฅผ ์ค์ ๋ก ์คํํ๋ ๋ฉ์๋๋ฅผ ๊ฐ์งํฉ๋๋ค.
์ฐธ๊ณ ๋ก `PreparedStatement`์์ ์ฟผ๋ฆฌ ์คํํ๋ ๋ฉ์๋๋ก๋ `executeQuery()`/`execute()`/`executeUpdate()`๊ฐ ์์ผ๋ฉฐ
executeQuery๋ SELECT, executeUpdate๋ INSERT/UPDATE/DELETE, execute๋ ๋ชจ๋ SQL ์คํ ๊ฐ๋ฅํ ๋ํ ๋ฉ์๋๋ค์ ๋๋ค.
@RequiredArgsConstructor
public class PreparedStatementProxyHandler implements InvocationHandler {
private final Object preparedStatement;
private final QueryStatistics queryStatistics;
private boolean isExecuteQuery(final Method method) {
// TODO:ํ์ฌ ํธ์ถ๋ Method๊ฐ execute, executeUpdate ๋ฑ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ๋ฉ์๋์ธ์ง ํ์ธํจ
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
// true๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์ค์ ๋ก ์คํํ๋ ์์
if (isExecuteQuery(method)) {
// TODO: ์ฟผ๋ฆฌ ์คํ ํ์์ ์๊ฐ์ ์นด์ดํธํ๋ ๋ก์ง ๊ตฌํ
}
return method.invoke(preparedStatement, args); // ์๋ PreparedStatement ๋์ ์ํ
}
}
์ง๊ธ๊น์ง์ ๊ณ์ธต์ ์ ๋ฆฌํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
ํด๋ผ์ด์ธํธ ์์ฒญ
↓
AOP (DataSource.getConnection)
↓
ConnectionProxyHandler
↓
PreparedStatementProxyHandler
↓
executeQuery / executeUpdate ํธ์ถ → QueryStatistics ๊ธฐ๋ก
๐ ์ฟผ๋ฆฌ ์คํ ์๊ฐ & ์คํ ํ์ ์ธก์ ํ๊ธฐ
`PreparedStatementProxyHandler`์์ ์ฟผ๋ฆฌ๋ฅผ ์ค์ ๋ก ์คํํ๋ ๋ฉ์๋๋ฅผ ๊ฐ์งํ๋ค๋ฉด
๊ทธ ์์์ ์ฟผ๋ฆฌ ์คํ ํ์์ ์ฟผ๋ฆฌ ์คํ ์๊ฐ์ ์ธก์ ํด์ผ ํฉ๋๋ค.
`QueryStatistics` ๊ฐ์ฒด์ ๊ด๋ จ ํ๋์ ๋ฉ์๋๋ฅผ ๋ง๋ค๊ณ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๋๋ง๋ค ๋ํ๋ฉด ๋ฉ๋๋ค.
@Component
@RequestScope
public class QueryStatistics {
private String apiUrl; // API ๊ฒฝ๋ก
private long queryCounts = 0L; // ์ฟผ๋ฆฌ ์คํ ํ์
private long queryTime = 0L; // ์ฟผ๋ฆฌ ์คํ ์๊ฐ
public void setApiUrl(String url) {
this.apiUrl = url;
}
public void addQueryTime(long queryTime) {
this.queryTime += queryTime;
}
public void addQueryCount() {
this.queryCounts++;
}
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
if (isExecuteQuery(method)) { // ํ ๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๋๋ง๋ค
// ์ด๋ฒ์ ์คํ๋ ์ฟผ๋ฆฌ์ ์คํ ์๊ฐ ๊ตฌํ๊ธฐ
final long beforeTime = System.currentTimeMillis();
final Object result = method.invoke(preparedStatement, args);
final long afterTime = System.currentTimeMillis();
// ์นด์ดํฐ ์ฆ๊ฐ
queryStatistics.addQueryCount();
queryStatistics.addQueryTime(afterTime - beforeTime);
return result;
}
return method.invoke(preparedStatement, args);
}
๐ API ๋จ์์ ๊ณตํต ์ ์ฉํ๊ธฐ
์ด์ ๋ AOP๋ฅผ ์ ์ํ๋ ํด๋์ค `QueryStatisticsAop` ํด๋์ค์์
๊ฐ API ์์ฒญ๋ง๋ค ๊ฐ๋ก์ฑ์ ์์งํ ์ฟผ๋ฆฌ ํต๊ณ๋ฅผ ๋ก๊ทธ๋ก ์ถ๋ ฅํ๋๋ก ํด๋ด
๋๋ค.
@RequestScope๋ก ์์ฑ๋ QueryStatistics๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ํ ์์ฒญ ๋จ์๋ง๋ค ๋ ๋ฆฝ์ ์ผ๋ก ๊ธฐ๋ก๋ฉ๋๋ค.
// RestController ๋ด ๋ชจ๋ ๋ฉ์๋ ์ ์ฒด๋ฅผ ๋์์ผ๋ก ์ ์ฉ (within)
@Around("within(@org.springframework.web.bind.annotation.RestController *)")
public Object calculateExecutionTime(final ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// QueryStatistics ๊ฐ์ฒด์ API ๊ฒฝ๋ก ์ค์
queryStatistics.setApiUrl(
attributes.getRequest().getMethod() + " " + attributes.getRequest()
.getRequestURI());
}
Object result = joinPoint.proceed();
// ํต๊ณ ๊ฒฐ๊ณผ ๋ก๊ทธ๋ก ์ถ๋ ฅ
log.info("Query Statistics: URL = {}, Query Count = {}, Query Time = {}(ms)",
queryStatistics.getApiUrl(), queryStatistics.getQueryCounts(),
queryStatistics.getQueryTime());
return result;
}
์คํ ๊ฒฐ๊ณผ
์ด๋ ๊ฒ API๋ณ๋ก ๊ฒฝ๋ก, ์คํ๋๋ ์ฟผ๋ฆฌ ํ์์ ์ฟผ๋ฆฌ๋ค์ ์คํ ์๊ฐ์ด ๋ก๊ทธ๋ก ์ถ๋ ฅ๋์ต๋๋ค.
์ด์ ๋ ๋ ์ด์ ์ผ์ผ์ด ์ธ์ง ์์๋ ๋ฉ๋๋ค!
๋ํ N+1 ๋ฌธ์ ํด๊ฒฐ ์ ํ ์ฟผ๋ฆฌ ์คํ ์๊ฐ์ ๋น๊ตํ๋ฉด ์ฑ๋ฅ์ด ๋นจ๋ผ์ก๋์ง๋ฅผ ํ์คํ๊ฒ ๋น๊ตํ ์ ์์ต๋๋ค.



์ ์ฒด ๊ตฌํ ์ฝ๋
์ค์ ๋ก ์ฌ๋ฆฐ Github PR: ๐
@Component
@RequestScope
@Getter
@Profile("dev")
public class QueryStatistics {
private String apiUrl;
private Long queryCounts = 0L;
private Long queryTime = 0L;
public void setApiUrl(String url) {
this.apiUrl = url;
}
public void addQueryTime(Long queryTime) {
this.queryTime += queryTime;
}
public void addQueryCount() {
this.queryCounts++;
}
}
@Aspect
@Component
@Profile("dev")
@Slf4j
@RequiredArgsConstructor
public class QueryStatisticsAop {
private final QueryStatistics queryStatistics;
@Around("execution(* javax.sql.DataSource.getConnection())")
public Object getConnection(ProceedingJoinPoint joinPoint) throws Throwable {
if (RequestContextHolder.getRequestAttributes() == null) {
// HTTP ์์ฒญ์ด ์๋ ๊ฒฝ์ฐ AOP๋ฅผ ๊ฑด๋๋ฐ๊ณ ์๋๋๋ก ์คํ
return joinPoint.proceed();
}
Object connection = joinPoint.proceed();
return Proxy.newProxyInstance(
connection.getClass().getClassLoader(),
connection.getClass().getInterfaces(),
new ConnectionProxyHandler(connection, queryStatistics)
);
}
@Around("within(@org.springframework.web.bind.annotation.RestController *)")
public Object calculateExecutionTime(final ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
queryStatistics.setApiUrl(
attributes.getRequest().getMethod() + " " + attributes.getRequest()
.getRequestURI());
}
Object result = joinPoint.proceed();
log.info("Query Statistics: URL = {}, Query Count = {}, Query Time = {}(ms)",
queryStatistics.getApiUrl(), queryStatistics.getQueryCounts(),
queryStatistics.getQueryTime());
return result;
}
}
@Profile("dev")
@RequiredArgsConstructor
public class ConnectionProxyHandler implements InvocationHandler {
private final Object connection;
private final QueryStatistics queryStatistics;
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
Object invokeResult = method.invoke(connection, args);
if (isGeneratePrepareStatement(method)) {
return Proxy.newProxyInstance(
invokeResult.getClass().getClassLoader(),
invokeResult.getClass().getInterfaces(),
new PreparedStatementProxyHandler(invokeResult, queryStatistics)
);
}
return invokeResult;
}
private boolean isGeneratePrepareStatement(final Method method) {
return method.getName().contains("prepareStatement");
}
}
@Profile("dev")
@Slf4j
@RequiredArgsConstructor
public class PreparedStatementProxyHandler implements InvocationHandler {
private final Object preparedStatement;
private final QueryStatistics queryStatistics;
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
if (isExecuteQuery(method)) {
final Long beforeTime = System.currentTimeMillis();
final Object result = method.invoke(preparedStatement, args);
final Long afterTime = System.currentTimeMillis();
queryStatistics.addQueryCount();
queryStatistics.addQueryTime(afterTime - beforeTime);
return result;
}
return method.invoke(preparedStatement, args);
}
/**
* @param method
* @return ์ด ๋ฉ์๋๊ฐ execute, executeUpdate ๋ฑ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ๋ฉ์๋์ธ์ง ํ์ธํจ
*/
private boolean isExecuteQuery(final Method method) {
List<String> JDBC_QUERY_METHOD = List.of("executeQuery", "execute", "executeUpdate");
return JDBC_QUERY_METHOD.contains(method.getName());
}
}
์ฐธ๊ณ ์๋ฃ
์คํ๋ง์ ์น ์ค์ฝํ(Web Scope) ์ดํดํ๊ธฐ: Request Scope
[Spring]Request Scope๋ฅผ ์ฌ์ฉํด์ ๊น๋ํ๊ฒ ๋ก๊ทธ๋จ๊ธฐ๊ธฐ
[AOP] ์ฟผ๋ฆฌ ์นด์ดํฐ ์ ์๊ธฐ
AOP ํ์ฉ - ์ฟผ๋ฆฌ ์นด์ดํฐ ๋ง๋ค๊ธฐ
'๐ฟSpring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Spring] AOP(Aspect Oriented Programming) ๊ฐ๋ ์ดํดํ๊ธฐ (1) | 2025.09.15 |
|---|---|
| [Spring] ํํฐ(Filter)์ ์ธํฐ์ ํฐ(Interceptor) ์ฐจ์ด (0) | 2025.09.11 |
| [Spring] DI(Dependency Injection) ์์กด์ฑ ์ฃผ์ (4) | 2025.08.14 |
| [Spring] IoC ์ ์ด์ ์ญ์ (1) | 2025.08.12 |
| Spring์์ ์บ์ ์ฌ์ฉํ๊ธฐ: CaffeineCache, @Cacheable, @CachePut, @CacheEvict (1) | 2025.06.12 |
