1. AOP๊ฐ ๋ญ๊ฐ์? ๐ค
์ค์ํ ๋น์ ๋ก ์ดํดํ๊ธฐ
์ฌ๋ฌ๋ถ์ด ํ์ฌ CEO๋ผ๊ณ ์๊ฐํด๋ณด์ธ์. ํ๋ฃจ์ ์๋ง์ ์
๋ฌด๊ฐ ์์ง๋ง, ๋ชจ๋ ์
๋ฌด ์ ํ๋ก ํด์ผ ํ ์ผ๋ค์ด ์์ต๋๋ค.
graph TB
subgraph "CEO์ ํ๋ฃจ (AOP ์ ์ฉ ์ )"
A1[ํ์ ์ค๋น] --> A2[๋ณด์ ์ฒดํฌ] --> A3[์๊ฐ ๊ธฐ๋ก] --> A4[ํ์ ์งํ] --> A5[๊ฒฐ๊ณผ ๊ธฐ๋ก] --> A6[์ ๋ฆฌ]
B1[๊ณ์ฝ์ ๊ฒํ ์ค๋น] --> B2[๋ณด์ ์ฒดํฌ] --> B3[์๊ฐ ๊ธฐ๋ก] --> B4[๊ณ์ฝ์ ๊ฒํ ] --> B5[๊ฒฐ๊ณผ ๊ธฐ๋ก] --> B6[์ ๋ฆฌ]
C1[์์ฌ๊ฒฐ์ ์ค๋น] --> C2[๋ณด์ ์ฒดํฌ] --> C3[์๊ฐ ๊ธฐ๋ก] --> C4[์์ฌ๊ฒฐ์ ] --> C5[๊ฒฐ๊ณผ ๊ธฐ๋ก] --> C6[์ ๋ฆฌ]
end
style A2 fill:#ffebee
style A3 fill:#e8f5e8
style A5 fill:#e3f2fd
style B2 fill:#ffebee
style B3 fill:#e8f5e8
style B5 fill:#e3f2fd
style C2 fill:#ffebee
style C3 fill:#e8f5e8
style C5 fill:#e3f2fd
๋ฌธ์ ์ : ๋ณด์ ์ฒดํฌ, ์๊ฐ ๊ธฐ๋ก, ๊ฒฐ๊ณผ ๊ธฐ๋ก ๋ฑ์ด ๋ชจ๋ ์
๋ฌด์ ๋ฐ๋ณต๋จ ๐ต
AOP ์ ์ฉ ํ
graph TB
subgraph "CEO์ ํ๋ฃจ (AOP ์ ์ฉ ํ)"
D[๋น์/์์คํ
] -.-> E1[ํ์ ์งํ]
D -.-> E2[๊ณ์ฝ์ ๊ฒํ ]
D -.-> E3[์์ฌ๊ฒฐ์ ]
F[๋ณด์ ๋ด๋น์] -.-> D
G[์๊ฐ ๊ด๋ฆฌ์] -.-> D
H[๊ธฐ๋ก ๋ด๋น์] -.-> D
end
style D fill:#e8f5e8
style F fill:#ffebee
style G fill:#e3f2fd
style H fill:#fff3e0
ํด๊ฒฐ์ฑ
: ๋ถ๊ฐ ์
๋ฌด๋ค์ ์ ๋ด ์์คํ
์ด ์๋์ผ๋ก ์ฒ๋ฆฌ! ๐ฏ
2. ํ๋ก์ ํจํด ์ดํดํ๊ธฐ ๐ต๏ธโโ๏ธ
ํ๋ก์๋?
ํ๋ก์๋ ๋๋ฆฌ์ธ ์ญํ ์ ํฉ๋๋ค. ์ค์ ๊ฐ์ฒด ๋์ ์์ฒญ์ ๋ฐ์์ ๋ถ๊ฐ ์์
์ ์ํํ ํ ์ค์ ๊ฐ์ฒด์๊ฒ ์ ๋ฌํฉ๋๋ค.
sequenceDiagram
participant Client as ํด๋ผ์ด์ธํธ
participant Proxy as ํ๋ก์ ๊ฐ์ฒด
participant Target as ์ค์ ๊ฐ์ฒด
Client->>Proxy: ๋ฉ์๋ ํธ์ถ
Note over Proxy: ๋ถ๊ฐ ๊ธฐ๋ฅ ์คํ (๋ก๊น
, ๋ณด์ ๋ฑ)
Proxy->>Target: ์ค์ ๋ฉ์๋ ํธ์ถ
Target-->>Proxy: ๊ฒฐ๊ณผ ๋ฐํ
Note over Proxy: ๋ถ๊ฐ ๊ธฐ๋ฅ ์คํ (๊ฒฐ๊ณผ ๋ก๊น
๋ฑ)
Proxy-->>Client: ์ต์ข
๊ฒฐ๊ณผ ๋ฐํ
ํ๋ก์๊ฐ ์๋ค๋ฉด? ๐ฐ
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
| // ๐ฐ ๋ชจ๋ ๋ฉ์๋์ ์ค๋ณต ์ฝ๋๊ฐ ๋ค์ด๊ฐ
@Service
public class UserService {
public User findUser(Long id) {
// ๋ก๊น
System.out.println("์ฌ์ฉ์ ์กฐํ ์์");
long startTime = System.currentTimeMillis();
// ๋ณด์ ์ฒดํฌ
if (!SecurityContext.getCurrentUser().hasPermission("READ_USER")) {
throw new SecurityException("๊ถํ ์์");
}
// ํธ๋์ญ์
์์
TransactionManager.begin();
try {
// ๐ฏ ์ค์ ๋น์ฆ๋์ค ๋ก์ง (์ด๊ฒ๋ง ํ๊ณ ์ถ์๋๋ฐ...)
User user = userRepository.findById(id);
// ํธ๋์ญ์
์ปค๋ฐ
TransactionManager.commit();
// ์ฑ๋ฅ ๋ก๊น
long endTime = System.currentTimeMillis();
System.out.println("์คํ ์๊ฐ: " + (endTime - startTime) + "ms");
return user;
} catch (Exception e) {
TransactionManager.rollback();
throw e;
}
}
public void saveUser(User user) {
// ๐ฑ ๋ ๊ฐ์ ์ฝ๋ ๋ฐ๋ณต...
System.out.println("์ฌ์ฉ์ ์ ์ฅ ์์");
// ... ์ค๋ณต ์ฝ๋๋ค
}
}
|
3. JDK Dynamic Proxy ๐
ํน์ง
- ์ธํฐํ์ด์ค ๊ธฐ๋ฐ: ๋ฐ๋์ ์ธํฐํ์ด์ค๊ฐ ์์ด์ผ ํจ
- JDK ๋ด์ฅ: ์ถ๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ถํ์
- ๋ฆฌํ๋ ์
์ฌ์ฉ:
InvocationHandler
ํ์ฉ
๊ตฌ์กฐ
graph TB
subgraph "JDK Dynamic Proxy ๊ตฌ์กฐ"
A[<<interface>> UserService] --> B[UserServiceImpl]
A --> C[JDK Proxy]
C -.-> B
D[InvocationHandler] --> C
end
style A fill:#e3f2fd
style C fill:#ffebee
style D fill:#e8f5e8
์ค์ต ์์
1๋จ๊ณ: ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด ๋ง๋ค๊ธฐ
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
| // ๐ ์ธํฐํ์ด์ค (ํ์!)
public interface UserService {
User findUser(Long id);
void saveUser(User user);
}
// ๐๏ธ ์ค์ ๊ตฌํ์ฒด
public class UserServiceImpl implements UserService {
@Override
public User findUser(Long id) {
// ์์ํ ๋น์ฆ๋์ค ๋ก์ง๋ง!
System.out.println("DB์์ ์ฌ์ฉ์ ์กฐํ: " + id);
return new User(id, "ํ๊ธธ๋");
}
@Override
public void saveUser(User user) {
System.out.println("DB์ ์ฌ์ฉ์ ์ ์ฅ: " + user.getName());
}
}
// ๐ฆ ๊ฐ๋จํ User ํด๋์ค
public class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
// getter, setter, toString...
public Long getId() { return id; }
public String getName() { return name; }
public String toString() { return "User{id=" + id + ", name='" + name + "'}"; }
}
|
2๋จ๊ณ: InvocationHandler ๊ตฌํ
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
| import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target; // ์ค์ ๊ฐ์ฒด
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ๐ฏ Before: ๋ฉ์๋ ์คํ ์
System.out.println("๐ [JDK Proxy] " + method.getName() + " ๋ฉ์๋ ์์");
long startTime = System.currentTimeMillis();
try {
// ์ค์ ๋ฉ์๋ ์คํ
Object result = method.invoke(target, args);
// ๐ฏ After: ๋ฉ์๋ ์คํ ํ (์ฑ๊ณต)
long endTime = System.currentTimeMillis();
System.out.println("โ
[JDK Proxy] " + method.getName() + " ์ฑ๊ณต (์คํ์๊ฐ: " + (endTime - startTime) + "ms)");
return result;
} catch (Exception e) {
// ๐ฏ After: ๋ฉ์๋ ์คํ ํ (์คํจ)
System.out.println("โ [JDK Proxy] " + method.getName() + " ์คํจ: " + e.getMessage());
throw e;
}
}
}
|
3๋จ๊ณ: ํ๋ก์ ์์ฑ ๋ฐ ์คํ
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
| import java.lang.reflect.Proxy;
public class JdkProxyExample {
public static void main(String[] args) {
// 1. ์ค์ ๊ฐ์ฒด ์์ฑ
UserService target = new UserServiceImpl();
// 2. ํ๋ก์ ์์ฑ
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(), // ํด๋์ค๋ก๋
new Class[]{UserService.class}, // ์ธํฐํ์ด์ค๋ค
new LoggingInvocationHandler(target) // ํธ๋ค๋ฌ
);
// 3. ํ๋ก์๋ฅผ ํตํ ๋ฉ์๋ ํธ์ถ
System.out.println("=== JDK Dynamic Proxy ํ
์คํธ ===");
User user = proxy.findUser(1L);
System.out.println("์กฐํ ๊ฒฐ๊ณผ: " + user);
System.out.println();
proxy.saveUser(new User(2L, "๊น์ฒ ์"));
// 4. ํ๋ก์ ํ์
ํ์ธ
System.out.println("\n=== ํ์
ํ์ธ ===");
System.out.println("ํ๋ก์ ํด๋์ค: " + proxy.getClass().getName());
System.out.println("UserService ์ธํฐํ์ด์ค? " + (proxy instanceof UserService));
System.out.println("UserServiceImpl ํด๋์ค? " + (proxy instanceof UserServiceImpl));
}
}
|
์คํ ๊ฒฐ๊ณผ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| === JDK Dynamic Proxy ํ
์คํธ ===
๐ [JDK Proxy] findUser ๋ฉ์๋ ์์
DB์์ ์ฌ์ฉ์ ์กฐํ: 1
โ
[JDK Proxy] findUser ์ฑ๊ณต (์คํ์๊ฐ: 15ms)
์กฐํ ๊ฒฐ๊ณผ: User{id=1, name='ํ๊ธธ๋'}
๐ [JDK Proxy] saveUser ๋ฉ์๋ ์์
DB์ ์ฌ์ฉ์ ์ ์ฅ: ๊น์ฒ ์
โ
[JDK Proxy] saveUser ์ฑ๊ณต (์คํ์๊ฐ: 8ms)
=== ํ์
ํ์ธ ===
ํ๋ก์ ํด๋์ค: com.sun.proxy.$Proxy0
UserService ์ธํฐํ์ด์ค? true
UserServiceImpl ํด๋์ค? false
|
4. CGLIB Proxy โก
ํน์ง
- ํด๋์ค ๊ธฐ๋ฐ: ์ธํฐํ์ด์ค ์์ด๋ ํ๋ก์ ์์ฑ ๊ฐ๋ฅ
- ์์ ๋ฐฉ์: ๋์ ํด๋์ค๋ฅผ ์์๋ฐ์ ํ๋ก์ ์์ฑ
- ๋ฐ์ดํธ์ฝ๋ ์กฐ์: ๋ ๋น ๋ฅธ ์ฑ๋ฅ
๊ตฌ์กฐ
graph TB
subgraph "CGLIB Proxy ๊ตฌ์กฐ"
A[OrderService] --> B[CGLIB Proxy]
B -.-> A
C[MethodInterceptor] --> B
end
style A fill:#e8f5e8
style B fill:#ffebee
style C fill:#e3f2fd
์ค์ต ์์
1๋จ๊ณ: ๊ตฌ์ฒด ํด๋์ค ๋ง๋ค๊ธฐ (์ธํฐํ์ด์ค ์์!)
1
2
3
4
5
6
7
8
9
10
11
12
| // ๐๏ธ ๊ตฌ์ฒด ํด๋์ค๋ง ์์ด๋ OK!
public class OrderService {
public void createOrder(String orderInfo) {
System.out.println("์ฃผ๋ฌธ ์์ฑ: " + orderInfo);
}
public String getOrderStatus(String orderId) {
System.out.println("์ฃผ๋ฌธ ์ํ ์กฐํ: " + orderId);
return "์ฃผ๋ฌธ ์ํ: ์ฒ๋ฆฌ์ค";
}
}
|
2๋จ๊ณ: MethodInterceptor ๊ตฌํ
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
| import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class LoggingMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// ๐ฏ Before: ๋ฉ์๋ ์คํ ์
System.out.println("โก [CGLIB] " + method.getName() + " ๋ฉ์๋ ์์");
long startTime = System.currentTimeMillis();
try {
// ์ค์ ๋ฉ์๋ ์คํ (๋ ํจ์จ์ ์ธ ๋ฐฉ๋ฒ)
Object result = proxy.invokeSuper(obj, args);
// ๐ฏ After: ๋ฉ์๋ ์คํ ํ (์ฑ๊ณต)
long endTime = System.currentTimeMillis();
System.out.println("โ
[CGLIB] " + method.getName() + " ์ฑ๊ณต (์คํ์๊ฐ: " + (endTime - startTime) + "ms)");
return result;
} catch (Exception e) {
// ๐ฏ After: ๋ฉ์๋ ์คํ ํ (์คํจ)
System.out.println("โ [CGLIB] " + method.getName() + " ์คํจ: " + e.getMessage());
throw e;
}
}
}
|
3๋จ๊ณ: ์์กด์ฑ ์ถ๊ฐ (build.gradle)
1
2
3
| dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
}
|
4๋จ๊ณ: ํ๋ก์ ์์ฑ ๋ฐ ์คํ
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
| import org.springframework.cglib.proxy.Enhancer;
public class CglibProxyExample {
public static void main(String[] args) {
// 1. CGLIB Enhancer ์์ฑ
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class); // ์์ํ ํด๋์ค
enhancer.setCallback(new LoggingMethodInterceptor()); // ์ธํฐ์
ํฐ
// 2. ํ๋ก์ ์์ฑ
OrderService proxy = (OrderService) enhancer.create();
// 3. ํ๋ก์๋ฅผ ํตํ ๋ฉ์๋ ํธ์ถ
System.out.println("=== CGLIB Proxy ํ
์คํธ ===");
proxy.createOrder("๋งฅ๋ถ ํ๋ก ๊ตฌ๋งค");
System.out.println();
String status = proxy.getOrderStatus("ORDER-001");
System.out.println("๊ฒฐ๊ณผ: " + status);
// 4. ํ๋ก์ ํ์
ํ์ธ
System.out.println("\n=== ํ์
ํ์ธ ===");
System.out.println("ํ๋ก์ ํด๋์ค: " + proxy.getClass().getName());
System.out.println("OrderService ํด๋์ค? " + (proxy instanceof OrderService));
}
}
|
์คํ ๊ฒฐ๊ณผ:
1
2
3
4
5
6
7
8
9
10
11
12
13
| === CGLIB Proxy ํ
์คํธ ===
โก [CGLIB] createOrder ๋ฉ์๋ ์์
์ฃผ๋ฌธ ์์ฑ: ๋งฅ๋ถ ํ๋ก ๊ตฌ๋งค
โ
[CGLIB] createOrder ์ฑ๊ณต (์คํ์๊ฐ: 12ms)
โก [CGLIB] getOrderStatus ๋ฉ์๋ ์์
์ฃผ๋ฌธ ์ํ ์กฐํ: ORDER-001
โ
[CGLIB] getOrderStatus ์ฑ๊ณต (์คํ์๊ฐ: 5ms)
๊ฒฐ๊ณผ: ์ฃผ๋ฌธ ์ํ: ์ฒ๋ฆฌ์ค
=== ํ์
ํ์ธ ===
ํ๋ก์ ํด๋์ค: com.example.OrderService$$EnhancerByCGLIB$$12345678
OrderService ํด๋์ค? true
|
5. ๋ ๋ฐฉ์ ๋น๊ต โ๏ธ
๋น๊ตํ
ํน์ง |
JDK Dynamic Proxy |
CGLIB Proxy |
ํ์ ์กฐ๊ฑด |
์ธํฐํ์ด์ค ํ์ โ
|
๊ตฌ์ฒด ํด๋์ค๋ง ์์ด๋ OK โ
|
์์ฑ ๋ฐฉ์ |
์ธํฐํ์ด์ค ๊ตฌํ |
ํด๋์ค ์์ |
์ฑ๋ฅ |
๋๋ฆผ (๋ฆฌํ๋ ์
) |
๋น ๋ฆ (๋ฐ์ดํธ์ฝ๋) |
์์กด์ฑ |
JDK ๋ด์ฅ |
์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ |
ํ์
์บ์คํ
|
์ธํฐํ์ด์ค๋ง ๊ฐ๋ฅ |
๊ตฌ์ฒด ํด๋์ค ๊ฐ๋ฅ |
final ์ ํ |
์์ |
final ํด๋์ค/๋ฉ์๋ ๋ถ๊ฐ |
์ ํ ๊ธฐ์ค
flowchart TD
A[ํ๋ก์ ์์ฑ ํ์] --> B{์ธํฐํ์ด์ค ์์?}
B -->|Yes| C{์ฑ๋ฅ์ด ์ค์?}
B -->|No| D[CGLIB ์ ํ]
C -->|Yes| E[CGLIB ๊ถ์ฅ]
C -->|No| F[JDK Proxy ๊ถ์ฅ]
G[Spring Boot] --> H[๊ธฐ๋ณธ์ ์ผ๋ก CGLIB ์ฌ์ฉ]
style D fill:#e8f5e8
style E fill:#e8f5e8
style F fill:#e3f2fd
style H fill:#fff3e0
6. Spring AOP ์ค์ต ๐ฑ
ํ๋ก์ ํธ ์ค์
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
|
application.yml
1
2
3
4
5
6
7
| spring:
aop:
proxy-target-class: true # CGLIB ์ฌ์ฉ ๊ฐ์
logging:
level:
com.example: DEBUG
|
๊ธฐ๋ณธ ์ค์
1
2
3
4
5
6
7
8
| // ๐ Application.java
@SpringBootApplication
@EnableAspectJAutoProxy // AOP ํ์ฑํ
public class AopDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AopDemoApplication.class, args);
}
}
|
7. ๊ณตํต ๊ธฐ๋ฅ ๊ตฌํ ๐ง
1. ๋ก๊น
Aspect
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
| @Aspect
@Component
@Slf4j
public class LoggingAspect {
// ๐ฏ ๋ชจ๋ Service ํด๋์ค์ public ๋ฉ์๋์ ์ ์ฉ
@Pointcut("execution(public * com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
log.info("๐ [{}] {} ์์ - ํ๋ผ๋ฏธํฐ: {}", className, methodName, Arrays.toString(args));
}
@AfterReturning(value = "serviceLayer()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.info("โ
[{}] {} ์ฑ๊ณต - ๊ฒฐ๊ณผ: {}", className, methodName, result);
}
@AfterThrowing(value = "serviceLayer()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.error("โ [{}] {} ์คํจ - ์๋ฌ: {}", className, methodName, ex.getMessage());
}
}
|
2. ์ฑ๋ฅ ์ธก์ Aspect
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
| @Aspect
@Component
@Slf4j
public class PerformanceAspect {
// ๐ฏ @PerformanceMonitor ์ด๋
ธํ
์ด์
์ด ๋ถ์ ๋ฉ์๋์ ์ ์ฉ
@Around("@annotation(PerformanceMonitor)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// ์ฑ๋ฅ ์๊ณ์น ์ฒดํฌ (100ms)
if (executionTime > 100) {
log.warn("๐ [{}] {} ๋๋ฆฐ ์คํ ๊ฐ์ง! {}ms", className, methodName, executionTime);
} else {
log.info("โก [{}] {} ์คํ ์๋ฃ: {}ms", className, methodName, executionTime);
}
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("๐ฅ [{}] {} ์คํ ์ค ์ค๋ฅ ({}ms): {}", className, methodName,
endTime - startTime, e.getMessage());
throw e;
}
}
}
// ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ์ด๋
ธํ
์ด์
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceMonitor {
}
|
3. ๋ณด์ Aspect
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
43
44
| @Aspect
@Component
@Slf4j
public class SecurityAspect {
@Before("@annotation(RequireAuth)")
public void checkAuthentication(JoinPoint joinPoint, RequireAuth requireAuth) {
// ํ์ฌ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ (์ค์ ๋ก๋ SecurityContext์์)
String currentUser = getCurrentUser();
if (currentUser == null) {
log.error("๐ ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์ ์ ๊ทผ ์๋: {}",
joinPoint.getSignature().getName());
throw new SecurityException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
}
// ๊ถํ ์ฒดํฌ
String requiredRole = requireAuth.role();
if (!hasRole(currentUser, requiredRole)) {
log.error("๐ซ ๊ถํ ์๋ ์ ๊ทผ ์๋: ์ฌ์ฉ์={}, ํ์๊ถํ={}", currentUser, requiredRole);
throw new SecurityException("๊ถํ์ด ๋ถ์กฑํฉ๋๋ค. ํ์ ๊ถํ: " + requiredRole);
}
log.info("๐ ๋ณด์ ๊ฒ์ฆ ํต๊ณผ: ์ฌ์ฉ์={}, ๋ฉ์๋={}", currentUser,
joinPoint.getSignature().getName());
}
private String getCurrentUser() {
// ์ค์ ๋ก๋ SecurityContextHolder.getContext().getAuthentication()
return "testUser"; // ํ
์คํธ์ฉ
}
private boolean hasRole(String user, String role) {
// ์ค์ ๋ก๋ DB๋ ์บ์์์ ๊ถํ ํ์ธ
return "ADMIN".equals(role) && "testUser".equals(user);
}
}
// ๋ณด์ ์ด๋
ธํ
์ด์
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAuth {
String role() default "USER";
}
|
8. ์ข
ํฉ ์ค์ต ๐ฏ
๋น์ฆ๋์ค ์๋น์ค ํด๋์ค
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| @Service
public class ProductService {
@PerformanceMonitor
@RequireAuth(role = "USER")
public Product findProduct(Long productId) {
log.info("์ํ ์กฐํ ๋ก์ง ์คํ");
// ์๋์ ์ผ๋ก ์ง์ฐ ์๋ฎฌ๋ ์ด์
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new Product(productId, "๋งฅ๋ถ ํ๋ก", new BigDecimal("2500000"));
}
@PerformanceMonitor
@RequireAuth(role = "ADMIN")
public void createProduct(Product product) {
log.info("์ํ ์์ฑ ๋ก์ง ์คํ");
// ์๋์ ์ผ๋ก ๋๋ฆฐ ์์
์๋ฎฌ๋ ์ด์
try {
Thread.sleep(150);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("์ํ ์์ฑ ์๋ฃ: {}", product.getName());
}
public void deleteProduct(Long productId) {
log.info("์ํ ์ญ์ ๋ก์ง ์คํ: {}", productId);
// ์์ธ ๋ฐ์ ์๋ฎฌ๋ ์ด์
if (productId == 999L) {
throw new IllegalArgumentException("์ญ์ ํ ์ ์๋ ์ํ์
๋๋ค.");
}
}
}
// Product ์ํฐํฐ
public class Product {
private Long id;
private String name;
private BigDecimal price;
public Product(Long id, String name, BigDecimal price) {
this.id = id;
this.name = name;
this.price = price;
}
// getter, setter, toString
public Long getId() { return id; }
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + "}";
}
}
|
ํ
์คํธ ์ปจํธ๋กค๋ฌ
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
| @RestController
@RequestMapping("/api")
public class TestController {
@Autowired
private ProductService productService;
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findProduct(id);
return ResponseEntity.ok(product);
}
@PostMapping("/products")
public ResponseEntity<String> createProduct(@RequestBody Product product) {
productService.createProduct(product);
return ResponseEntity.ok("์ํ์ด ์์ฑ๋์์ต๋๋ค.");
}
@DeleteMapping("/products/{id}")
public ResponseEntity<String> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.ok("์ํ์ด ์ญ์ ๋์์ต๋๋ค.");
}
}
|
์คํ ๊ฒฐ๊ณผ ํ์ธ
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
| @Component
@Slf4j
public class AopTestRunner implements CommandLineRunner {
@Autowired
private ProductService productService;
@Override
public void run(String... args) {
log.info("=== AOP ์ข
ํฉ ํ
์คํธ ์์ ===");
try {
// 1. ์ ์์ ์ธ ์ํ ์กฐํ (๋น ๋ฅธ ์คํ)
log.info("\n--- ํ
์คํธ 1: ์ํ ์กฐํ ---");
Product product = productService.findProduct(1L);
// 2. ๊ด๋ฆฌ์ ๊ถํ์ด ํ์ํ ์ํ ์์ฑ (๋๋ฆฐ ์คํ)
log.info("\n--- ํ
์คํธ 2: ์ํ ์์ฑ ---");
productService.createProduct(new Product(2L, "์์ดํจ๋", new BigDecimal("800000")));
// 3. ์์ธ ๋ฐ์ ์ผ์ด์ค
log.info("\n--- ํ
์คํธ 3: ์์ธ ๋ฐ์ ---");
productService.deleteProduct(999L);
} catch (Exception e) {
log.info("์์๋ ์์ธ ๋ฐ์: {}", e.getMessage());
}
log.info("=== AOP ์ข
ํฉ ํ
์คํธ ์๋ฃ ===");
}
}
|
์คํ ๊ฒฐ๊ณผ ์์:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| === AOP ์ข
ํฉ ํ
์คํธ ์์ ===
--- ํ
์คํธ 1: ์ํ ์กฐํ ---
๐ ๋ณด์ ๊ฒ์ฆ ํต๊ณผ: ์ฌ์ฉ์=testUser, ๋ฉ์๋=findProduct
๐ [ProductService] findProduct ์์ - ํ๋ผ๋ฏธํฐ: [1]
์ํ ์กฐํ ๋ก์ง ์คํ
โก [ProductService] findProduct ์คํ ์๋ฃ: 52ms
โ
[ProductService] findProduct ์ฑ๊ณต - ๊ฒฐ๊ณผ: Product{id=1, name='๋งฅ๋ถ ํ๋ก', price=2500000}
--- ํ
์คํธ 2: ์ํ ์์ฑ ---
๐ ๋ณด์ ๊ฒ์ฆ ํต๊ณผ: ์ฌ์ฉ์=testUser, ๋ฉ์๋=createProduct
๐ [ProductService] createProduct ์์ - ํ๋ผ๋ฏธํฐ: [Product{id=2, name='์์ดํจ๋', price=800000}]
์ํ ์์ฑ ๋ก์ง ์คํ
์ํ ์์ฑ ์๋ฃ: ์์ดํจ๋
๐ [ProductService] createProduct ๋๋ฆฐ ์คํ ๊ฐ์ง! 152ms
โ
[ProductService] createProduct ์ฑ๊ณต - ๊ฒฐ๊ณผ: null
--- ํ
์คํธ 3: ์์ธ ๋ฐ์ ---
๐ [ProductService] deleteProduct ์์ - ํ๋ผ๋ฏธํฐ: [999]
์ํ ์ญ์ ๋ก์ง ์คํ: 999
โ [ProductService] deleteProduct ์คํจ - ์๋ฌ: ์ญ์ ํ ์ ์๋ ์ํ์
๋๋ค.
์์๋ ์์ธ ๋ฐ์: ์ญ์ ํ ์ ์๋ ์ํ์
๋๋ค.
=== AOP ์ข
ํฉ ํ
์คํธ ์๋ฃ ===
|
๐ธ @Transactional์ด ์ ๊ฑธ๋ฆฌ๋ 4๊ฐ์ง ์ผ์ด์ค
๐ค Spring Boot vs ์๋ AOP ์ค์ ์ฐจ์ด์
Spring Boot (์๋ ์ค์ )
1
2
3
4
5
6
| @SpringBootApplication // ์ด๊ฒ๋ง์ผ๋ก @Transactional ๋์!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
|
์๋ AOP ์ค์ (Spring Boot ์๋ ๋)
1
2
3
4
5
6
| @Configuration
@EnableTransactionManagement // ์๋์ผ๋ก ์ถ๊ฐํด์ผ ํจ
@EnableAspectJAutoProxy
public class Config {
// ํธ๋์ญ์
๋งค๋์ ๋ฑ ์ถ๊ฐ ์ค์ ํ์
}
|
๐ ๏ธ ํ๋ก์ ํธ ์ค์
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
| plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
|
application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
| spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
open-in-view: false
show-sql: true
hibernate:
ddl-auto: create-drop
logging:
level:
org.springframework.transaction: DEBUG # ํธ๋์ญ์
๋ก๊ทธ ํ์ธ
|
๐ ๊ธฐ๋ณธ ์ฝ๋
User ์ํฐํฐ
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Entity
@Getter @Setter @NoArgsConstructor
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
}
|
UserRepository
1
2
3
| @Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
|
โ ์ผ์ด์ค 1: ํ๋ก์ ์ธ๋ถ ํธ์ถ (Self-Invocation)
๋ฌธ์ ๊ฐ ๋๋ ์ฝ๋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @Slf4j
@RequiredArgsConstructor
@Service
public class Case1Service {
private final UserRepository userRepository;
public void selfInvocationExample() {
this.saveUserInternal("user1", "user1@test.com");
}
@Transactional
public void saveUserInternal(String name, String email) {
User user = new User(name, email);
User savedUser= userRepository.save(user);
savedUser.setName("Case1Service");
}
}
|
โ ์ผ์ด์ค 2: Private ๋ฉ์๋
๋ฌธ์ ๊ฐ ๋๋ ์ฝ๋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @Service
@RequiredArgsConstructor
@Slf4j
public class Case2Service {
private final UserRepository userRepository;
public void privateMethodExample() {
this.updateUserPrivate();
throw new RuntimeException("private Exception");
}
@Transactional // โ private ๋ฉ์๋๋ ํ๋ก์ ๋ถ๊ฐ!
private void updateUserPrivate() {
User newUser = new User("๋กค๋ฐฑ๋์ผํ ์ ์ ", "temp@test.com");
User savedUser= userRepository.save(newUser);
savedUser.setName("Private");
}
}
|
๐คทโโ๏ธ ์ ์๋๋์?
- ํ๋ก์๋ public ๋ฉ์๋๋ง ๊ฐ๋ก์ฑ ์ ์์
- private ๋ฉ์๋๋ ์ธ๋ถ์์ ์ ๊ทผ ๋ถ๊ฐํ๋ฏ๋ก ํ๋ก์ ์์ฑ ๋ถ๊ฐ
โ ์ผ์ด์ค 3: Checked Exception
๋ฌธ์ ๊ฐ ๋๋ ์ฝ๋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Slf4j
@RequiredArgsConstructor
@Service
public class Case3Service {
private final UserRepository userRepository;
@Transactional
public void checkedExceptionExample() throws Exception {
User newUser = new User("๋กค๋ฐฑ๋์ผํ ์ ์ ", "temp@test.com");
userRepository.save(newUser);
throw new Exception("์กธ๋ฆฌ๋ค.");
}
}
|
๐คทโโ๏ธ ์ ์๋๋์?
- Spring ๊ธฐ๋ณธ ์ค์ : RuntimeException๋ง ๋กค๋ฐฑ
- Checked Exception(Exception, IOException ๋ฑ)์ ๋กค๋ฐฑ ์๋จ
- ๋น์ฆ๋์ค ๋ก์ง์์ ๋ณต๊ตฌ ๊ฐ๋ฅํ ์์ธ๋ก ๊ฐ์ฃผ
โ ์ผ์ด์ค 4: ํ๋ก์ ๋ด๋ถ์์ ์๊ธฐ ์์ ๋ฉ์๋ ํธ์ถ
๋ฌธ์ ๊ฐ ๋๋ ์ฝ๋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @Service
@RequiredArgsConstructor
@Slf4j
public class Case4Service {
private final UserRepository userRepository;
@Transactional
public void createMultipleUsers() {
createSingleUser("ํ๊ธธ๋", "hong@test.com");
createSingleUser("๊น์ฒ ์", "invalid-email");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSingleUser(String name, String email) throws RuntimeException {
if (!email.contains("@")) {
throw new RuntimeException("์๋ชป๋ ์ด๋ฉ์ผ");
}
User user = new User(name, email);
userRepository.save(user);
}
}
|
๐คทโโ๏ธ ์ ์๋๋์?
createSingleUser()
ํธ์ถ์ด ํ๋ก์๋ฅผ ๊ฑฐ์น์ง ์์
- ๋ด๋ถ์ ์ผ๋ก๋ ๋ชจ๋ ๊ฐ์ ํธ๋์ญ์
์์ ์คํ๋จ
- ๊ฐ๋ณ ๋ฉ์๋์
@Transactional
์ค์ ์ด ๋ฌด์๋จ
โ
ํด๊ฒฐ ๋ฐฉ๋ฒ
๋ฐฉ๋ฒ 1: ๋ณ๋ ์๋น์ค ๋ถ๋ฆฌ
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
| @Service
public class UserService {
@Autowired
private UserCreationService userCreationService;
@Transactional
public void createMultipleUsers() {
userCreationService.createSingleUser("ํ๊ธธ๋", "hong@test.com");
userCreationService.createSingleUser("๊น์ฒ ์", "kim@test.com");
}
}
@Service
public class UserCreationService {
@Autowired
private UserRepository userRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW) // ์ ํธ๋์ญ์
public void createSingleUser(String name, String email) {
if (!email.contains("@")) {
throw new RuntimeException("์๋ชป๋ ์ด๋ฉ์ผ");
}
User user = new User(name, email);
userRepository.save(user);
}
}
|
๐ ์ ๋ฆฌ
์ผ์ด์ค |
๋ฌธ์ ์ |
ํด๊ฒฐ๋ฐฉ๋ฒ |
Self-Invocation |
๊ฐ์ ํด๋์ค ๋ฉ์๋ ํธ์ถ |
๋ณ๋ ์๋น์ค๋ก ๋ถ๋ฆฌ |
Private ๋ฉ์๋ |
ํ๋ก์ ์์ฑ ๋ถ๊ฐ |
public์ผ๋ก ๋ณ๊ฒฝ |
Checked Exception |
๊ธฐ๋ณธ์ ์ผ๋ก ๋กค๋ฐฑ ์๋จ |
rollbackFor ๋ช
์ |
๋ด๋ถ ๋ฉ์๋ ํธ์ถ |
ํ๋ก์ ์ฐํ |
์๋น์ค ๋ถ๋ฆฌ + ์ ํ ์ค์ |
๐ฏ ํต์ฌ ํฌ์ธํธ
- Spring Boot๋ ์๋์ผ๋ก @Transactional ์ง์ - ๋ณ๋ AOP ์ค์ ๋ถํ์
- ํ๋ก์ ๊ธฐ๋ฐ์ด๋ฏ๋ก ์ธ๋ถ์์ ํธ์ถ๋์ด์ผ ํจ
- Public ๋ฉ์๋๋ง ๊ฐ๋ฅ
- RuntimeException๋ง ๊ธฐ๋ณธ ๋กค๋ฐฑ
- ๋ณต์กํ ํธ๋์ญ์
๋ก์ง์ ์๋น์ค๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ์ด ์ข์
๋๊ธ๋จ๊ธฐ๊ธฐ