프록시와 프록시 패턴, 데코레이터 패턴
트랜잭션 경계 설정 코드를 비즈니스 로직에서 분리하는 방법에 초점을 맞추고 있습니다.
프록시 패턴
프록시 패턴은 클라이언트와 타깃(실제 객체) 사이에 프록시(대리자) 객체를 배치하여, 타깃의 메소드 호출을 제어하거나 부가적인 작업을 수행하는 패턴입니다. 이 패턴의 주된 목적은 두 가지입니다:
1. 접근 제어: 클라이언트가 타깃 객체에 직접 접근하는 것을 제한하거나 특정 조건에서만 접근을 허용합니다.
2. 부가 기능 추가: 타깃 객체의 메소드 호출 전후에 부가적인 작업을 수행하여, 타깃의 기능을 확장합니다.
프록시 패턴의 핵심은 타깃과 동일한 인터페이스를 프록시가 구현함으로써 클라이언트가 타깃인 줄 알고 프록시를 통해 작업을 수행하게 하는 것입니다. 예를 들어, UserServiceTx는 트랜잭션 관리 기능을 추가하기 위해 UserServiceImpl의 메소드 호출을 중간에서 처리합니다.
데코레이터 패턴
데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가하는 패턴입니다. 이 패턴은 프록시 패턴과 유사하지만, 데코레이터는 여러 개의 객체를 체인처럼 연결하여 복합적인 기능을 생성할 수 있습니다. 데코레이터의 각 구성 요소는 독립적으로 타깃 또는 다른 데코레이터에게 작업을 위임할 수 있습니다.
데코레이터 패턴의 장점은 다음과 같습니다:
- 유연성: 런타임에 다양한 데코레이터를 조합하여 복잡한 기능을 구성할 수 있습니다.
- 확장성: 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
- 재사용성: 일반적인 기능을 데코레이터로 구현하여 다양한 컨텍스트에서 재사용할 수 있습니다.
예를 들어, InputStream의 BufferedInputStream 데코레이터는 버퍼링 기능을 추가하여 입출력 성능을 향상시킵니다.
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
마찬가지로, UserServiceTx는 UserServiceImpl에 트랜잭션 관리 기능을 추가하는 데코레이터로 작용합니다.
@Configuration
public class TestServiceFactory {
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost:3306/testdb?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
@Bean
public UserDaoJdbc userDao() {
UserDaoJdbc userDaoJdbc = new UserDaoJdbc();
userDaoJdbc.setDataSource(dataSource());
return userDaoJdbc;
}
@Bean /*(name="userService2")*/
public UserServiceImpl/*UserService*/ userServiceImpl() {
UserServiceImpl userServiceImpl = new UserServiceImpl();
userServiceImpl.setUserDao(userDao());
userServiceImpl.setMailSender(mailSender());
return userServiceImpl;
}
@Bean /*(name="userService1")*/
public UserService userService() {
UserServiceTx userServiceTx = new UserServiceTx();
userServiceTx.setTransactionManager(transactionManager());
userServiceTx.setUserService(userServiceImpl());
return userServiceTx;
}
@Bean
public DummyMailSender mailSender() {
DummyMailSender dummyMailSender = new DummyMailSender();
return dummyMailSender;
}
}
public class UserServiceImpl implements UserService {
public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
public static final int MIN_RECCOMEND_FOR_GOLD = 30;
private UserDao userDao;
private MailSender mailSender;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
private boolean canUpgradeLevel(User user) {
Level currentLevel = user.getLevel();
switch(currentLevel) {
case BASIC: return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
case SILVER: return (user.getRecommend() >= MIN_RECCOMEND_FOR_GOLD);
case GOLD: return false;
default: throw new IllegalArgumentException("Unknown Level: " + currentLevel);
}
}
protected void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
sendUpgradeEMail(user);
}
private void sendUpgradeEMail(User user) {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(user.getEmail());
mailMessage.setFrom("useradmin@ksug.org");
mailMessage.setSubject("Upgrade 반가워요");
mailMessage.setText("등급을 축하 드려요. " + user.getLevel().name());
this.mailSender.send(mailMessage);
}
public void add(User user) {
if (user.getLevel() == null) user.setLevel(Level.BASIC);
userDao.add(user);
}
}
public class UserServiceTx implements UserService {
UserService userService;
PlatformTransactionManager transactionManager;
public void setTransactionManager(
PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void add(User user) {
this.userService.add(user);
}
public void upgradeLevels() {
TransactionStatus status = this.transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
userService.upgradeLevels();
this.transactionManager.commit(status);
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
스프링 프레임워크와 DI
스프링 프레임워크의 DI(Dependency Injection)는 데코레이터 패턴을 구현하는 데 매우 유용합니다. 스프링은 런타임에 객체 간의 의존성을 주입하여, 유연하고 확장 가능한 애플리케이션 구조를 가능하게 합니다. 예를 들어, UserServiceTx 데코레이터는 UserServiceImpl 타깃 객체를 DI를 통해 주입 받아, 클라이언트의 요청을 처리하는 동안 트랜잭션 관리 기능을 추가할 수 있습니다.
프록시와 데코레이터 패턴은 객체 지향 설계에서 강력한 도구입니다. 이들은 코드의 재사용성과 확장성을 높이고, 복잡한 비즈니스 로직과 부가 기능을 효율적으로 분리할 수 있게 도와줍니다. 특히, 스프링과 같은 프레임워크를 사용하면 이러한 패턴을 더욱 간편하고 효과적으로 적용할 수 있습니다.
프록시 패턴의 일반적 문제
프록시 패턴은 타깃 객체의 기능을 확장하거나 접근 방법을 제어하는 데 유용하지만, 일반적으로 다음과 같은 두 가지 문제가 있습니다:
1. 번거로운 코드 작성: 각 타깃 인터페이스의 모든 메소드를 구현해야 하며, 이를 위임하는 코드를 작성하는 것이 번거롭습니다. 특히 인터페이스의 메소드가 많거나 변경될 경우, 이를 반영하는 작업은 상당한 부담이 될 수 있습니다.
2. 부가 기능 코드의 중복: 예를 들어, 트랜잭션 관리와 같은 부가 기능은 여러 메소드나 클래스에서 필요로 할 수 있으며, 이로 인해 중복 코드가 발생할 가능성이 높습니다.
Hello 인터페이스
static interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
타겟 클래스 HelloTarget 클래스
static class HelloTarget implements Hello {
public String sayHello(String name) {
return "Hello " + name;
}
public String sayHi(String name) {
return "Hi " + name;
}
public String sayThankYou(String name) {
return "Thank You " + name;
}
}
타겟 클래스 테스트
@Test
public void simpleProxy() {
Hello hello = new HelloTarget();
assertEquals(hello.sayHello("Toby"), "Hello Toby");
assertEquals(hello.sayHi("Toby"), "Hi Toby");
assertEquals(hello.sayThankYou("Toby"), "Thank You Toby");
}
프록시 클래스 정의
public class HelloUppercase implements Hello {
Hello hello;
public HelloUppercase(Hello hello) {
this.hello = hello;
}
public String sayHello(String name) {
return hello.sayHello(name).toUpperCase();
}
public String sayHi(String name) {
return hello.sayHi(name).toUpperCase();
}
public String sayThankYou(String name) {
return hello.sayThankYou(name).toUpperCase();
}
}
프록시 클래스 테스트
@Test
public void simpleProxy() {
Hello hello = new HelloTarget();
assertEquals(hello.sayHello("Toby"), "Hello Toby");
assertEquals(hello.sayHi("Toby"), "Hi Toby");
assertEquals(hello.sayThankYou("Toby"), "Thank You Toby");
// simple proxy 생성
Hello proxiedHello = new HelloUppercase(new HelloTarget());
assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY");
assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY");
assertEquals(proxiedHello.sayThankYou("Toby"), "THANK YOU TOBY");
}
이 프록시는 프록시 적용의 일반적인 문제점 두 가지를 모두 갖고 있습니다.
1. 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 합니다.
2. 부가기능인 리턴 값을 대문자로 바꾸는 기능이 모든 메소드에 중복되어 나타납니다.
JDK 다이내믹 프록시 적용
프록시 클래스인 HelloUppercase를 JDK 다이내믹 프록시를 이용해 만들어 보겠습니다.
다이내믹 프록시가 동작하는 방식은 그림 6-13과 같다.
*위 그림에서 프록시 팩토리는 Proxy.newProxyInstance임.
다이나믹 프록시(dynamic proxy)는 자바의 Proxy.newProxyInstance 메소드에 의해 런타임 시 동적으로 생성되는 객체입니다. 이러한 프록시 객체의 특징과 장점을 다음과 같이 정리할 수 있습니다:
1. 인터페이스 기반 생성: 다이나믹 프록시 객체는 대상(target) 객체의 인터페이스와 동일한 타입으로 생성됩니다. 이는 프록시가 인터페이스를 구현하여, 대상 객체와 동일한 메소드를 제공할 수 있음을 의미합니다.
2. 클라이언트 사용 용이성: 클라이언트는 대상 객체를 직접 사용하는 것과 동일한 방식으로 다이나믹 프록시 객체를 사용할 수 있습니다. 이는 프록시 객체가 대상 객체의 인터페이스를 구현하기 때문입니다.
3. 구현 클래스의 자동 생성: 프록시 팩토리는 제공된 인터페이스 정보를 기반으로 해당 인터페이스를 구현하는 클래스[프록시 클래스]의 객체를 자동으로 생성합니다. 이는 개발자가 인터페이스를 구현하는 별도의 클래스를 정의하는 번거로움을 줄여줍니다.
다이나믹 프록시의 이러한 특성 덕분에, 개발자는 복잡한 클래스 구현 없이 효율적으로 프록시 객체를 생성하고 관리할 수 있으며, 이를 통해 메소드 호출의 가로채기(interception), 추가 로직의 실행 등 다양한 작업을 수행할 수 있습니다. 이는 특히 Aspect-Oriented Programming(AOP) 같은 기법에서 매우 유용하게 활용됩니다.
다이나믹 프록시(dynamic proxy)는 인터페이스를 구현하는 클래스의 객체를 자동으로 생성해주지만, 프록시가 제공해야 할 부가 기능(additional functionality)은 별도로 작성해야 합니다. 이 부가 기능은 InvocationHandler 인터페이스를 구현하는 객체에 의해 제공됩니다. 여기에 다이나믹 프록시의 주요 특징과 InvocationHandler의 역할을 정리하면 다음과 같습니다:
1. 부가 기능의 분리: 다이나믹 프록시는 인터페이스 구현을 자동으로 처리하지만, 프록시 객체의 실제 작동 방식과 부가 기능은 InvocationHandler에 의해 정의됩니다.
2. InvocationHandler의 역할: InvocationHandler는 프록시 객체의 모든 메소드 호출을 처리하는 중앙 처리 단계입니다. 이 인터페이스는 대상 메소드 호출에 대한 로직을 커스터마이징할 수 있도록 하나의 메소드를 제공합니다.
3. 간단한 인터페이스 구조: InvocationHandler 인터페이스는 단 하나의 메소드를 가집니다:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
이 메소드는 프록시 객체를 통해 호출되는 모든 메소드에 대해 실행되며, 프록시 객체 자신(proxy), 호출된 메소드(method), 그리고 메소드 인자들(args)을 인자로 받습니다.
이러한 구조 덕분에, 다이나믹 프록시를 통해 개발자는 인터페이스의 구현 부분에 신경 쓰지 않고, 프록시를 통해 수행되어야 할 부가적인 기능만을 집중적으로 작성할 수 있습니다. 이는 특히 메소드 호출의 가로채기, 로깅, 트랜잭션 처리 등 다양한 상황에서 유용하게 활용될 수 있습니다.
InvocationHandler 인터페이스의 invoke 메소드는 다이나믹 프록시가 메소드 호출을 가로챌 때 중심적인 역할을 합니다. 이 메소드는 프록시 객체를 통해 호출되는 모든 메소드에 대해 실행되며, 다음 세 가지 주요 파라미터를 받습니다:
1. Object proxy: 이 파라미터는 메소드 호출이 이루어진 프록시 객체 자신을 나타냅니다. 이를 통해 프록시 객체의 참조에 접근할 수 있으며, 필요한 경우 프록시 객체의 상태를 검사하거나 조작할 수 있습니다.
2. Method method: 이 파라미터는 호출되는 메소드에 대한 정보를 담고 있는 java.lang.reflect.Method 객체입니다. 이를 통해 메소드의 이름, 반환 타입, 파라미터 타입 등 메소드에 대한 상세 정보를 얻을 수 있습니다.
3. Object[] args: 이 파라미터는 메소드 호출 시 전달된 인자들의 배열입니다. 이 배열을 통해 호출된 메소드의 인자 값을 얻을 수 있으며, 필요에 따라 이 인자들을 사용하거나 수정할 수 있습니다.
invoke 메소드의 리턴 값은 호출된 메소드의 리턴 타입에 맞게 반환됩니다. 만약 호출된 메소드가 void 타입이라면 invoke 메소드는 null을 반환할 수 있습니다.
invoke 메소드는 다이나믹 프록시의 핵심적인 메커니즘으로 사용되며, 이를 통해 다음과 같은 기능을 구현할 수 있습니다:
- 메소드 호출의 가로채기: 호출된 메소드에 대한 로직을 변경하거나, 추가적인 작업을 실행할 수 있습니다.
- 로깅 및 모니터링: 메소드 호출 전후에 로깅이나 모니터링을 수행할 수 있습니다.
- 트랜잭션 관리: 특정 메소드 호출에 트랜잭션 처리를 적용할 수 있습니다.
- 보안 검사: 메소드 호출 전에 보안 검사를 수행할 수 있습니다.
이러한 기능들은 AOP(Aspect-Oriented Programming)의 핵심 개념과 밀접하게 연결되어 있으며, 다이나믹 프록시를 통해 유연하게 구현될 수 있습니다.
그림 6-14는 다이내믹 프록시 클래스 오브젝트와 InvocationHandler 오브젝트, 타깃 오브젝트 사이의 메소드 호출이 일어나는 과정을 나타냅니다.
InvocationHandler 구현 클래스
static class UppercaseHandler implements InvocationHandler {
Object target;
private UppercaseHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object ret = method.invoke(target, args);
if (ret instanceof String && method.getName().startsWith("say")) {
return ((String)ret).toUpperCase();
}
else {
return ret;
}
}
}
JDK 다이나믹 프록시 생성
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Hello.class},
new UppercaseHandler(new HelloTarget()));
다이내믹 프록시를 이용한 트랜잭션 부가기능
다이나믹 프록시를 이용한 트랜잭션 부가기능 구현은 기존의 트랜잭션 처리 방법을 보다 효율적으로 변환하는 좋은 예입니다. 여기서 기존의 방식, 즉 UserServiceTx와 같은 프록시 클래스를 사용하는 방식은 각 서비스 인터페이스 메소드마다 트랜잭션 관리 코드를 중복해서 구현해야 했습니다. 이 방법은 트랜잭션이 필요한 클래스와 메소드가 증가함에 따라 비효율적이고 부담스러워질 수 있습니다.
다이나믹 프록시를 이용한 접근 방식은 이러한 문제를 해결합니다. 트랜잭션 부가기능을 제공하는 다이나믹 프록시를 만들고, 이와 연동하는 InvocationHandler를 정의함으로써, 트랜잭션 처리 코드를 한 곳에 모아 효율적으로 관리할 수 있게 됩니다. 이 방식의 주요 장점은 다음과 같습니다:
1. 코드 중복 감소: 트랜잭션 관리 코드를 InvocationHandler 내에 중앙화함으로써, 각 메소드마다 트랜잭션 코드를 반복적으로 작성하는 것을 방지할 수 있습니다.
2. 유연성 증가: 다이나믹 프록시를 사용하면, 트랜잭션이 필요한 모든 서비스에 대해 하나의 InvocationHandler를 재사용할 수 있습니다. 이는 새로운 서비스가 추가될 때마다 새로운 프록시 클래스를 만들 필요가 없다는 것을 의미합니다.
3. 유지보수 용이성: 트랜잭션 관련 로직이 한 곳에 모여 있기 때문에, 변경이나 유지보수가 용이해집니다.
4. 확장성: 다이나믹 프록시 방식은 트랜잭션 관리뿐만 아니라 다른 종류의 부가 기능을 추가하기에도 유연하며, 시스템의 확장성을 높여줍니다.
이러한 방식을 통해, 트랜잭션 처리가 필요한 메소드에 대한 공통적인 트랜잭션 관리 로직을 보다 효율적으로 관리할 수 있으며, 소프트웨어 설계의 원칙인 DRY(Don't Repeat Yourself)를 준수할 수 있습니다.
트랜잭션 InvocationHandler
package com.coga.springframe.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
public class TransactionHandler implements InvocationHandler {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(
PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInTransaction(Method method, Object[] args)
throws Throwable {
TransactionStatus status = this.transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = method.invoke(target, args);
this.transactionManager.commit(status);
return ret;
} catch (InvocationTargetException e) {
this.transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
TransactionHandler는 자바의 리플렉션 API를 활용하여 트랜잭션 관리 기능을 다이나믹 프록시에 부가하는 InvocationHandler 구현체입니다. 이 클래스는 일반적인 비즈니스 로직을 실행하는 타깃 객체에 트랜잭션 관리 기능을 추가하여, 트랜잭션이 필요한 메소드 호출 시 자동으로 트랜잭션을 처리합니다. 다음은 TransactionHandler의 주요 특징과 구현 방식에 대한 세련되고 정확한 설명입니다:
1. 다목적 타깃 객체 지원: TransactionHandler는 Object 타입의 target 필드를 통해 다양한 타깃 객체를 지원합니다. 이로 인해 UserServiceImpl뿐만 아니라 다른 여러 서비스에도 트랜잭션 기능을 동적으로 추가할 수 있습니다.
2. 트랜잭션 관리자 통합: PlatformTransactionManager 인스턴스를 DI(의존성 주입)를 통해 받아, 트랜잭션 관리를 추상화합니다. 이는 트랜잭션의 시작, 커밋, 롤백을 효율적으로 관리할 수 있게 합니다.
3. 메소드 이름 패턴 매칭: 트랜잭션을 적용할 메소드를 선택하기 위해 메소드 이름의 패턴을 pattern 필드를 통해 설정합니다. 예를 들어, pattern이 "get"으로 설정되면, "get"으로 시작하는 모든 메소드에 트랜잭션이 적용됩니다.
4. 조건부 트랜잭션 처리: invoke 메소드는 메소드 호출 시 해당 메소드 이름이 설정된 패턴과 일치하는지를 검사하고, 일치할 경우 트랜잭션 처리 로직을 수행합니다. 일치하지 않으면, 부가 기능 없이 타깃 객체의 원래 메소드를 호출합니다.
5. 예외 처리와 롤백: 트랜잭션 중 예외가 발생하면, InvocationTargetException을 캐치하여 원래 발생한 예외를 추출하고, 트랜잭션을 롤백합니다. Method.invoke()는 타깃 객체의 예외를 InvocationTargetException으로 감싸서 전달하기 때문에 이러한 처리가 필요합니다.
이 구현 방식은 UserServiceTx와 같은 별도의 프록시 클래스를 생성하는 것보다 훨씬 유연하고 확장성이 높으며, 다양한 서비스에 대해 트랜잭션 관리 기능을 쉽게 추가할 수 있게 해줍니다.
TransactionHandler와 다이내믹 프록시를 이용하는 테스트
다이내믹 프록시를 이용한 트랜잭션 테스트
@Test
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.mailSender);
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(testUserService);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern("upgradeLevels");
UserService txUserService = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] {UserService.class}, txHandler);
userDao.deleteAll();
for(User user : users) userDao.add(user);
try {
txUserService.upgradeLevels();
fail("TestUserServiceException expected");
}
catch(TestUserServiceException e) {
}
checkLevelUpgraded(users.get(1), false);
}
Spring FactoryBean
다음 코드는 Spring Framework를 사용하여 FactoryBean을 구현하고 테스트하는 예제입니다. FactoryBean은 Spring의 빈(Bean) 관리를 통해 런타임에 객체를 생성하고 구성하는 데 사용되는 인터페이스입니다. 코드는 다음과 같은 요소로 구성되어 있습니다:
1. Message 클래스:
- Message 클래스는 텍스트 메시지를 저장하는 간단한 POJO(Plain Old Java Object)입니다.
- newMessage 스태틱 메서드를 통해 객체를 생성합니다.
public class Message {
String text;
private Message(String text) {
this.text = text;
}
public String getText() {
return text;
}
public static Message newMessage(String text) {
return new Message(text);
}
}
2. MessageFactoryBean 클래스:
- MessageFactoryBean 클래스는 FactoryBean<Message> 인터페이스를 구현합니다.
- text 필드는 메시지 텍스트를 저장합니다.
- 객체를 생성하고 구성하기 위한 getObject 메서드를 구현합니다. 이 메서드에서 Message.newMessage(this.text)를 호출하여 Message 객체를 생성하고 반환합니다.
- getObjectType 메서드는 반환할 빈 객체의 클래스 타입을 지정합니다.
- isSingleton 메서드는 빈이 싱글톤인지 여부를 지정합니다.
package com.coga.learningtest.spring.factorybean;
import org.springframework.beans.factory.FactoryBean;
public class MessageFactoryBean implements FactoryBean<Message>{
String text;
public void setText(String text) {
this.text = text;
}
public Message getObject() throws Exception {
return Message.newMessage(this.text);
}
public Class<? extends Message> getObjectType() {
return Message.class;
}
public boolean isSingleton() {
return true;
}
}
3. FactoryBeanConfig 클래스:
- @Configuration 어노테이션을 사용하여 Spring 구성 클래스임을 나타냅니다.
- message 메서드는 MessageFactoryBean 빈을 정의하고 구성합니다. 이 메서드에서 MessageFactoryBean 객체를 생성하고 텍스트를 설정한 후 반환합니다.
package com.coga.learningtest.spring.factorybean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FactoryBeanConfig {
@Bean
public MessageFactoryBean message() {
MessageFactoryBean messageFactoryBean = new MessageFactoryBean();
messageFactoryBean.setText("Factory Bean");
return messageFactoryBean;
}
}
4. FactoryBeanTest 클래스:
- Spring 테스트를 위한 JUnit 5 테스트 클래스입니다.
- @ExtendWith(SpringExtension.class) 어노테이션은 Spring 테스트 확장을 사용하도록 활성화합니다.
- @ContextConfiguration(classes = {FactoryBeanConfig.class}) 어노테이션은 Spring 컨텍스트를 설정하기 위해 FactoryBeanConfig 클래스를 사용한다고 선언합니다.
5. getMessageFromFactoryBean 메서드:
- message 빈을 가져와서 해당 빈이 Message 클래스의 인스턴스인지 확인합니다.
- assertEquals를 사용하여 예상 결과를 검사합니다.
6. getFactoryBean 메서드:
- &message를 사용하여 MessageFactoryBean 빈 자체를 가져옵니다.
- assertEquals를 사용하여 예상 결과를 검사합니다.
package com.coga.learningtest.spring.factorybean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {FactoryBeanConfig.class})
public class FactoryBeanTest {
@Autowired
ApplicationContext context;
@Test
public void getMessageFromFactoryBean() {
Object message = context.getBean("message");
assertEquals(message.getClass(), Message.class);
assertEquals(((Message)message).getText(), "Factory Bean");
}
@Test
public void getFactoryBean() throws Exception {
Object factory = context.getBean("&message");
assertEquals(factory.getClass(), MessageFactoryBean.class);
}
}
코드는 MessageFactoryBean을 사용하여 Message 객체를 생성하고 Spring 컨텍스트에서 이를 관리합니다. 테스트 메서드는 빈이 올바르게 생성되고 구성되었는지 확인하기 위해 Spring 테스트 기능을 사용합니다.
Spring FactoryBean & Java Dynamic Proxy
package com.coga.springframe.service;
import com.coga.springframe.domain.User;
public interface UserService {
void add(User user);
void upgradeLevels();
}
package com.coga.springframe.dao;
import java.util.List;
import java.util.Optional;
import com.intheeast.springframe.domain.User;
public interface UserDao {
void add(User user);
Optional<User> get(String id);
List<User> getAll();
void deleteAll();
int getCount();
public void update(User user);
}
package com.coga.springframe.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import com.coga.springframe.domain.Level;
import com.coga.springframe.domain.User;
public class UserDaoJdbc implements UserDao {
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
private JdbcTemplate jdbcTemplate;
private RowMapper<User> userMapper =
new RowMapper<User>() {
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setLevel(Level.valueOf(rs.getInt("level")));
user.setLogin(rs.getInt("login"));
user.setRecommend(rs.getInt("recommend"));
return user;
}
};
@Override
public void add(User user) {
//System.out.println("UserDaoJdbc.add");
this.jdbcTemplate.update(
"insert into users(id, name, password, email, level, login, recommend) " +
"values(?,?,?,?,?,?,?)",
user.getId(), user.getName(), user.getPassword(), user.getEmail(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}
@Override
public Optional<User> get(String id) {
//System.out.println("UserDaoJdbc.get");
String sql = "select * from users where id = ?";
try (Stream<User> stream = jdbcTemplate.queryForStream(sql, this.userMapper, id)) {
return stream.findFirst();
} catch (DataAccessException e) {
return Optional.empty();
}
}
@Override
public void deleteAll() {
//System.out.println("UserDaoJdbc.deleteAll");
this.jdbcTemplate.update("delete from users");
}
@Override
public int getCount() {
//System.out.println("UserDaoJdbc.getCount");
List<Integer> result = jdbcTemplate.query("select count(*) from users",
(rs, rowNum) -> rs.getInt(1));
return (int) DataAccessUtils.singleResult(result);
}
@Override
public List<User> getAll() {
//System.out.println("UserDaoJdbc.getAll");
return this.jdbcTemplate.query("select * from users order by id",
this.userMapper
);
}
@Override
public void update(User user) {
//System.out.println("UserDaoJdbc.update");
this.jdbcTemplate.update(
"update users set name = ?, password = ?, email = ?, level = ?, login = ?, " +
"recommend = ? where id = ? ", user.getName(), user.getPassword(), user.getEmail(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend(),
user.getId());
}
}
package com.coga.springframe.service;
import java.util.List;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import com.coga.springframe.dao.UserDao;
import com.coga.springframe.domain.Level;
import com.coga.springframe.domain.User;
public class UserServiceImpl implements UserService {
public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
public static final int MIN_RECCOMEND_FOR_GOLD = 30;
private UserDao userDao;
private MailSender mailSender;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void add(User user) {
if (user.getLevel() == null)
user.setLevel(Level.BASIC);
userDao.add(user);
}
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
private boolean canUpgradeLevel(User user) {
Level currentLevel = user.getLevel();
switch(currentLevel) {
case BASIC: return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
case SILVER: return (user.getRecommend() >= MIN_RECCOMEND_FOR_GOLD);
case GOLD: return false;
default: throw new IllegalArgumentException("Unknown Level: " + currentLevel);
}
}
protected void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
sendUpgradeEMail(user);
}
private void sendUpgradeEMail(User user) {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(user.getEmail());
mailMessage.setFrom("useradmin@ksug.org");
mailMessage.setSubject("Upgrade �ȳ�");
mailMessage.setText("����ڴ��� ����� " + user.getLevel().name());
this.mailSender.send(mailMessage);
}
}
TransactionHandler 클래스 코드는 Spring Framework에서 사용되는 AOP (Aspect-Oriented Programming) 스타일의 트랜잭션 관리를 지원하기 위한 핸들러 클래스인 TransactionHandler를 정의하고 있습니다. 이 핸들러는 주로 프록시 객체를 통해 실제 서비스 객체에 대한 메서드 호출을 감싸서 트랜잭션 관리를 수행하는 데 사용됩니다.
여기서 코드의 주요 요소를 살펴보겠습니다:
1. TransactionHandler 클래스는 InvocationHandler 인터페이스를 구현하고 있으며, 이는 자바의 다이내믹 프록시를 사용하여 메서드 호출을 가로채고 제어할 수 있도록 합니다.
2. target 필드는 실제 서비스 객체를 가리키며, 이 객체의 메서드 호출은 트랜잭션 관리 대상이 됩니다.
3. transactionManager 필드는 Spring의 PlatformTransactionManager 인터페이스를 구현한 구체적인 트랜잭션 관리자(bean)를 주입받습니다. 이 트랜잭션 관리자는 트랜잭션의 시작, 커밋, 롤백을 처리하는 데 사용됩니다.
4. pattern 필드는 메서드 이름의 패턴을 나타내며, 이 패턴과 일치하는 메서드 호출만 트랜잭션으로 감싸집니다.
5. invoke 메서드는 InvocationHandler 인터페이스에서 정의된 메서드로, 프록시 객체를 통해 호출되는 메서드입니다. 이 메서드에서 메서드 이름에 따라 트랜잭션을 적용하거나 일반적으로 메서드를 호출합니다.
6. invokeInTransaction 메서드는 실제로 트랜잭션을 적용하는 로직을 포함하고 있습니다. 이 메서드에서는 새로운 트랜잭션을 시작하고, 메서드를 호출한 후에 트랜잭션을 커밋하거나 예외가 발생한 경우 롤백합니다.
이 TransactionHandler 클래스를 사용하면, 특정 패턴을 가진 메서드 호출을 트랜잭션으로 감싸서 데이터베이스 등의 리소스에 대한 안전한 작업을 수행할 수 있습니다. Spring AOP를 통해 이러한 방식으로 트랜잭션 관리를 수행하면 코드 중복을 줄이고 효율적으로 트랜잭션을 관리할 수 있습니다.
package com.coga.springframe.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
public class TransactionHandler implements InvocationHandler {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(
PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("TransactionHandler:invoke:" + method.getName());
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInTransaction(Method method, Object[] args)
throws Throwable {
TransactionStatus status = this.transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = method.invoke(target, args);
this.transactionManager.commit(status);
return ret;
} catch (InvocationTargetException e) {
this.transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
TxProxyFactoryBean 클래스 코드는 Spring Framework에서 사용할 수 있는 AOP 스타일의 트랜잭션 프록시를 생성하는 클래스를 정의하고 있습니다. 이 FactoryBean은 실제 서비스 객체에 대한 프록시를 생성하고 관리하는 역할을 합니다.
주요 요소 및 동작에 대한 설명은 다음과 같습니다:
1. target 필드는 프록시를 생성할 대상 서비스 객체를 저장합니다.
2. transactionManager 필드는 해당 서비스 객체의 메서드 호출에 대한 트랜잭션 관리를 수행할 PlatformTransactionManager 빈을 주입합니다.
3. pattern 필드는 어떤 메서드 호출을 트랜잭션으로 감싸야 하는지를 결정하는 패턴을 지정합니다.
4. serviceInterface 필드는 프록시가 구현해야 하는 인터페이스를 지정합니다. 프록시는 해당 인터페이스를 구현하므로 클라이언트 코드는 이 인터페이스를 통해 프록시를 사용할 수 있습니다.
5. getObject() 메서드는 FactoryBean 인터페이스를 구현한 메서드로, 이 메서드에서 프록시 객체를 생성하고 반환합니다. 실제로 프록시 객체를 만들 때 Proxy.newProxyInstance() 메서드를 사용하여 TransactionHandler를 등록하여 트랜잭션 관리를 수행하도록 합니다.
6. getObjectType() 메서드는 FactoryBean이 생성하는 객체의 타입을 반환합니다. 이 경우, serviceInterface와 같은 타입을 반환하므로 해당 타입의 빈을 가져오는 데 사용됩니다.
7. isSingleton() 메서드는 FactoryBean이 싱글톤 빈을 생성하는지 여부를 나타냅니다. 이 경우 false를 반환하므로 매번 getObject()를 호출할 때마다 새로운 프록시 객체가 생성됩니다.
이 TxProxyFactoryBean 클래스를 사용하면 Spring 컨테이너에서 빈으로 등록하여, 원하는 서비스 객체에 트랜잭션 처리를 쉽게 적용할 수 있습니다. 이를 통해 트랜잭션 관리 코드를 서비스 객체에 직접 추가하지 않고도 트랜잭션을 적용할 수 있어 코드의 재사용성과 유지 보수성이 향상됩니다.
package com.coga.springframe.service;
import java.lang.reflect.Proxy;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
public class TxProxyFactoryBean implements FactoryBean<Object> {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
Class<?> serviceInterface;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public void setServiceInterface(Class<?> serviceInterface) {
this.serviceInterface = serviceInterface;
}
// FactoryBean 인터페이스 구현 메서드
// 인스턴스를 반환하는 메서드입니다.
public Object getObject() throws Exception {
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(target);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern(pattern);
//Class<? extends TxProxyFactoryBean> whatclass = this.getClass();
// Create Dynamic Proxy!!!
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { serviceInterface },
txHandler);
}
public Class<?> getObjectType() {
return serviceInterface;
}
public boolean isSingleton() {
return false;
}
}
package com.coga.springframe.service;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import com.coga.springframe.dao.UserDaoJdbc;
@Configuration
public class TestServiceFactory {
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost:3306/testdb?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean
public UserDaoJdbc userDao() {
UserDaoJdbc userDaoJdbc = new UserDaoJdbc();
userDaoJdbc.setDataSource(dataSource());
return userDaoJdbc;
}
@Bean
public TxProxyFactoryBean userService() {
TxProxyFactoryBean txProxyFactoryBean = new TxProxyFactoryBean();
txProxyFactoryBean.setTarget(userServiceImpl());
txProxyFactoryBean.setTransactionManager(transactionManager());
txProxyFactoryBean.setPattern("upgradeLevels");
txProxyFactoryBean.setServiceInterface(UserService.class);
return txProxyFactoryBean;
}
@Bean
public UserServiceImpl userServiceImpl() {
UserServiceImpl userServiceImpl = new UserServiceImpl();
userServiceImpl.setUserDao(userDao());
userServiceImpl.setMailSender(mailSender());
return userServiceImpl;
}
@Bean
public DummyMailSender mailSender() {
DummyMailSender dummyMailSender = new DummyMailSender();
return dummyMailSender;
}
@Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
}
'Spring Framework' 카테고리의 다른 글
Spring ProxyFactoryBean (0) | 2024.04.09 |
---|---|
Factory Bean (0) | 2024.04.09 |
InvocationHandler (0) | 2024.04.09 |
서비스 추상화 (0) | 2024.04.09 |
Service Layer (0) | 2024.04.09 |