Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JDBC 라이브러리 구현하기 - 4단계] 페드로(류형욱) 미션 제출합니다. #918

Merged
merged 9 commits into from
Oct 17, 2024
2 changes: 1 addition & 1 deletion study/src/test/java/aop/stage0/Stage0Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ void testTransactionRollback() {
private @NotNull Object createUserServiceProxy(AppUserService appUserService) {
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{UserService.class},
new Class[]{UserService.class}, // JDK Dynamic Proxy는 인터페이스 기반 프록시만 생성 가능
new TransactionHandler(platformTransactionManager, appUserService)
);
}
Expand Down
20 changes: 15 additions & 5 deletions study/src/test/java/aop/stage1/Stage1Test.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package aop.stage1;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import aop.DataAccessException;
import aop.StubUserHistoryDao;
import aop.domain.User;
Expand All @@ -9,13 +12,11 @@
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.PlatformTransactionManager;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Stage1Test {

Expand All @@ -33,15 +34,23 @@ class Stage1Test {
@Autowired
private PlatformTransactionManager platformTransactionManager;

private final ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();

@BeforeEach
void setUp() {
final var user = new User("gugu", "password", "hkkang@woowahan.com");
userDao.insert(user);
proxyFactoryBean.setProxyTargetClass(true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링은 JDK dynamic proxy를 default로 둔 반면에 스프링 부트는 CGLib Proxy를 default로 둔 이유는 무엇일까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서로 지향하는 관점이 조금 다르다고 생각합니다. 스프링은 스프링 부트에 비해 등장 시기가 (당연히) 이르기도 하고, 제 기준에서 스프링은 조금 날 것의 기술이예요. CGLib은 태생적으로 리플렉션 기반의 Dynamic Proxy보다 성능이 좋을 수 밖에 없는데, CGLib 등장 초기에는 성능 문제가 있어 오히려 다이나믹 프록시가 성능이 더 좋았다고 해요. 이러한 이유 때문에 Spring Framework에서는 JDK Dynamix Proxy가 기본값이었지 않나 싶어요. DI 컨테이너의 특성상 인터페이스를 권장하는 방향으로 기본값이 설정됐을 수도 있겠네요.

반면 SpringBoot의 경우에는 'Spring Framework의 쉬운 사용'을 컨셉으로 하고 있어요.
JDK 프록시는 다음과 같은 불편을 안고 있습니다.

  • 인터페이스 생성을 강제
  • 빈 주입 시 인터페이스로만 주입받을 수 있음 (구체 클래스 주입 불가)

시간이 지나면서 CGLib방식이 성능과 안정성 면에서 많은 개선이 있었고, 이러한 불편을 해소할 수 있다면 정책을 변경해도 된다고 판단했을 것 같아요. Spring 3.2부터 CGLib이 내장되면서 더 이상 외부 라이브러리를 사용하지 않아도 된다는 점도 선택에 영향을 미쳤을 것 같네요.

우주는 이유가 뭐라고 생각하시나요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 첫번째 이유는 쉽게 사용하기 위함이라고 생각합니다!
그동안 CGLIb를 내장하지 못했던 이유는 스프링 공식 문서에 나와있는 생성자를 2번 호출하는 문제 때문이라고 생각해요.
Objenesis 라이브러리의 등장으로 위 문제를 해결하고 그 때부터 CGLib를 default로 설정한 것이라 추측합니다!

As of Spring 4.0, the constructor of your proxied object is NOT called twice anymore, since the CGLIB proxy instance is created through Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

Spring Docs: Proxying Mechanisms

proxyFactoryBean.addAdvisor(new TransactionAdvisor(
new TransactionPointcut(),
new TransactionAdvice(platformTransactionManager)
));
}

@Test
void testChangePassword() {
final UserService userService = null;
proxyFactoryBean.setTarget(new UserService(userDao, userHistoryDao));
final UserService userService = (UserService) proxyFactoryBean.getObject();

final var newPassword = "qqqqq";
final var createBy = "gugu";
Expand All @@ -54,7 +63,8 @@ void testChangePassword() {

@Test
void testTransactionRollback() {
final UserService userService = null;
proxyFactoryBean.setTarget(new UserService(userDao, stubUserHistoryDao));
final UserService userService = (UserService) proxyFactoryBean.getObject();

final var newPassword = "newPassword";
final var createBy = "gugu";
Expand Down
25 changes: 22 additions & 3 deletions study/src/test/java/aop/stage1/TransactionAdvice.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
package aop.stage1;

import aop.DataAccessException;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;


/**
* 어드바이스(advice). 부가기능을 담고 있는 클래스
*/
public class TransactionAdvice implements MethodInterceptor {
public class TransactionAdvice implements MethodInterceptor {

private final PlatformTransactionManager platformTransactionManager;

public TransactionAdvice(PlatformTransactionManager platformTransactionManager) {
this.platformTransactionManager = platformTransactionManager;
}

@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
return null;
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object result = invocation.getMethod().invoke(invocation.getThis(), invocation.getArguments());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invocation.proceed()를 활용해보는 건 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정보를 이것저것 끄집어 내서 쓰는 것 같아서 뭔가 맘에 안 들었는데 훨씬 깔끔해졌네요! 감사합니다😊

platformTransactionManager.commit(transactionStatus);
return result;
} catch (Throwable e) {
platformTransactionManager.rollback(transactionStatus);
throw new DataAccessException(e);
}
}
}
20 changes: 14 additions & 6 deletions study/src/test/java/aop/stage1/TransactionAdvisor.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package aop.stage1;

import org.aopalliance.aop.Advice;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;

/**
* 어드바이저(advisor). 포인트컷과 어드바이스를 하나씩 갖고 있는 객체.
* AOP의 애스팩트(aspect)에 해당되는 클래스다.
* 어드바이저(advisor). 포인트컷과 어드바이스를 하나씩 갖고 있는 객체. AOP의 애스팩트(aspect)에 해당되는 클래스다.
*/
public class TransactionAdvisor implements PointcutAdvisor {

private final Pointcut pointcut;
private final Advice advice;

public TransactionAdvisor(Pointcut pointcut, Advice advice) {
this.pointcut = pointcut;
this.advice = advice;
}

@Override
public Pointcut getPointcut() {
return null;
public @NotNull Pointcut getPointcut() {
return pointcut;
}

@Override
public Advice getAdvice() {
return null;
public @NotNull Advice getAdvice() {
return advice;
}

@Override
Expand Down
3 changes: 2 additions & 1 deletion study/src/test/java/aop/stage1/TransactionPointcut.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package aop.stage1;

import aop.Transactional;
import org.springframework.aop.support.StaticMethodMatcherPointcut;

import java.lang.reflect.Method;
Expand All @@ -14,6 +15,6 @@ public class TransactionPointcut extends StaticMethodMatcherPointcut {

@Override
public boolean matches(final Method method, final Class<?> targetClass) {
return false;
return method.isAnnotationPresent(Transactional.class);
}
}