[Spring] AOP ๊ธฐ๋ฐ˜ API ์š”์ฒญ๋ณ„ ์ฟผ๋ฆฌ ํšŸ์ˆ˜ / ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ • ์นด์šดํ„ฐ ๊ตฌํ˜„

2025. 11. 2. 14:33ยท๐ŸŒฟSpring

๊ตฌํ˜„ ๊ณ„๊ธฐ

์ด์ „์— ์ง„ํ–‰ํ•œ "๋ชจ๋‘์˜ ๋‰ด์Šค"๋ผ๋Š” ํ”„๋กœ์ ํŠธ์— ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋“ฑ ์กฐํšŒ 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๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

 


์„ค๊ณ„ ๊ณผ์ •

ํ•ต์‹ฌ ๋ชฉํ‘œ๋Š” ๋‹ค์Œ ์„ธ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

  1. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ์ž๋™ ์ธก์ •ํ•  ๊ฒƒ
  2. API ์š”์ฒญ ๋‹จ์œ„๋งˆ๋‹ค ๋…๋ฆฝ์ ์œผ๋กœ ์ธก์ •ํ•  ๊ฒƒ
  3. ํ•˜๋‚˜์˜ 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์™€ ํ†ต์‹ ํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ๊ณผ์ •์„

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์š”์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. `DataSource` ๊ฐ์ฒด์—์„œ `Connection` ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค
  2. `Connection` ๊ฐ์ฒด์—์„œ `PreparedStatement` ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
  3. `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 ํ™œ์šฉ - ์ฟผ๋ฆฌ ์นด์šดํ„ฐ ๋งŒ๋“ค๊ธฐ

ํ•˜๋‚˜์˜ ์š”์ฒญ์— ๋‚˜๊ฐ€๋Š” ์ฟผ๋ฆฌ ๊ฐœ์ˆ˜๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์„ธ์–ด๋ณด์ž (Hibernate StatementInspector, Spring 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
'๐ŸŒฟSpring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [Spring] AOP(Aspect Oriented Programming) ๊ฐœ๋… ์ดํ•ดํ•˜๊ธฐ
  • [Spring] ํ•„ํ„ฐ(Filter)์™€ ์ธํ„ฐ์…‰ํ„ฐ(Interceptor) ์ฐจ์ด
  • [Spring] DI(Dependency Injection) ์˜์กด์„ฑ ์ฃผ์ž…
  • [Spring] IoC ์ œ์–ด์˜ ์—ญ์ „
์†Œ์˜ ๐Ÿ€
์†Œ์˜ ๐Ÿ€
Hello World โœจ
  • ์†Œ์˜ ๐Ÿ€
    Soyoung's Dev Lab
    ์†Œ์˜ ๐Ÿ€
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
  • ๊ธ€์“ฐ๊ธฐ ๊ด€๋ฆฌ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (79)
      • ๐Ÿ“ข ๊ฒŒ์‹œํŒ (0)
      • ๐ŸŒฟSpring (20)
      • โ˜•Java (7)
        • ์ฝ”๋”ฉํ…Œ์ŠคํŠธ (7)
      • โš™๏ธ CS (26)
        • ๐Ÿ›œ ๋„คํŠธ์›Œํฌ (5)
        • ๐Ÿ“Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (8)
        • ๐Ÿ–ฒ๏ธ์šด์˜์ฒด์ œ (9)
        • ๐Ÿ“š ์ž๋ฃŒ๊ตฌ์กฐ & ์•Œ๊ณ ๋ฆฌ์ฆ˜ (4)
      • ๐Ÿ“ค ๋ฐฐํฌ (4)
        • Docker (4)
        • AWS (0)
      • ๐Ÿ“ฐ ๊ธฐํƒ€ ๊ฐœ๋ฐœ ์ž๋ฃŒ (12)
      • ๐Ÿ–ฅ๏ธ ํ”„๋กœ์ ํŠธ (0)
      • ๐Ÿ‘ฉ‍๐Ÿ’ป ํ™œ๋™ & ํ›„๊ธฐ (1)
      • ๐Ÿต ์ด์•ผ๊ธฐ (2)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํƒœ๊ทธ
  • ๋งํฌ

    • github
    • velog
  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    ๊ฐ์ฒด์ง€ํ–ฅํ”„๋กœ๊ทธ๋ž˜๋ฐ
    ์„œ๋ฒ„
    Java
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    ๊ฐœ๋ฐœ
    Spring Security
    Spring
    GIT
    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
    ์šด์˜์ฒด์ œ
    ๊ธฐ์ˆ  ๋ฉด์ ‘
    ๋„คํŠธ์›Œํฌ
    ์ž๋ฃŒ๊ตฌ์กฐ
    docker
    ์ฝ”๋“œ์ž‡ ์Šคํ”„๋ฆฐํŠธ
    ์œ„ํด๋ฆฌ ํŽ˜์ดํผ
    ๋ฐฐํฌ
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.3
์†Œ์˜ ๐Ÿ€
[Spring] AOP ๊ธฐ๋ฐ˜ API ์š”์ฒญ๋ณ„ ์ฟผ๋ฆฌ ํšŸ์ˆ˜ / ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ • ์นด์šดํ„ฐ ๊ตฌํ˜„
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”