Spring AOP의 핵심인 Proxy 패턴과 이벤트 기반 설계의 Observer 패턴을 학습합니다. JDK Dynamic Proxy와 CGLIB의 차이점, @Transactional의 동작 원리를 이해합니다.
"Proxy는 다른 객체에 대한 접근을 제어하는 대리자 역할을 한다."
— Gang of Four
Proxy 패턴은 Spring Framework에서 가장 중요한 패턴입니다. @Transactional, @Async, @Cacheable, @Retryable, AOP 등 Spring의 핵심 기능들이 모두 Proxy를 통해 구현됩니다. Proxy 패턴을 이해하면 Spring의 "마법"처럼 보이는 동작들이 어떻게 작동하는지 명확하게 이해할 수 있습니다.
Proxy는 "대리인"이라는 뜻입니다. 실제 객체를 대신하여 클라이언트의 요청을 받아 처리하는 객체입니다. 클라이언트는 실제 객체를 직접 호출하는 것처럼 보이지만, 실제로는 Proxy 객체가 중간에서 요청을 가로채어 부가적인 작업을 수행합니다.
권한 검사, 지연 로딩(Lazy Loading), 캐싱 등을 통해 실제 객체에 대한 접근을 제어합니다.
로깅, 트랜잭션 관리, 성능 측정 등 핵심 로직과 분리된 부가 기능을 추가합니다.
원격 서버의 객체를 마치 로컬 객체처럼 사용할 수 있게 합니다.
비즈니스 로직과 부가 기능(횡단 관심사)을 분리하기 위해 Proxy가 필요합니다. 로깅, 트랜잭션, 보안, 캐싱 같은 기능은 여러 클래스에 걸쳐 반복적으로 나타납니다. 이런 코드를 각 클래스에 직접 작성하면 코드 중복이 발생하고, 비즈니스 로직이 부가 기능 코드에 묻혀 가독성이 떨어집니다.
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public Order createOrder(Long customerId, List<OrderItem> items) {
// 로깅 - 시작
long startTime = System.currentTimeMillis();
log.info("[OrderService.createOrder] 시작 - customerId: {}, items: {}",
customerId, items.size());
try {
// 트랜잭션 시작 (수동)
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
// === 실제 비즈니스 로직 시작 ===
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
Order order = Order.create(customer, items);
// 재고 확인 및 차감
for (OrderItem item : items) {
inventoryService.decreaseStock(
item.getProductId(),
item.getQuantity()
);
}
// 주문 저장
Order savedOrder = orderRepository.save(order);
// === 실제 비즈니스 로직 끝 ===
// 트랜잭션 커밋
transactionManager.commit(status);
// 로깅 - 성공
log.info("[OrderService.createOrder] 성공 - orderId: {}",
savedOrder.getId());
return savedOrder;
} catch (Exception e) {
// 트랜잭션 롤백
transactionManager.rollback(status);
throw e;
}
} catch (Exception e) {
// 로깅 - 실패
log.error("[OrderService.createOrder] 실패 - customerId: {}",
customerId, e);
throw e;
} finally {
// 로깅 - 소요시간
long elapsed = System.currentTimeMillis() - startTime;
log.info("[OrderService.createOrder] 소요시간: {}ms", elapsed);
}
}
public void cancelOrder(Long orderId) {
// 또 같은 패턴의 로깅, 트랜잭션 코드 반복...
long startTime = System.currentTimeMillis();
log.info("[OrderService.cancelOrder] 시작 - orderId: {}", orderId);
try {
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
// 비즈니스 로직
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel();
// 재고 복구
for (OrderItem item : order.getItems()) {
inventoryService.increaseStock(
item.getProductId(),
item.getQuantity()
);
}
transactionManager.commit(status);
log.info("[OrderService.cancelOrder] 성공 - orderId: {}", orderId);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
} catch (Exception e) {
log.error("[OrderService.cancelOrder] 실패 - orderId: {}", orderId, e);
throw e;
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("[OrderService.cancelOrder] 소요시간: {}ms", elapsed);
}
}
}문제점: 비즈니스 로직(약 10줄)이 로깅/트랜잭션 코드(약 30줄)에 묻혀있습니다. 새로운 메서드를 추가할 때마다 같은 패턴을 반복해야 합니다.
// 1. 인터페이스 정의
public interface OrderService {
Order createOrder(Long customerId, List<OrderItem> items);
void cancelOrder(Long orderId);
Order getOrder(Long orderId);
List<Order> getOrdersByCustomer(Long customerId);
}
// 2. 실제 구현 - 순수한 비즈니스 로직만 포함
@Service
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
private final InventoryService inventoryService;
@Override
public Order createOrder(Long customerId, List<OrderItem> items) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
Order order = Order.create(customer, items);
for (OrderItem item : items) {
inventoryService.decreaseStock(item.getProductId(), item.getQuantity());
}
return orderRepository.save(order);
}
@Override
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel();
for (OrderItem item : order.getItems()) {
inventoryService.increaseStock(item.getProductId(), item.getQuantity());
}
}
@Override
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
@Override
public List<Order> getOrdersByCustomer(Long customerId) {
return orderRepository.findByCustomerId(customerId);
}
}public class LoggingOrderServiceProxy implements OrderService {
private final OrderService target;
private final Logger log = LoggerFactory.getLogger(LoggingOrderServiceProxy.class);
public LoggingOrderServiceProxy(OrderService target) {
this.target = target;
}
@Override
public Order createOrder(Long customerId, List<OrderItem> items) {
String methodName = "createOrder";
long startTime = System.currentTimeMillis();
log.info("[{}] 시작 - customerId: {}, itemCount: {}",
methodName, customerId, items.size());
try {
Order result = target.createOrder(customerId, items);
log.info("[{}] 성공 - orderId: {}, totalAmount: {}",
methodName, result.getId(), result.getTotalAmount());
return result;
} catch (Exception e) {
log.error("[{}] 실패 - customerId: {}, error: {}",
methodName, customerId, e.getMessage(), e);
throw e;
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("[{}] 소요시간: {}ms", methodName, elapsed);
}
}
@Override
public void cancelOrder(Long orderId) {
String methodName = "cancelOrder";
long startTime = System.currentTimeMillis();
log.info("[{}] 시작 - orderId: {}", methodName, orderId);
try {
target.cancelOrder(orderId);
log.info("[{}] 성공 - orderId: {}", methodName, orderId);
} catch (Exception e) {
log.error("[{}] 실패 - orderId: {}, error: {}",
methodName, orderId, e.getMessage(), e);
throw e;
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("[{}] 소요시간: {}ms", methodName, elapsed);
}
}
@Override
public Order getOrder(Long orderId) {
log.debug("[getOrder] orderId: {}", orderId);
return target.getOrder(orderId);
}
@Override
public List<Order> getOrdersByCustomer(Long customerId) {
log.debug("[getOrdersByCustomer] customerId: {}", customerId);
return target.getOrdersByCustomer(customerId);
}
}public class TransactionalOrderServiceProxy implements OrderService {
private final OrderService target;
private final PlatformTransactionManager transactionManager;
public TransactionalOrderServiceProxy(
OrderService target,
PlatformTransactionManager transactionManager) {
this.target = target;
this.transactionManager = transactionManager;
}
@Override
public Order createOrder(Long customerId, List<OrderItem> items) {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
// 실제 비즈니스 로직 위임
Order result = target.createOrder(customerId, items);
// 성공 시 커밋
transactionManager.commit(status);
return result;
} catch (Exception e) {
// 실패 시 롤백
transactionManager.rollback(status);
throw e;
}
}
@Override
public void cancelOrder(Long orderId) {
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
target.cancelOrder(orderId);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
// 읽기 전용 메서드는 트랜잭션 없이 위임
@Override
public Order getOrder(Long orderId) {
return target.getOrder(orderId);
}
@Override
public List<Order> getOrdersByCustomer(Long customerId) {
return target.getOrdersByCustomer(customerId);
}
}@Configuration
public class OrderServiceConfig {
@Bean
public OrderService orderService(
OrderRepository orderRepository,
CustomerRepository customerRepository,
InventoryService inventoryService,
PlatformTransactionManager transactionManager) {
// 1. 실제 구현체 생성
OrderService realService = new OrderServiceImpl(
orderRepository, customerRepository, inventoryService);
// 2. 트랜잭션 Proxy로 감싸기
OrderService transactionalProxy = new TransactionalOrderServiceProxy(
realService, transactionManager);
// 3. 로깅 Proxy로 감싸기 (가장 바깥)
OrderService loggingProxy = new LoggingOrderServiceProxy(transactionalProxy);
return loggingProxy;
}
}
// 호출 흐름:
// Client -> LoggingProxy -> TransactionalProxy -> RealService
//
// createOrder() 호출 시:
// 1. LoggingProxy: 로그 시작
// 2. TransactionalProxy: 트랜잭션 시작
// 3. RealService: 비즈니스 로직 실행
// 4. TransactionalProxy: 트랜잭션 커밋 (또는 롤백)
// 5. LoggingProxy: 로그 종료, 소요시간 기록위에서 본 것처럼 수동으로 Proxy를 만들면 관심사 분리는 되지만, 여전히 문제가 있습니다.
OrderService에 메서드가 10개 있다면, LoggingProxy도 10개 메서드를 모두 구현해야 합니다. 대부분은 단순 위임 코드입니다.
OrderService, ProductService, CustomerService 각각에 대해 LoggingProxy, TransactionalProxy를 만들어야 합니다. 서비스가 100개면 Proxy도 200개가 필요합니다.
OrderService에 새 메서드가 추가되면 모든 Proxy 클래스도 수정해야 합니다.
이 문제를 해결하기 위해 Java는 런타임에 Proxy를 동적으로 생성하는 기능을 제공합니다. 다음 섹션에서 JDK Dynamic Proxy와 CGLIB에 대해 알아보겠습니다.
Java는 java.lang.reflect.Proxy 클래스를 통해 런타임에 동적으로 Proxy 객체를 생성하는 기능을 제공합니다. 이를 JDK Dynamic Proxy라고 합니다. 인터페이스 기반으로 동작하며, Spring AOP의 기본 Proxy 생성 방식입니다.
JDK Dynamic Proxy의 핵심은 InvocationHandler 인터페이스입니다. Proxy 객체의 모든 메서드 호출은 InvocationHandler의 invoke() 메서드로 전달됩니다.
public interface InvocationHandler {
/**
* Proxy 인스턴스의 메서드 호출을 처리합니다.
*
* @param proxy - Proxy 인스턴스 자체
* @param method - 호출된 메서드의 Method 객체
* @param args - 메서드에 전달된 인자들
* @return 메서드 호출 결과
*/
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}public class LoggingInvocationHandler implements InvocationHandler {
private final Object target; // 실제 객체
private final Logger log = LoggerFactory.getLogger(LoggingInvocationHandler.class);
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String className = target.getClass().getSimpleName();
String methodName = method.getName();
// Object 클래스의 메서드는 로깅하지 않음
if (method.getDeclaringClass() == Object.class) {
return method.invoke(target, args);
}
long startTime = System.currentTimeMillis();
log.info("[{}.{}] 시작 - args: {}", className, methodName, formatArgs(args));
try {
// 실제 메서드 호출
Object result = method.invoke(target, args);
log.info("[{}.{}] 성공 - result: {}", className, methodName, formatResult(result));
return result;
} catch (InvocationTargetException e) {
// 실제 예외를 꺼내서 로깅
Throwable cause = e.getTargetException();
log.error("[{}.{}] 실패 - error: {}", className, methodName, cause.getMessage());
throw cause; // 원래 예외를 다시 던짐
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("[{}.{}] 소요시간: {}ms", className, methodName, elapsed);
}
}
private String formatArgs(Object[] args) {
if (args == null || args.length == 0) {
return "없음";
}
return Arrays.stream(args)
.map(arg -> arg == null ? "null" : arg.toString())
.collect(Collectors.joining(", "));
}
private String formatResult(Object result) {
if (result == null) {
return "null";
}
if (result instanceof Collection) {
return "Collection(size=" + ((Collection<?>) result).size() + ")";
}
return result.toString();
}
}public class ProxyFactory {
/**
* 로깅 기능이 추가된 Proxy를 생성합니다.
*/
@SuppressWarnings("unchecked")
public static <T> T createLoggingProxy(T target, Class<T> interfaceType) {
return (T) Proxy.newProxyInstance(
interfaceType.getClassLoader(), // 클래스 로더
new Class<?>[] { interfaceType }, // 구현할 인터페이스들
new LoggingInvocationHandler(target) // 호출 핸들러
);
}
/**
* 여러 인터페이스를 구현하는 Proxy 생성
*/
public static Object createProxy(Object target, InvocationHandler handler) {
// target이 구현한 모든 인터페이스 가져오기
Class<?>[] interfaces = target.getClass().getInterfaces();
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
interfaces,
handler
);
}
}
// 사용 예시
public class Application {
public static void main(String[] args) {
// 1. 실제 서비스 생성
OrderService realService = new OrderServiceImpl(
orderRepository, customerRepository, inventoryService);
// 2. 로깅 Proxy 생성
OrderService proxyService = ProxyFactory.createLoggingProxy(
realService, OrderService.class);
// 3. Proxy를 통해 호출 - 자동으로 로깅됨
Order order = proxyService.createOrder(1L, items);
// 출력:
// [OrderServiceImpl.createOrder] 시작 - args: 1, [OrderItem(...)]
// [OrderServiceImpl.createOrder] 성공 - result: Order(id=1, ...)
// [OrderServiceImpl.createOrder] 소요시간: 45ms
}
}public class TransactionalInvocationHandler implements InvocationHandler {
private final Object target;
private final PlatformTransactionManager transactionManager;
private final Set<String> transactionalMethods;
public TransactionalInvocationHandler(
Object target,
PlatformTransactionManager transactionManager,
String... methodNames) {
this.target = target;
this.transactionManager = transactionManager;
this.transactionalMethods = new HashSet<>(Arrays.asList(methodNames));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// 트랜잭션이 필요한 메서드인지 확인
if (!transactionalMethods.contains(methodName)) {
return method.invoke(target, args);
}
// 트랜잭션 시작
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
Object result = method.invoke(target, args);
transactionManager.commit(status);
return result;
} catch (InvocationTargetException e) {
transactionManager.rollback(status);
throw e.getTargetException();
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
// 사용 예시
OrderService transactionalProxy = (OrderService) Proxy.newProxyInstance(
OrderService.class.getClassLoader(),
new Class<?>[] { OrderService.class },
new TransactionalInvocationHandler(
realService,
transactionManager,
"createOrder", "cancelOrder", "updateOrder" // 트랜잭션 적용할 메서드들
)
);/**
* 여러 InvocationHandler를 체인으로 연결하는 핸들러
*/
public class ChainedInvocationHandler implements InvocationHandler {
private final Object target;
private final List<InvocationHandler> handlers;
public ChainedInvocationHandler(Object target, List<InvocationHandler> handlers) {
this.target = target;
this.handlers = handlers;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 체인의 첫 번째 핸들러부터 시작
return invokeChain(0, proxy, method, args);
}
private Object invokeChain(int index, Object proxy, Method method, Object[] args)
throws Throwable {
if (index >= handlers.size()) {
// 모든 핸들러를 거쳤으면 실제 메서드 호출
return method.invoke(target, args);
}
// 현재 핸들러에게 다음 핸들러를 호출하는 책임 위임
// (실제로는 더 복잡한 구현이 필요하지만 개념 설명용)
return handlers.get(index).invoke(proxy, method, args);
}
}
// 실제 Spring AOP는 이보다 훨씬 정교한 체이닝 메커니즘을 사용합니다.
// MethodInterceptor와 MethodInvocation을 통해 구현됩니다.JDK Dynamic Proxy는 인터페이스를 구현한 클래스만 프록시할 수 있습니다. 인터페이스 없이 클래스만 있는 경우 사용할 수 없습니다.
// 가능 - 인터페이스 있음
public interface OrderService { ... }
public class OrderServiceImpl implements OrderService { ... }
// 불가능 - 인터페이스 없음
public class OrderService { ... } // JDK Dynamic Proxy 사용 불가인터페이스에 정의된 메서드만 프록시됩니다. 구현 클래스에만 있는 메서드는 프록시를 통해 호출할 수 없습니다.
리플렉션을 사용하므로 직접 호출보다 약간의 성능 오버헤드가 있습니다. 하지만 대부분의 애플리케이션에서는 무시할 수 있는 수준입니다.
CGLIB(Code Generation Library)는 바이트코드 조작을 통해 클래스를 상속받는 Proxy를 생성합니다. 인터페이스 없이도 Proxy를 만들 수 있어 JDK Dynamic Proxy의 한계를 극복합니다. Spring Boot 2.0부터는 CGLIB가 기본 Proxy 생성 방식입니다.
interface OrderService
^
|
OrderServiceImpl <-- Proxy
(implements) (implements)OrderService
^
|
OrderService$$EnhancerByCGLIB
(extends, Proxy)import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
/**
* CGLIB의 MethodInterceptor - JDK의 InvocationHandler와 유사한 역할
*/
public class LoggingMethodInterceptor implements MethodInterceptor {
private final Logger log = LoggerFactory.getLogger(LoggingMethodInterceptor.class);
/**
* @param obj - Proxy 객체 (CGLIB가 생성한 서브클래스 인스턴스)
* @param method - 호출된 메서드
* @param args - 메서드 인자
* @param proxy - 부모 클래스의 메서드를 호출할 수 있는 MethodProxy
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
String methodName = method.getName();
// Object 클래스의 메서드는 그냥 통과
if (method.getDeclaringClass() == Object.class) {
return proxy.invokeSuper(obj, args);
}
long startTime = System.currentTimeMillis();
log.info("[{}] 시작 - args: {}", methodName, Arrays.toString(args));
try {
// 부모 클래스(실제 클래스)의 메서드 호출
// proxy.invokeSuper()는 리플렉션보다 빠름
Object result = proxy.invokeSuper(obj, args);
log.info("[{}] 성공 - result: {}", methodName, result);
return result;
} catch (Throwable e) {
log.error("[{}] 실패 - error: {}", methodName, e.getMessage());
throw e;
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("[{}] 소요시간: {}ms", methodName, elapsed);
}
}
}public class CglibProxyFactory {
/**
* CGLIB를 사용하여 Proxy 생성
*/
@SuppressWarnings("unchecked")
public static <T> T createProxy(Class<T> targetClass, MethodInterceptor interceptor) {
Enhancer enhancer = new Enhancer();
// 상속받을 클래스 지정
enhancer.setSuperclass(targetClass);
// 메서드 호출을 가로챌 인터셉터 지정
enhancer.setCallback(interceptor);
// Proxy 인스턴스 생성
return (T) enhancer.create();
}
/**
* 생성자 인자가 있는 경우
*/
@SuppressWarnings("unchecked")
public static <T> T createProxy(
Class<T> targetClass,
MethodInterceptor interceptor,
Class<?>[] argumentTypes,
Object[] arguments) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(interceptor);
// 생성자 인자와 함께 생성
return (T) enhancer.create(argumentTypes, arguments);
}
}
// 사용 예시 - 인터페이스 없는 클래스도 프록시 가능
public class OrderService { // 인터페이스 없음!
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order createOrder(Long customerId, List<OrderItem> items) {
// 비즈니스 로직
return orderRepository.save(new Order(customerId, items));
}
}
// Proxy 생성
OrderService proxy = CglibProxyFactory.createProxy(
OrderService.class,
new LoggingMethodInterceptor(),
new Class<?>[] { OrderRepository.class },
new Object[] { orderRepository }
);
// 호출 - 로깅이 자동으로 적용됨
Order order = proxy.createOrder(1L, items);CGLIB는 상속을 통해 Proxy를 생성하므로, final 클래스나 final 메서드는 프록시할 수 없습니다.
// 불가능 - final 클래스
public final class OrderService { ... }
// 불가능 - final 메서드
public class OrderService {
public final Order createOrder(...) { ... } // 프록시 안됨
}Spring 4.0 이전에는 CGLIB Proxy 생성 시 기본 생성자가 필요했습니다. Spring 4.0부터는 Objenesis 라이브러리를 사용하여 이 제약이 해결되었습니다.
CGLIB는 상속을 사용하므로 Proxy 생성 시 부모 클래스의 생성자가 호출됩니다. Spring 4.0부터는 Objenesis로 이 문제도 해결되었습니다.
// application.yml에서 설정
spring:
aop:
proxy-target-class: true # true: CGLIB 강제 (기본값)
# false: 인터페이스 있으면 JDK Dynamic Proxy
// 또는 Java Config에서 설정
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLIB 강제
public class AopConfig {
}
// Spring Boot 2.0 이후 기본 동작:
// 1. 인터페이스가 있어도 CGLIB 사용 (proxyTargetClass=true가 기본)
// 2. 이유: 일관된 동작, 인터페이스 없는 클래스도 프록시 가능
// 확인 방법
@Service
public class DebugService {
@Autowired
private OrderService orderService;
public void checkProxy() {
System.out.println("Proxy 클래스: " + orderService.getClass().getName());
// 출력 예: com.shop.service.OrderService$$EnhancerBySpringCGLIB$$abc123
System.out.println("CGLIB Proxy인가? " +
orderService.getClass().getName().contains("CGLIB"));
}
}Spring Framework의 많은 기능들이 Proxy를 통해 구현됩니다. @Transactional, @Async, @Cacheable 등의 어노테이션이 동작하는 원리를 이해하면 Spring을 더 효과적으로 사용할 수 있습니다.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
@Transactional // Proxy가 이 메서드를 감싸서 트랜잭션 처리
public Order createOrder(Long customerId, List<OrderItem> items) {
Order order = Order.create(customerId, items);
orderRepository.save(order);
paymentService.processPayment(order);
return order;
}
}
// Spring이 생성하는 Proxy의 동작 (개념적 코드)
public class OrderService$$SpringCGLIB extends OrderService {
private final OrderService target;
private final PlatformTransactionManager txManager;
@Override
public Order createOrder(Long customerId, List<OrderItem> items) {
// 1. @Transactional 어노테이션 확인
TransactionAttribute txAttr = getTransactionAttribute(method);
// 2. 트랜잭션 시작
TransactionStatus status = txManager.getTransaction(txAttr);
try {
// 3. 실제 메서드 호출
Order result = super.createOrder(customerId, items);
// 4. 커밋
txManager.commit(status);
return result;
} catch (RuntimeException e) {
// 5. 롤백 (RuntimeException은 기본적으로 롤백)
txManager.rollback(status);
throw e;
} catch (Exception e) {
// 6. Checked Exception은 기본적으로 커밋
txManager.commit(status);
throw e;
}
}
}Proxy를 거치지 않으면 @Transactional이 적용되지 않습니다.
@Service
public class OrderService {
public void processOrder(Long orderId) {
// 같은 클래스 내에서 this로 호출 - Proxy를 거치지 않음!
this.createOrder(orderId); // @Transactional 적용 안됨
}
@Transactional
public void createOrder(Long orderId) {
// 트랜잭션이 시작되지 않음!
orderRepository.save(...);
}
}
// 해결 방법 1: 별도 클래스로 분리
@Service
public class OrderService {
private final OrderTransactionService txService;
public void processOrder(Long orderId) {
txService.createOrder(orderId); // Proxy를 통해 호출
}
}
@Service
public class OrderTransactionService {
@Transactional
public void createOrder(Long orderId) {
// 트랜잭션 정상 동작
}
}
// 해결 방법 2: Self-injection (권장하지 않음)
@Service
public class OrderService {
@Autowired
private OrderService self; // Proxy가 주입됨
public void processOrder(Long orderId) {
self.createOrder(orderId); // Proxy를 통해 호출
}
}@Service
public class NotificationService {
@Async // Proxy가 별도 스레드에서 실행
public CompletableFuture<Void> sendEmailAsync(String to, String content) {
// 이메일 발송 (시간이 오래 걸림)
emailSender.send(to, content);
return CompletableFuture.completedFuture(null);
}
@Async("customExecutor") // 특정 Executor 사용
public void sendSmsAsync(String phoneNumber, String message) {
smsSender.send(phoneNumber, message);
}
}
// Spring이 생성하는 Proxy의 동작 (개념적 코드)
public class NotificationService$$SpringCGLIB extends NotificationService {
private final AsyncTaskExecutor executor;
@Override
public CompletableFuture<Void> sendEmailAsync(String to, String content) {
// 별도 스레드에서 실행
return CompletableFuture.supplyAsync(() -> {
return super.sendEmailAsync(to, content);
}, executor);
}
}
// 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("customExecutor")
public Executor customExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}@Service
public class ProductService {
@Cacheable(value = "products", key = "#productId")
public Product getProduct(Long productId) {
log.info("DB에서 상품 조회: {}", productId);
return productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
}
@CacheEvict(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
@CachePut(value = "products", key = "#result.id")
public Product createProduct(CreateProductCommand command) {
Product product = Product.create(command);
return productRepository.save(product);
}
}
// Spring이 생성하는 Proxy의 동작 (개념적 코드)
public class ProductService$$SpringCGLIB extends ProductService {
private final CacheManager cacheManager;
@Override
public Product getProduct(Long productId) {
Cache cache = cacheManager.getCache("products");
String key = String.valueOf(productId);
// 1. 캐시에서 먼저 조회
Product cached = cache.get(key, Product.class);
if (cached != null) {
return cached; // 캐시 히트 - DB 조회 안함
}
// 2. 캐시 미스 - 실제 메서드 호출
Product result = super.getProduct(productId);
// 3. 결과를 캐시에 저장
cache.put(key, result);
return result;
}
}
// 설정
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10)));
return cacheManager;
}
}| 어노테이션 | Proxy가 하는 일 | 활성화 설정 |
|---|---|---|
| @Transactional | 트랜잭션 시작/커밋/롤백 | @EnableTransactionManagement |
| @Async | 별도 스레드에서 비동기 실행 | @EnableAsync |
| @Cacheable | 캐시 조회/저장 | @EnableCaching |
| @Retryable | 실패 시 재시도 | @EnableRetry |
| @Secured | 권한 검사 | @EnableGlobalMethodSecurity |
| @Validated | 메서드 파라미터 검증 | 자동 활성화 |
Decorator 패턴은 객체에 동적으로 새로운 책임을 추가합니다. Proxy와 구조는 비슷하지만 목적이 다릅니다. Proxy는 접근 제어가 목적이고, Decorator는 기능 확장이 목적입니다.
배송비 계산에는 여러 규칙이 적용됩니다: 기본 배송비, 무료 배송 조건, 도서산간 추가 요금, 회원 등급 할인 등. Decorator 패턴으로 이 규칙들을 조합할 수 있습니다.
// 배송비 계산 인터페이스
public interface ShippingCalculator {
Money calculate(Order order);
}
// 기본 구현 - 거리 기반 배송비
@Component
@Primary
public class BasicShippingCalculator implements ShippingCalculator {
private static final Money BASE_FEE = Money.of(3000);
private static final Money PER_KM_FEE = Money.of(100);
@Override
public Money calculate(Order order) {
Address address = order.getShippingAddress();
int distanceKm = calculateDistance(address);
if (distanceKm <= 10) {
return BASE_FEE;
}
// 10km 초과 시 km당 100원 추가
int extraKm = distanceKm - 10;
return BASE_FEE.add(PER_KM_FEE.multiply(extraKm));
}
private int calculateDistance(Address address) {
// 실제로는 지도 API를 사용하여 거리 계산
return 15; // 예시
}
}// Decorator 기본 클래스
public abstract class ShippingCalculatorDecorator implements ShippingCalculator {
protected final ShippingCalculator delegate;
protected ShippingCalculatorDecorator(ShippingCalculator delegate) {
this.delegate = delegate;
}
@Override
public Money calculate(Order order) {
return delegate.calculate(order);
}
}// Decorator 1: 무료 배송 조건 적용
public class FreeShippingDecorator extends ShippingCalculatorDecorator {
private final Money freeShippingThreshold;
public FreeShippingDecorator(ShippingCalculator delegate, Money threshold) {
super(delegate);
this.freeShippingThreshold = threshold;
}
@Override
public Money calculate(Order order) {
// 주문 금액이 기준 이상이면 무료 배송
if (order.getTotalAmount().isGreaterThanOrEqual(freeShippingThreshold)) {
return Money.ZERO;
}
return delegate.calculate(order);
}
}
// Decorator 2: 도서산간 추가 요금
public class RemoteAreaDecorator extends ShippingCalculatorDecorator {
private static final Money REMOTE_AREA_FEE = Money.of(3000);
private static final Set<String> REMOTE_AREAS = Set.of(
"제주특별자치도", "울릉군", "옹진군"
);
public RemoteAreaDecorator(ShippingCalculator delegate) {
super(delegate);
}
@Override
public Money calculate(Order order) {
Money baseFee = delegate.calculate(order);
// 이미 무료 배송이면 도서산간 요금만 부과
if (baseFee.equals(Money.ZERO)) {
if (isRemoteArea(order.getShippingAddress())) {
return REMOTE_AREA_FEE;
}
return Money.ZERO;
}
// 도서산간 지역이면 추가 요금
if (isRemoteArea(order.getShippingAddress())) {
return baseFee.add(REMOTE_AREA_FEE);
}
return baseFee;
}
private boolean isRemoteArea(Address address) {
return REMOTE_AREAS.stream()
.anyMatch(area -> address.getRegion().contains(area));
}
}
// Decorator 3: 회원 등급별 배송비 할인
public class MemberDiscountDecorator extends ShippingCalculatorDecorator {
public MemberDiscountDecorator(ShippingCalculator delegate) {
super(delegate);
}
@Override
public Money calculate(Order order) {
Money baseFee = delegate.calculate(order);
if (baseFee.equals(Money.ZERO)) {
return Money.ZERO;
}
Customer customer = order.getCustomer();
double discountRate = getDiscountRate(customer.getGrade());
return baseFee.multiply(1 - discountRate);
}
private double getDiscountRate(CustomerGrade grade) {
return switch (grade) {
case VIP -> 0.5; // 50% 할인
case GOLD -> 0.3; // 30% 할인
case SILVER -> 0.1; // 10% 할인
default -> 0.0; // 할인 없음
};
}
}
// Decorator 4: 특정 상품 무료 배송
public class ProductFreeShippingDecorator extends ShippingCalculatorDecorator {
private final Set<Long> freeShippingProductIds;
public ProductFreeShippingDecorator(
ShippingCalculator delegate,
Set<Long> freeShippingProductIds) {
super(delegate);
this.freeShippingProductIds = freeShippingProductIds;
}
@Override
public Money calculate(Order order) {
// 무료 배송 상품이 포함되어 있으면 무료
boolean hasFreeShippingProduct = order.getItems().stream()
.anyMatch(item -> freeShippingProductIds.contains(item.getProductId()));
if (hasFreeShippingProduct) {
return Money.ZERO;
}
return delegate.calculate(order);
}
}@Configuration
public class ShippingConfig {
@Bean
public ShippingCalculator shippingCalculator(
@Value("${shipping.free-threshold:50000}") long freeThreshold,
@Value("${shipping.free-products:}") Set<Long> freeShippingProducts) {
// Decorator 체인 구성 (안쪽부터 바깥쪽 순서로)
// 실행 순서: 회원할인 -> 도서산간 -> 상품무료 -> 금액무료 -> 기본계산
ShippingCalculator calculator = new BasicShippingCalculator();
// 1. 금액 기준 무료 배송
calculator = new FreeShippingDecorator(
calculator,
Money.of(freeThreshold)
);
// 2. 특정 상품 무료 배송
if (!freeShippingProducts.isEmpty()) {
calculator = new ProductFreeShippingDecorator(
calculator,
freeShippingProducts
);
}
// 3. 도서산간 추가 요금
calculator = new RemoteAreaDecorator(calculator);
// 4. 회원 등급 할인 (가장 마지막에 적용)
calculator = new MemberDiscountDecorator(calculator);
return calculator;
}
}
// 사용하는 곳에서는 조합을 몰라도 됨
@Service
@RequiredArgsConstructor
public class OrderService {
private final ShippingCalculator shippingCalculator;
public Order createOrder(CreateOrderCommand command) {
Order order = Order.create(command);
// 배송비 계산 - 모든 규칙이 자동 적용됨
Money shippingFee = shippingCalculator.calculate(order);
order.setShippingFee(shippingFee);
return orderRepository.save(order);
}
}Java의 InputStream/OutputStream은 Decorator 패턴의 대표적인 예입니다. 기본 스트림에 버퍼링, 압축, 암호화 등의 기능을 동적으로 추가할 수 있습니다.
// Java I/O Decorator 체인
InputStream is = new BufferedInputStream( // 버퍼링 추가
new GZIPInputStream( // 압축 해제 추가
new FileInputStream("data.gz") // 기본 파일 읽기
)
);
// 읽기 - 모든 Decorator가 순서대로 적용됨
// FileInputStream -> GZIPInputStream -> BufferedInputStream -> 사용자
byte[] data = is.readAllBytes();
// Spring의 비슷한 패턴
RestTemplate restTemplate = new RestTemplateBuilder()
.interceptors(new LoggingInterceptor()) // 로깅 추가
.interceptors(new RetryInterceptor()) // 재시도 추가
.interceptors(new AuthInterceptor()) // 인증 추가
.build();Observer 패턴은 객체 간의 일대다 의존 관계를 정의합니다. 한 객체의 상태가 변경되면 의존하는 모든 객체에게 자동으로 알림이 갑니다. Spring의 ApplicationEvent가 대표적인 구현입니다.
@Service
public class OrderService {
// 너무 많은 의존성 - 주문 완료 후 해야 할 일이 늘어날 때마다 추가됨
private final OrderRepository orderRepository;
private final EmailService emailService;
private final SmsService smsService;
private final PushNotificationService pushService;
private final InventoryService inventoryService;
private final PointService pointService;
private final AnalyticsService analyticsService;
private final RecommendationService recommendationService;
private final PartnerNotificationService partnerService;
@Transactional
public Order completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.complete();
orderRepository.save(order);
// 주문 완료 후 해야 할 일들이 모두 여기에 나열됨
// 새로운 기능 추가할 때마다 이 클래스를 수정해야 함!
// 1. 알림 발송
emailService.sendOrderConfirmation(order);
smsService.sendOrderNotification(order);
pushService.sendOrderPush(order);
// 2. 재고 차감
for (OrderItem item : order.getItems()) {
inventoryService.decreaseStock(item.getProductId(), item.getQuantity());
}
// 3. 포인트 적립
int points = calculatePoints(order);
pointService.addPoints(order.getCustomerId(), points);
// 4. 분석 데이터 전송
analyticsService.trackOrderCompleted(order);
// 5. 추천 시스템 업데이트
recommendationService.updatePurchaseHistory(order);
// 6. 파트너사 알림 (마켓플레이스인 경우)
if (order.hasPartnerProducts()) {
partnerService.notifyPartners(order);
}
return order;
}
}문제점: OrderService가 너무 많은 책임을 가지고 있습니다. 새로운 후처리 로직이 추가될 때마다 OrderService를 수정해야 합니다. 또한 하나의 후처리가 실패하면 전체 트랜잭션이 롤백됩니다.
// 1. 이벤트 정의
public record OrderCompletedEvent(
Long orderId,
Long customerId,
Money totalAmount,
List<OrderItemInfo> items,
LocalDateTime completedAt
) {
// 이벤트에 필요한 정보만 포함
// Order 엔티티를 직접 전달하지 않음 (영속성 컨텍스트 문제 방지)
public record OrderItemInfo(
Long productId,
int quantity,
Money price
) {}
public static OrderCompletedEvent from(Order order) {
List<OrderItemInfo> itemInfos = order.getItems().stream()
.map(item -> new OrderItemInfo(
item.getProductId(),
item.getQuantity(),
item.getPrice()
))
.toList();
return new OrderCompletedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount(),
itemInfos,
LocalDateTime.now()
);
}
}
// 2. 이벤트 발행 - OrderService는 이벤트만 발행
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.complete();
Order savedOrder = orderRepository.save(order);
// 이벤트 발행 - 누가 처리하는지 몰라도 됨
eventPublisher.publishEvent(OrderCompletedEvent.from(savedOrder));
return savedOrder;
}
}// 알림 리스너
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderNotificationListener {
private final EmailService emailService;
private final SmsService smsService;
private final CustomerRepository customerRepository;
@EventListener
public void sendEmail(OrderCompletedEvent event) {
log.info("주문 완료 이메일 발송 - orderId: {}", event.orderId());
Customer customer = customerRepository.findById(event.customerId())
.orElseThrow();
emailService.sendOrderConfirmation(
customer.getEmail(),
event.orderId(),
event.totalAmount()
);
}
@EventListener
public void sendSms(OrderCompletedEvent event) {
log.info("주문 완료 SMS 발송 - orderId: {}", event.orderId());
Customer customer = customerRepository.findById(event.customerId())
.orElseThrow();
smsService.send(
customer.getPhoneNumber(),
String.format("주문이 완료되었습니다. 주문번호: %d", event.orderId())
);
}
}
// 재고 리스너
@Component
@RequiredArgsConstructor
@Slf4j
public class InventoryEventListener {
private final InventoryService inventoryService;
@EventListener
public void decreaseStock(OrderCompletedEvent event) {
log.info("재고 차감 시작 - orderId: {}", event.orderId());
for (OrderCompletedEvent.OrderItemInfo item : event.items()) {
inventoryService.decrease(item.productId(), item.quantity());
}
log.info("재고 차감 완료 - orderId: {}", event.orderId());
}
}
// 포인트 리스너
@Component
@RequiredArgsConstructor
@Slf4j
public class PointEventListener {
private final PointService pointService;
@EventListener
public void addPoints(OrderCompletedEvent event) {
// 결제 금액의 1% 포인트 적립
int points = event.totalAmount().getValue().intValue() / 100;
log.info("포인트 적립 - customerId: {}, points: {}",
event.customerId(), points);
pointService.add(event.customerId(), points);
}
}
// 분석 리스너 - 비동기 처리
@Component
@RequiredArgsConstructor
@Slf4j
public class AnalyticsEventListener {
private final AnalyticsService analyticsService;
@Async // 별도 스레드에서 비동기 실행
@EventListener
public void trackOrder(OrderCompletedEvent event) {
log.info("분석 데이터 전송 - orderId: {}", event.orderId());
analyticsService.track("order_completed", Map.of(
"orderId", event.orderId(),
"customerId", event.customerId(),
"amount", event.totalAmount().getValue(),
"itemCount", event.items().size()
));
}
}@EventListener는 이벤트 발행 시점에 즉시 실행됩니다. 트랜잭션이 롤백되어도 이미 실행된 리스너는 취소되지 않습니다. @TransactionalEventListener를 사용하면 트랜잭션 상태에 따라 실행 시점을 제어할 수 있습니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class TransactionalOrderEventListener {
private final NotificationService notificationService;
private final ExternalApiClient externalApiClient;
// 트랜잭션 커밋 후에 실행 (기본값)
// 주문이 확실히 저장된 후에 알림 발송
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendNotificationAfterCommit(OrderCompletedEvent event) {
log.info("트랜잭션 커밋 후 알림 발송 - orderId: {}", event.orderId());
// 이 시점에서는 주문이 확실히 DB에 저장됨
notificationService.send(event);
}
// 트랜잭션 롤백 후에 실행
// 주문 실패 시 보상 처리
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleRollback(OrderCompletedEvent event) {
log.warn("트랜잭션 롤백 - orderId: {}", event.orderId());
// 롤백 시 필요한 보상 처리
// 예: 외부 시스템에 취소 요청
}
// 트랜잭션 완료 후에 실행 (커밋/롤백 무관)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void cleanup(OrderCompletedEvent event) {
log.info("트랜잭션 완료 후 정리 - orderId: {}", event.orderId());
// 임시 데이터 정리 등
}
// 트랜잭션 커밋 전에 실행
// 같은 트랜잭션 내에서 추가 작업 필요할 때
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void validateBeforeCommit(OrderCompletedEvent event) {
log.info("커밋 전 검증 - orderId: {}", event.orderId());
// 외부 시스템 검증 등
// 여기서 예외 발생 시 트랜잭션 롤백됨
}
}
// TransactionPhase 정리
// BEFORE_COMMIT: 커밋 직전 (같은 트랜잭션)
// AFTER_COMMIT: 커밋 후 (기본값, 가장 많이 사용)
// AFTER_ROLLBACK: 롤백 후
// AFTER_COMPLETION: 완료 후 (커밋/롤백 무관)@Component
public class OrderedEventListeners {
@EventListener
@Order(1) // 가장 먼저 실행
public void validateFirst(OrderCompletedEvent event) {
log.info("1. 검증 - orderId: {}", event.orderId());
// 필수 검증 로직
if (event.items().isEmpty()) {
throw new IllegalStateException("주문 항목이 없습니다");
}
}
@EventListener
@Order(2)
public void processSecond(OrderCompletedEvent event) {
log.info("2. 핵심 처리 - orderId: {}", event.orderId());
// 핵심 비즈니스 로직
}
@EventListener
@Order(3) // 가장 나중에 실행
public void notifyLast(OrderCompletedEvent event) {
log.info("3. 알림 - orderId: {}", event.orderId());
// 알림은 마지막에
}
}
// 조건부 이벤트 처리
@Component
public class ConditionalEventListener {
// SpEL 조건으로 필터링
@EventListener(condition = "#event.totalAmount.value > 100000")
public void handleHighValueOrder(OrderCompletedEvent event) {
log.info("고가 주문 처리 - orderId: {}, amount: {}",
event.orderId(), event.totalAmount());
// VIP 고객 처리 등
}
// 특정 조건에서만 실행
@EventListener(condition = "#event.items.size() >= 5")
public void handleBulkOrder(OrderCompletedEvent event) {
log.info("대량 주문 처리 - orderId: {}, itemCount: {}",
event.orderId(), event.items().size());
}
}Adapter 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있게 합니다. 외부 라이브러리나 레거시 시스템을 통합할 때 자주 사용됩니다. 기존 코드를 수정하지 않고 새로운 시스템과 연동할 수 있습니다.
여러 PG사(Payment Gateway)를 통합해야 하는 상황을 생각해봅시다. 각 PG사마다 SDK의 인터페이스가 다릅니다. Adapter 패턴으로 우리 시스템의 인터페이스에 맞게 변환할 수 있습니다.
// 우리 시스템에서 사용할 통합 결제 인터페이스
public interface PaymentGateway {
PaymentResult process(PaymentRequest request);
PaymentStatus checkStatus(String transactionId);
RefundResult refund(String transactionId, Money amount);
}
public record PaymentRequest(
String orderId,
Money amount,
String customerName,
String customerEmail,
PaymentMethod method
) {}
public record PaymentResult(
String transactionId,
PaymentStatus status,
LocalDateTime approvedAt,
String message
) {}public class TossPaymentsSdk {
public TossPaymentResponse requestPayment(TossPaymentRequest request) { }
public TossStatusResponse getPaymentByPaymentKey(String paymentKey) { }
public TossCancelResponse cancelPayment(String paymentKey, TossCancelRequest req) { }
}
public class TossPaymentRequest {
private int amount;
private String orderId;
private String orderName;
private String successUrl;
private String failUrl;
}@Component("tossPaymentGateway")
@RequiredArgsConstructor
public class TossPaymentsAdapter implements PaymentGateway {
private final TossPaymentsSdk tossSdk;
@Override
public PaymentResult process(PaymentRequest request) {
// 1. 우리 요청을 Toss 형식으로 변환
TossPaymentRequest tossRequest = TossPaymentRequest.builder()
.amount(request.amount().getValue().intValue())
.orderId(request.orderId())
.orderName("주문 " + request.orderId())
.build();
// 2. Toss SDK 호출
TossPaymentResponse response = tossSdk.requestPayment(tossRequest);
// 3. Toss 응답을 우리 형식으로 변환
return new PaymentResult(
response.getPaymentKey(),
mapTossStatus(response.getStatus()),
parseDateTime(response.getApprovedAt()),
"결제 완료"
);
}
@Override
public PaymentStatus checkStatus(String transactionId) {
TossStatusResponse response = tossSdk.getPaymentByPaymentKey(transactionId);
return mapTossStatus(response.getStatus());
}
@Override
public RefundResult refund(String transactionId, Money amount) {
TossCancelResponse response = tossSdk.cancelPayment(
transactionId,
new TossCancelRequest("고객 요청")
);
return new RefundResult(
Money.of(response.getCancelAmount()),
response.getCanceledAt()
);
}
private PaymentStatus mapTossStatus(String tossStatus) {
return switch (tossStatus) {
case "DONE" -> PaymentStatus.COMPLETED;
case "CANCELED" -> PaymentStatus.REFUNDED;
case "WAITING_FOR_DEPOSIT" -> PaymentStatus.PENDING;
default -> PaymentStatus.FAILED;
};
}
}// Spring MVC의 HandlerAdapter 인터페이스
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
}
// @Controller 메서드를 처리하는 Adapter
public class RequestMappingHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}
@Override
public ModelAndView handle(...) {
// @RequestMapping 메서드 호출, 파라미터 바인딩 등
}
}Facade 패턴은 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공합니다. 클라이언트는 복잡한 내부 구조를 알 필요 없이 Facade만 사용하면 됩니다. DDD의 Application Service가 대표적인 Facade입니다.
@RestController
public class OrderController {
// 너무 많은 의존성
private final CustomerService customerService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final DiscountService discountService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final OrderRepository orderRepository;
@PostMapping("/orders")
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// 컨트롤러가 비즈니스 로직을 조율해야 함
Customer customer = customerService.findById(request.getCustomerId());
List<Product> products = productService.findByIds(request.getProductIds());
for (Product product : products) {
if (!inventoryService.isAvailable(product.getId(), request.getQuantity())) {
throw new OutOfStockException(product.getId());
}
}
Money subtotal = pricingService.calculate(products);
Money discount = discountService.apply(customer, subtotal);
Money shipping = shippingService.calculate(request.getAddress());
Money total = subtotal.subtract(discount).add(shipping);
PaymentResult payment = paymentService.process(customer, total);
Order order = new Order(...);
orderRepository.save(order);
inventoryService.decrease(...);
return new OrderResponse(order);
}
}// Facade - 복잡한 주문 프로세스를 단순화
@Service
@RequiredArgsConstructor
public class OrderFacade {
private final CustomerService customerService;
private final ProductService productService;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final PaymentService paymentService;
private final OrderRepository orderRepository;
@Transactional
public OrderResult placeOrder(OrderCommand command) {
// 1. 고객 및 상품 조회
Customer customer = customerService.getById(command.customerId());
List<OrderLine> orderLines = createOrderLines(command.items());
// 2. 재고 확인
validateStock(orderLines);
// 3. 가격 계산
PriceCalculation price = calculatePrice(customer, orderLines);
// 4. 결제 처리
PaymentResult payment = processPayment(customer, price.total());
// 5. 주문 생성 및 저장
Order order = createAndSaveOrder(customer, orderLines, price, payment);
// 6. 재고 차감
decreaseInventory(orderLines);
return OrderResult.success(order);
}
private PriceCalculation calculatePrice(Customer customer, List<OrderLine> lines) {
Money subtotal = pricingService.calculate(lines);
Money discount = pricingService.applyDiscount(customer, subtotal);
return new PriceCalculation(subtotal, discount);
}
}
// Controller는 Facade만 의존
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderFacade orderFacade;
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
OrderCommand command = request.toCommand();
OrderResult result = orderFacade.placeOrder(command);
return ResponseEntity.ok(OrderResponse.from(result));
}
}| 패턴 | 목적 | Spring 적용 |
|---|---|---|
| Proxy | 접근 제어, 부가 기능 | @Transactional, AOP, @Async |
| JDK Dynamic Proxy | 인터페이스 기반 Proxy | InvocationHandler |
| CGLIB | 클래스 기반 Proxy | Spring Boot 기본 방식 |
| Decorator | 동적 기능 확장 | Interceptor, Filter |
| Observer | 이벤트 기반 통신 | ApplicationEvent |
| Adapter | 인터페이스 변환 | HandlerAdapter |
| Facade | 복잡성 숨기기 | Application Service |
@Transactional, @Async, @Cacheable 등 대부분의 Spring 기능이 Proxy로 동작합니다. Proxy를 이해하면 Spring의 동작 원리가 보입니다.
같은 클래스 내에서 this.method()로 호출하면 Proxy를 거치지 않아 @Transactional 등이 동작하지 않습니다.
Observer 패턴(Spring Event)을 사용하면 서비스 간 의존성을 제거하고 확장성 있는 구조를 만들 수 있습니다.
Application Service를 Facade로 사용하여 컨트롤러와 도메인 사이의 복잡성을 관리합니다.
다음 세션 예고: Spring 03에서는 IoC/DI의 원리와 Spring Container의 동작 방식을 깊이 있게 다룹니다.