Featured image of post [Design Patterns] 어댑터와 파사드: 인터페이스의 철학

[Design Patterns] 어댑터와 파사드: 인터페이스의 철학

호환되지 않는 인터페이스를 연결하는 Adapter와 복잡한 시스템을 단순화하는 Facade 패턴의 철학과 실무 적용을 탐구합니다. 레거시 시스템 통합, API 래핑, 시스템 간 브릿지 구축 등 실제 개발 현장에서 마주치는 인터페이스 설계 문제에 대한 우아한 해결책을 제시합니다.

Adapter와 Facade 패턴을 통해 인터페이스 설계의 철학을 탐구합니다. 시스템 간 호환성 문제를 해결하고, 복잡한 서브시스템을 단순화하는 방법을 학습합니다.

서론: 시스템 통합의 영원한 딜레마

“소프트웨어 시스템은 홀로 존재하지 않는다. 모든 시스템은 다른 시스템과 소통해야 하고, 그 소통의 핵심은 인터페이스다.”

현대 소프트웨어 개발에서 완전히 독립적인 시스템은 존재하지 않습니다. 우리는 항상 다른 시스템과 통합해야 하는 상황에 직면합니다:

  • 새로운 결제 시스템을 기존 이커머스 플랫폼에 통합
  • 레거시 메인프레임 시스템과 최신 웹 애플리케이션 연동
  • 다양한 써드파티 API를 하나의 일관된 인터페이스로 통합
  • 마이크로서비스 간의 복잡한 통신 관리

이런 상황에서 Adapter와 Facade 패턴은 서로 다른 철학으로 해결책을 제시합니다:

Adapter의 철학: “다름을 연결하는 다리”

  • 호환성: 서로 다른 인터페이스를 연결
  • 변환: 한 형태에서 다른 형태로 변환
  • 보존: 기존 시스템의 변경 없이 통합
  • 적응: 환경 변화에 유연하게 대응

Facade의 철학: “복잡함을 단순함으로”

  • 단순화: 복잡한 서브시스템을 간단한 인터페이스로 제공
  • 추상화: 구현 세부사항을 숨김
  • 조화: 여러 컴포넌트를 하나의 일관된 서비스로 통합
  • 보호: 클라이언트를 복잡성으로부터 보호
 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
// 현실적인 문제 상황
public class PaymentService {
    // 문제: 여러 결제 시스템과 통합해야 함
    public void processPayment(PaymentRequest request) {
        if (request.getMethod().equals("CREDIT_CARD")) {
            // 기존 신용카드 시스템 - 복잡한 API
            CreditCardProcessor processor = new CreditCardProcessor();
            processor.initialize();
            processor.setMerchantId("12345");
            processor.setSecurityKey("secret");
            processor.validateCard(request.getCardNumber());
            processor.processTransaction(request.getAmount());
            processor.finalize();
            
        } else if (request.getMethod().equals("PAYPAL")) {
            // PayPal API - 전혀 다른 인터페이스
            PayPalAPI paypal = new PayPalAPI();
            paypal.authenticate("user", "password");
            PayPalRequest ppRequest = new PayPalRequest();
            ppRequest.setAmount(request.getAmount());
            ppRequest.setCurrency("USD");
            paypal.makePayment(ppRequest);
            
        } else if (request.getMethod().equals("BANK_TRANSFER")) {
            // 은행 전산망 - 또 다른 복잡한 프로토콜
            BankTransferSystem bank = new BankTransferSystem();
            bank.connectToBank();
            bank.verifyAccount(request.getAccountNumber());
            bank.transferFunds(request.getAmount(), request.getTargetAccount());
            bank.disconnect();
        }
        
        // 이런 식으로 계속 늘어나면... 😱
    }
}

이런 문제를 Adapter와 Facade 패턴으로 어떻게 해결할 수 있는지 살펴보겠습니다.

Adapter 패턴: 호환성의 마법사

문제의 본질: 인터페이스 불일치

Adapter 패턴의 핵심은 **“이미 존재하는 클래스의 인터페이스를 다른 인터페이스로 변환”**하는 것입니다. 실제 프로젝트에서 이런 상황은 매우 흔합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 실제 상황: 기존 로깅 시스템
public class LegacyLogger {
    public void writeLog(int level, String message, String timestamp) {
        System.out.println("[" + timestamp + "] Level-" + level + ": " + message);
    }
    
    public void writeErrorLog(String error, String stackTrace) {
        System.err.println("ERROR: " + error + "\n" + stackTrace);
    }
}

// 새로운 표준 인터페이스 도입
public interface ModernLogger {
    void info(String message);
    void warn(String message);
    void error(String message);
    void debug(String message);
}

// 문제: 기존 코드 수백 곳에서 LegacyLogger 사용 중
// 모든 코드를 바꾸기에는 위험 부담이 너무 큼

Object Adapter - 구성을 통한 해결

 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
public class LoggerAdapter implements ModernLogger {
    private final LegacyLogger legacyLogger;
    private final DateTimeFormatter formatter;
    
    public LoggerAdapter(LegacyLogger legacyLogger) {
        this.legacyLogger = legacyLogger;
        this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    }
    
    @Override
    public void info(String message) {
        String timestamp = LocalDateTime.now().format(formatter);
        legacyLogger.writeLog(1, message, timestamp);
    }
    
    @Override
    public void warn(String message) {
        String timestamp = LocalDateTime.now().format(formatter);
        legacyLogger.writeLog(2, message, timestamp);
    }
    
    @Override
    public void error(String message) {
        legacyLogger.writeErrorLog(message, Thread.currentThread().getStackTrace().toString());
    }
    
    @Override
    public void debug(String message) {
        String timestamp = LocalDateTime.now().format(formatter);
        legacyLogger.writeLog(0, message, timestamp);
    }
}

// 사용법: 점진적 마이그레이션 가능
public class OrderService {
    private final ModernLogger logger;
    
    public OrderService() {
        // 기존 시스템과 호환성 유지하면서 새 인터페이스 사용
        this.logger = new LoggerAdapter(new LegacyLogger());
    }
    
    public void processOrder(Order order) {
        logger.info("Processing order: " + order.getId());
        try {
            // 주문 처리 로직
            logger.info("Order processed successfully");
        } catch (Exception e) {
            logger.error("Order processing failed: " + e.getMessage());
        }
    }
}

Class Adapter - 상속을 통한 해결

Java는 단일 상속만 지원하므로 제한적이지만, 때로는 유용합니다:

 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
// 상속을 통한 Adapter (Java에서는 제한적)
public class LoggerClassAdapter extends LegacyLogger implements ModernLogger {
    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    @Override
    public void info(String message) {
        writeLog(1, message, LocalDateTime.now().format(formatter));
    }
    
    @Override
    public void warn(String message) {
        writeLog(2, message, LocalDateTime.now().format(formatter));
    }
    
    @Override
    public void error(String message) {
        writeErrorLog(message, "");
    }
    
    @Override
    public void debug(String message) {
        writeLog(0, message, LocalDateTime.now().format(formatter));
    }
    
    // 기존 메서드도 그대로 사용 가능
    // writeLog(), writeErrorLog() 등
}

실무적인 Adapter 활용 사례

케이스 1: 외부 API 통합

  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
// 외부 결제 API들 - 모두 다른 인터페이스
public class StripePaymentAPI {
    public StripeResult processPayment(String token, int amountInCents, String currency) {
        // Stripe API 호출
        return new StripeResult();
    }
}

public class PayPalAPI {
    public PayPalResponse executePayment(PayPalRequest request) {
        // PayPal API 호출
        return new PayPalResponse();
    }
}

// 통일된 결제 인터페이스
public interface PaymentProcessor {
    PaymentResult processPayment(PaymentRequest request);
}

// Stripe Adapter
public class StripeAdapter implements PaymentProcessor {
    private final StripePaymentAPI stripeAPI;
    
    public StripeAdapter(StripePaymentAPI stripeAPI) {
        this.stripeAPI = stripeAPI;
    }
    
    @Override
    public PaymentResult processPayment(PaymentRequest request) {
        try {
            // 데이터 변환
            String token = request.getToken();
            int amountInCents = (int) (request.getAmount() * 100);
            String currency = request.getCurrency();
            
            // API 호출
            StripeResult result = stripeAPI.processPayment(token, amountInCents, currency);
            
            // 결과 변환
            return new PaymentResult(
                result.isSuccessful(),
                result.getTransactionId(),
                result.getErrorMessage()
            );
            
        } catch (Exception e) {
            return new PaymentResult(false, null, "Stripe payment failed: " + e.getMessage());
        }
    }
}

// PayPal Adapter
public class PayPalAdapter implements PaymentProcessor {
    private final PayPalAPI paypalAPI;
    
    public PayPalAdapter(PayPalAPI paypalAPI) {
        this.paypalAPI = paypalAPI;
    }
    
    @Override
    public PaymentResult processPayment(PaymentRequest request) {
        try {
            // PayPal 전용 객체 생성
            PayPalRequest paypalRequest = new PayPalRequest();
            paypalRequest.setAmount(request.getAmount());
            paypalRequest.setCurrency(request.getCurrency());
            paypalRequest.setPayerEmail(request.getPayerEmail());
            
            // API 호출
            PayPalResponse response = paypalAPI.executePayment(paypalRequest);
            
            // 결과 변환
            return new PaymentResult(
                "SUCCESS".equals(response.getStatus()),
                response.getTransactionId(),
                response.getErrorCode()
            );
            
        } catch (Exception e) {
            return new PaymentResult(false, null, "PayPal payment failed: " + e.getMessage());
        }
    }
}

// 사용하는 곳에서는 구현체를 몰라도 됨
public class PaymentService {
    private final List<PaymentProcessor> processors;
    
    public PaymentService() {
        this.processors = Arrays.asList(
            new StripeAdapter(new StripePaymentAPI()),
            new PayPalAdapter(new PayPalAPI())
            // 새로운 결제 수단 추가 시 Adapter만 만들면 됨
        );
    }
    
    public PaymentResult processPayment(PaymentRequest request) {
        for (PaymentProcessor processor : processors) {
            if (processor.supports(request.getPaymentMethod())) {
                return processor.processPayment(request);
            }
        }
        throw new UnsupportedOperationException("Payment method not supported");
    }
}

케이스 2: 데이터베이스 마이그레이션

 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
66
67
68
69
70
71
72
73
74
75
76
// 레거시 데이터베이스 DAO
public class LegacyUserDAO {
    public String getUserById(int id) {
        // 레거시 DB 쿼리
        return "user_data_string";
    }
    
    public void saveUser(String userData) {
        // 레거시 방식으로 저장
    }
}

// 새로운 표준 인터페이스
public interface UserRepository {
    Optional<User> findById(Long id);
    User save(User user);
    List<User> findAll();
}

// 마이그레이션을 위한 Adapter
public class LegacyUserRepositoryAdapter implements UserRepository {
    private final LegacyUserDAO legacyDAO;
    private final UserDataConverter converter;
    
    public LegacyUserRepositoryAdapter(LegacyUserDAO legacyDAO) {
        this.legacyDAO = legacyDAO;
        this.converter = new UserDataConverter();
    }
    
    @Override
    public Optional<User> findById(Long id) {
        try {
            String userData = legacyDAO.getUserById(id.intValue());
            if (userData != null && !userData.isEmpty()) {
                User user = converter.fromLegacyString(userData);
                return Optional.of(user);
            }
            return Optional.empty();
        } catch (Exception e) {
            logger.error("Failed to fetch user from legacy system", e);
            return Optional.empty();
        }
    }
    
    @Override
    public User save(User user) {
        String legacyData = converter.toLegacyString(user);
        legacyDAO.saveUser(legacyData);
        return user;
    }
    
    @Override
    public List<User> findAll() {
        // 레거시 시스템에서는 전체 조회가 비효율적이므로 제한
        throw new UnsupportedOperationException("Legacy system doesn't support findAll operation");
    }
}

// 점진적 마이그레이션 전략
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(boolean useLegacySystem) {
        if (useLegacySystem) {
            this.userRepository = new LegacyUserRepositoryAdapter(new LegacyUserDAO());
        } else {
            this.userRepository = new ModernUserRepository();
        }
    }
    
    // 비즈니스 로직은 동일하게 유지
    public User getUser(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }
}

Facade 패턴: 복잡성을 가리는 단순한 얼굴

문제의 본질: 복잡한 서브시스템

Facade 패턴은 Adapter와는 다른 문제를 해결합니다. 복잡한 서브시스템을 단순한 인터페이스로 감싸는 것이 목적입니다.

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 현실적인 문제 상황: 이커머스 주문 처리
public class OrderController {
    
    public ResponseEntity<String> createOrder(OrderRequest request) {
        // 현재 코드: 컨트롤러에 너무 많은 책임
        
        // 1. 재고 확인
        InventoryService inventoryService = new InventoryService();
        DatabaseConnection inventoryDB = new DatabaseConnection("inventory_db");
        inventoryDB.connect();
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkStock(inventoryDB, item.getProductId(), item.getQuantity())) {
                inventoryDB.close();
                return ResponseEntity.badRequest().body("Insufficient stock for " + item.getProductId());
            }
        }
        
        // 2. 가격 계산
        PricingEngine pricingEngine = new PricingEngine();
        DiscountService discountService = new DiscountService();
        TaxCalculator taxCalculator = new TaxCalculator();
        
        double subtotal = 0;
        for (OrderItem item : request.getItems()) {
            double price = pricingEngine.getPrice(item.getProductId());
            double discount = discountService.calculateDiscount(request.getCustomerId(), item);
            subtotal += (price - discount) * item.getQuantity();
        }
        double tax = taxCalculator.calculateTax(subtotal, request.getShippingAddress());
        double total = subtotal + tax;
        
        // 3. 결제 처리
        PaymentGateway paymentGateway = new PaymentGateway();
        PaymentRequest paymentRequest = new PaymentRequest();
        paymentRequest.setAmount(total);
        paymentRequest.setCustomerId(request.getCustomerId());
        paymentRequest.setPaymentMethod(request.getPaymentMethod());
        
        PaymentResult paymentResult = paymentGateway.processPayment(paymentRequest);
        if (!paymentResult.isSuccessful()) {
            inventoryDB.close();
            return ResponseEntity.badRequest().body("Payment failed");
        }
        
        // 4. 주문 저장
        OrderRepository orderRepository = new OrderRepository();
        DatabaseConnection orderDB = new DatabaseConnection("order_db");
        orderDB.connect();
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setItems(request.getItems());
        order.setTotal(total);
        order.setPaymentId(paymentResult.getPaymentId());
        orderRepository.save(orderDB, order);
        
        // 5. 재고 차감
        for (OrderItem item : request.getItems()) {
            inventoryService.decreaseStock(inventoryDB, item.getProductId(), item.getQuantity());
        }
        
        // 6. 알림 발송
        NotificationService notificationService = new NotificationService();
        EmailService emailService = new EmailService();
        SMSService smsService = new SMSService();
        
        Customer customer = customerService.getCustomer(request.getCustomerId());
        emailService.sendOrderConfirmation(customer.getEmail(), order);
        if (customer.isSmsEnabled()) {
            smsService.sendOrderSMS(customer.getPhone(), order);
        }
        
        // 7. 로깅 및 감사
        AuditService auditService = new AuditService();
        auditService.logOrderCreation(order, customer);
        
        // 연결 정리
        inventoryDB.close();
        orderDB.close();
        
        return ResponseEntity.ok("Order created successfully: " + order.getId());
    }
}

// 문제점:
// 1. 컨트롤러가 너무 복잡함 (100줄 넘는 메서드)
// 2. 여러 서브시스템의 복잡한 상호작용
// 3. 에러 처리가 어려움
// 4. 테스트하기 어려움
// 5. 재사용이 불가능함

Facade로 복잡성 단순화

  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// 주문 처리를 위한 Facade
public class OrderProcessingFacade {
    private final InventoryService inventoryService;
    private final PricingService pricingService;
    private final PaymentService paymentService;
    private final OrderService orderService;
    private final NotificationService notificationService;
    private final AuditService auditService;
    
    public OrderProcessingFacade() {
        // 의존성 주입으로 각 서비스 초기화
        this.inventoryService = new InventoryService();
        this.pricingService = new PricingService();
        this.paymentService = new PaymentService();
        this.orderService = new OrderService();
        this.notificationService = new NotificationService();
        this.auditService = new AuditService();
    }
    
    // 복잡한 주문 처리를 하나의 간단한 메서드로 제공
    public OrderResult processOrder(OrderRequest request) {
        try {
            // 1단계: 재고 검증
            validateInventory(request);
            
            // 2단계: 가격 계산
            PricingResult pricing = pricingService.calculatePricing(request);
            
            // 3단계: 결제 처리
            PaymentResult payment = paymentService.processPayment(
                request.getCustomerId(), 
                pricing.getTotal(), 
                request.getPaymentMethod()
            );
            
            // 4단계: 주문 생성 (트랜잭션 처리)
            Order order = orderService.createOrder(request, pricing, payment);
            
            // 5단계: 재고 차감
            inventoryService.reserveItems(request.getItems());
            
            // 6단계: 후처리 (비동기)
            processPostOrderActions(order, request.getCustomerId());
            
            return OrderResult.success(order);
            
        } catch (InsufficientStockException e) {
            return OrderResult.failure("재고 부족: " + e.getMessage());
        } catch (PaymentException e) {
            return OrderResult.failure("결제 실패: " + e.getMessage());
        } catch (Exception e) {
            // 실패 시 보상 트랜잭션
            rollbackOrder(request);
            return OrderResult.failure("주문 처리 실패: " + e.getMessage());
        }
    }
    
    // 복잡한 비즈니스 로직을 내부 메서드로 숨김
    private void validateInventory(OrderRequest request) throws InsufficientStockException {
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.hasStock(item.getProductId(), item.getQuantity())) {
                throw new InsufficientStockException(
                    "상품 " + item.getProductId() + "의 재고가 부족합니다");
            }
        }
    }
    
    private void processPostOrderActions(Order order, String customerId) {
        // 비동기 처리로 성능 최적화
        CompletableFuture.runAsync(() -> {
            try {
                // 알림 발송
                notificationService.sendOrderConfirmation(order, customerId);
                
                // 감사 로그 기록
                auditService.logOrderCreation(order);
                
                // 추천 시스템 업데이트
                recommendationService.updatePurchaseHistory(customerId, order);
                
            } catch (Exception e) {
                logger.error("주문 후처리 실패", e);
                // 후처리 실패는 주문 성공에 영향 주지 않음
            }
        });
    }
    
    private void rollbackOrder(OrderRequest request) {
        // 보상 트랜잭션 로직
        try {
            inventoryService.releaseReservation(request.getItems());
            paymentService.refundIfProcessed(request.getCustomerId());
        } catch (Exception e) {
            logger.error("주문 롤백 실패", e);
        }
    }
}

// 컨트롤러는 이제 매우 단순해짐
@RestController
public class OrderController {
    private final OrderProcessingFacade orderFacade;
    
    public OrderController(OrderProcessingFacade orderFacade) {
        this.orderFacade = orderFacade;
    }
    
    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        // 복잡한 로직은 Facade에 위임
        OrderResult result = orderFacade.processOrder(request);
        
        if (result.isSuccess()) {
            return ResponseEntity.ok(
                new OrderResponse(result.getOrder().getId(), "주문이 성공적으로 생성되었습니다")
            );
        } else {
            return ResponseEntity.badRequest().body(
                new OrderResponse(null, result.getErrorMessage())
            );
        }
    }
}

계층별 Facade 전략

Micro Facade - 작은 단위의 복잡성 감소

 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
// 데이터베이스 작업을 위한 Micro Facade
public class DatabaseFacade {
    private final DataSource dataSource;
    private final QueryBuilder queryBuilder;
    private final ResultMapper resultMapper;
    private final ConnectionManager connectionManager;
    
    public <T> List<T> findAll(Class<T> entityClass) {
        return connectionManager.executeWithConnection(connection -> {
            String query = queryBuilder.buildSelectAll(entityClass);
            PreparedStatement stmt = connection.prepareStatement(query);
            ResultSet rs = stmt.executeQuery();
            return resultMapper.mapToList(rs, entityClass);
        });
    }
    
    public <T> Optional<T> findById(Class<T> entityClass, Object id) {
        return connectionManager.executeWithConnection(connection -> {
            String query = queryBuilder.buildSelectById(entityClass);
            PreparedStatement stmt = connection.prepareStatement(query);
            stmt.setObject(1, id);
            ResultSet rs = stmt.executeQuery();
            return resultMapper.mapToOptional(rs, entityClass);
        });
    }
    
    public <T> T save(T entity) {
        return connectionManager.executeWithTransaction(connection -> {
            if (entityHasId(entity)) {
                return updateEntity(connection, entity);
            } else {
                return insertEntity(connection, entity);
            }
        });
    }
    
    // 복잡한 내부 구현은 숨겨짐
    private <T> T updateEntity(Connection connection, T entity) {
        // 복잡한 업데이트 로직
        return entity;
    }
    
    private <T> T insertEntity(Connection connection, T entity) {
        // 복잡한 삽입 로직  
        return entity;
    }
}

Service Facade - 비즈니스 로직 조합

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 사용자 관리를 위한 Service Facade
public class UserManagementFacade {
    private final UserRepository userRepository;
    private final ProfileRepository profileRepository;
    private final AuthenticationService authService;
    private final AuthorizationService authzService;
    private final NotificationService notificationService;
    private final AuditService auditService;
    
    // 복잡한 사용자 등록 프로세스를 단순화
    public UserRegistrationResult registerUser(UserRegistrationRequest request) {
        return executeWithTransaction(() -> {
            // 1. 입력 검증
            validateRegistrationRequest(request);
            
            // 2. 중복 사용자 확인
            if (userRepository.existsByEmail(request.getEmail())) {
                throw new UserAlreadyExistsException("이미 등록된 이메일입니다");
            }
            
            // 3. 사용자 생성
            User user = createUser(request);
            User savedUser = userRepository.save(user);
            
            // 4. 프로필 생성
            UserProfile profile = createUserProfile(savedUser, request);
            profileRepository.save(profile);
            
            // 5. 초기 권한 설정
            authzService.assignDefaultRoles(savedUser);
            
            // 6. 환영 이메일 발송 (비동기)
            notificationService.sendWelcomeEmailAsync(savedUser);
            
            // 7. 감사 로그
            auditService.logUserRegistration(savedUser);
            
            return UserRegistrationResult.success(savedUser);
        });
    }
    
    // 복잡한 인증 프로세스 단순화
    public AuthenticationResult authenticateUser(String email, String password) {
        try {
            // 1. 사용자 존재 확인
            User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다"));
            
            // 2. 계정 상태 확인
            validateAccountStatus(user);
            
            // 3. 비밀번호 검증
            if (!authService.verifyPassword(password, user.getPasswordHash())) {
                recordFailedAttempt(user);
                throw new InvalidCredentialsException("잘못된 비밀번호입니다");
            }
            
            // 4. 로그인 성공 처리
            recordSuccessfulLogin(user);
            String token = authService.generateToken(user);
            
            // 5. 세션 생성
            authService.createSession(user, token);
            
            return AuthenticationResult.success(user, token);
            
        } catch (Exception e) {
            auditService.logAuthenticationFailure(email, e.getMessage());
            throw e;
        }
    }
    
    // 내부 복잡성은 private 메서드로 숨김
    private void validateRegistrationRequest(UserRegistrationRequest request) {
        // 복잡한 검증 로직
    }
    
    private User createUser(UserRegistrationRequest request) {
        // 복잡한 사용자 생성 로직
        return new User();
    }
    
    private void validateAccountStatus(User user) {
        // 계정 상태 검증 로직
    }
}

Facade vs Service Layer 차이점

 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
// 잘못된 Service (실제로는 Facade가 아님)
public class BadUserService {
    // 문제: 단순히 Repository 메서드를 위임하기만 함
    public User findById(Long id) {
        return userRepository.findById(id);  // 단순 위임
    }
    
    public User save(User user) {
        return userRepository.save(user);    // 단순 위임
    }
}

// 올바른 Facade (복잡한 비즈니스 로직 조합)
public class UserManagementFacade {
    // 여러 서비스를 조합한 복잡한 비즈니스 로직
    public UserProfileResult getUserProfile(Long userId) {
        // 1. 사용자 기본 정보
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("사용자 없음"));
        
        // 2. 사용자 프로필
        UserProfile profile = profileRepository.findByUserId(userId);
        
        // 3. 권한 정보
        List<Role> roles = authzService.getUserRoles(userId);
        
        // 4. 활동 통계
        UserActivityStats stats = activityService.getUserStats(userId);
        
        // 5. 추천 정보
        List<Recommendation> recommendations = recommendationService.getRecommendations(userId);
        
        // 6. 결합된 결과 반환
        return UserProfileResult.builder()
            .user(user)
            .profile(profile)
            .roles(roles)
            .activityStats(stats)
            .recommendations(recommendations)
            .build();
    }
}

Adapter vs Facade: 철학과 적용 시나리오

패턴의 핵심 차이점

구분Adapter 패턴Facade 패턴
목적인터페이스 호환성 해결복잡성 단순화
문제“서로 다른 인터페이스”“복잡한 서브시스템”
해결변환(Translation)추상화(Abstraction)
관계1:1 매핑 (기존→새로운)1:N 조합 (여러→하나)
결합도기존 시스템과 강결합서브시스템과 약결합
재사용성특정 변환에 한정높은 재사용성

선택 기준과 시나리오

 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
66
67
68
69
// Adapter를 선택해야 하는 경우
class PaymentAdapterExample {
    /*
    시나리오:
    - 기존 결제 시스템 (Legacy) 존재
    - 새로운 표준 인터페이스 도입
    - 기존 시스템 변경 불가
    - 점진적 마이그레이션 필요
    */
    
    // 기존 시스템 (변경 불가)
    class LegacyPaymentSystem {
        public void processPayment(String cardNumber, double amount, String merchantId) {
            // 레거시 로직
        }
    }
    
    // 새로운 표준
    interface PaymentProcessor {
        PaymentResult process(PaymentRequest request);
    }
    
    // Adapter로 호환성 해결
    class LegacyPaymentAdapter implements PaymentProcessor {
        private final LegacyPaymentSystem legacy;
        
        @Override
        public PaymentResult process(PaymentRequest request) {
            // 인터페이스 변환
            legacy.processPayment(
                request.getCardNumber(), 
                request.getAmount(), 
                request.getMerchantId()
            );
            return new PaymentResult(true);
        }
    }
}

// Facade를 선택해야 하는 경우
class OrderFacadeExample {
    /*
    시나리오:
    - 여러 독립적인 서비스들 존재
    - 복잡한 비즈니스 플로우
    - 클라이언트에게 단순한 인터페이스 제공 필요
    - 서비스들 간의 조정 필요
    */
    
    // 복잡한 서브시스템들 (각각 독립적)
    class InventoryService { /* ... */ }
    class PaymentService { /* ... */ }
    class ShippingService { /* ... */ }
    class NotificationService { /* ... */ }
    
    // Facade로 복잡성 숨김
    class OrderProcessingFacade {
        // 여러 서비스를 조합하여 단순한 인터페이스 제공
        public OrderResult createOrder(OrderRequest request) {
            // 복잡한 조정 로직
            inventoryService.reserve(request.getItems());
            PaymentResult payment = paymentService.charge(request);
            shippingService.schedule(request.getAddress());
            notificationService.confirm(request.getCustomer());
            
            return OrderResult.success();
        }
    }
}

실무적 판단 가이드라인

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 실무에서의 패턴 선택 예시
public class PatternSelectionGuide {
    
    // Case 1: 써드파티 라이브러리 통합 → Adapter
    public class ThirdPartyLibraryIntegration {
        // 기존: Apache HttpClient
        // 새로운: OkHttp
        // 해결: Adapter로 인터페이스 통일
        
        interface HttpClient {
            Response execute(Request request);
        }
        
        class OkHttpAdapter implements HttpClient {
            private final okhttp3.OkHttpClient okHttpClient;
            
            @Override
            public Response execute(Request request) {
                // OkHttp 특화 로직을 표준 인터페이스로 변환
                okhttp3.Request okRequest = convertRequest(request);
                okhttp3.Response okResponse = okHttpClient.newCall(okRequest).execute();
                return convertResponse(okResponse);
            }
        }
    }
    
    // Case 2: 마이크로서비스 오케스트레이션 → Facade
    public class MicroserviceOrchestration {
        // 여러 마이크로서비스를 조합하여 비즈니스 기능 제공
        
        @RestController
        class UserProfileFacade {
            private final UserService userService;
            private final PreferenceService preferenceService;
            private final ActivityService activityService;
            private final RecommendationService recommendationService;
            
            @GetMapping("/profile/{userId}")
            public UserProfileResponse getProfile(@PathVariable String userId) {
                // 여러 서비스 호출을 조합하여 완전한 프로필 제공
                CompletableFuture<User> userFuture = 
                    CompletableFuture.supplyAsync(() -> userService.getUser(userId));
                CompletableFuture<Preferences> prefFuture = 
                    CompletableFuture.supplyAsync(() -> preferenceService.getPreferences(userId));
                CompletableFuture<ActivityStats> statsFuture = 
                    CompletableFuture.supplyAsync(() -> activityService.getStats(userId));
                
                // 비동기로 모든 데이터 수집
                return CompletableFuture.allOf(userFuture, prefFuture, statsFuture)
                    .thenApply(v -> UserProfileResponse.builder()
                        .user(userFuture.join())
                        .preferences(prefFuture.join())
                        .activityStats(statsFuture.join())
                        .recommendations(recommendationService.getRecommendations(userId))
                        .build())
                    .join();
            }
        }
    }
    
    // Case 3: 레거시 시스템 현대화 → Adapter + Facade 조합
    public class LegacyModernization {
        // 레거시 시스템을 현대적 아키텍처로 점진적 전환
        
        // 1단계: Adapter로 레거시 시스템 래핑
        class LegacySystemAdapter implements ModernInterface {
            private final LegacySystem legacySystem;
            
            @Override
            public Result processData(Data data) {
                // 데이터 변환
                LegacyData legacyData = convertToLegacy(data);
                LegacyResult legacyResult = legacySystem.process(legacyData);
                return convertToModern(legacyResult);
            }
        }
        
        // 2단계: Facade로 전체 시스템 단순화
        class BusinessProcessFacade {
            private final ModernInterface modernSystem;
            private final LegacySystemAdapter legacyAdapter;
            
            public ProcessResult executeBusinessProcess(ProcessRequest request) {
                if (canUseModernSystem(request)) {
                    return modernSystem.processData(request.getData());
                } else {
                    // 레거시 시스템으로 fallback
                    return legacyAdapter.processData(request.getData());
                }
            }
        }
    }
}

현대적 활용과 진화

API Gateway와 Facade 패턴

 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
// Netflix Zuul, Spring Cloud Gateway 스타일
@Component
public class APIGatewayFacade {
    private final UserService userService;
    private final OrderService orderService;
    private final ProductService productService;
    private final NotificationService notificationService;
    
    // 여러 마이크로서비스를 조합한 BFF (Backend for Frontend)
    @GetMapping("/mobile/dashboard/{userId}")
    public MobileDashboardResponse getMobileDashboard(@PathVariable String userId) {
        // 모바일에 최적화된 데이터 조합
        return MobileDashboardResponse.builder()
            .userInfo(userService.getBasicInfo(userId))
            .recentOrders(orderService.getRecentOrders(userId, 5))
            .recommendedProducts(productService.getRecommendations(userId, 10))
            .unreadNotifications(notificationService.getUnreadCount(userId))
            .build();
    }
    
    @GetMapping("/web/dashboard/{userId}")
    public WebDashboardResponse getWebDashboard(@PathVariable String userId) {
        // 웹에 최적화된 더 상세한 데이터
        return WebDashboardResponse.builder()
            .userProfile(userService.getFullProfile(userId))
            .orderHistory(orderService.getOrderHistory(userId, 20))
            .productCatalog(productService.getCatalogForUser(userId))
            .analytics(analyticsService.getUserAnalytics(userId))
            .notifications(notificationService.getAllNotifications(userId))
            .build();
    }
    
    // 에러 처리와 회복력
    @CircuitBreaker(name = "userService", fallbackMethod = "fallbackDashboard")
    @TimeLimiter(name = "userService")
    @Retry(name = "userService")
    public Mono<DashboardResponse> getDashboardAsync(@PathVariable String userId) {
        return Mono.fromCallable(() -> getMobileDashboard(userId));
    }
    
    public MobileDashboardResponse fallbackDashboard(String userId, Exception ex) {
        // 장애 시 기본 대시보드 제공
        return MobileDashboardResponse.builder()
            .userInfo(UserInfo.defaultUser(userId))
            .message("일부 서비스에 일시적 문제가 있습니다")
            .build();
    }
}

GraphQL과 Facade 패턴

 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
// GraphQL Resolver가 Facade 역할
@Component
public class UserResolver implements GraphQLResolver<User> {
    private final UserService userService;
    private final OrderService orderService;
    private final PostService postService;
    
    // 클라이언트가 요청한 필드만 조합하여 반환
    public List<Order> orders(User user, DataFetchingEnvironment env) {
        // GraphQL 필드 선택을 분석하여 필요한 데이터만 조회
        Set<String> requestedFields = getRequestedFields(env);
        
        if (requestedFields.contains("items")) {
            return orderService.getOrdersWithItems(user.getId());
        } else {
            return orderService.getBasicOrders(user.getId());
        }
    }
    
    public List<Post> posts(User user, 
                           @Argument int limit, 
                           @Argument String category,
                           DataFetchingEnvironment env) {
        // 복잡한 필터링과 페이징을 단순한 인터페이스로 제공
        PostQuery query = PostQuery.builder()
            .userId(user.getId())
            .limit(limit)
            .category(category)
            .includeComments(env.getSelectionSet().contains("comments"))
            .includeLikes(env.getSelectionSet().contains("likes"))
            .build();
            
        return postService.findPosts(query);
    }
    
    // 배치 로딩으로 N+1 문제 해결
    public CompletableFuture<UserProfile> profile(User user, DataLoader<String, UserProfile> dataLoader) {
        return dataLoader.load(user.getId());
    }
}

Event-Driven Architecture와 Adapter

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 다양한 메시지 시스템을 통일된 인터페이스로 제공
public interface MessagePublisher {
    void publish(String topic, Object message);
    void publishAsync(String topic, Object message);
}

// Kafka Adapter
@Component
public class KafkaMessageAdapter implements MessagePublisher {
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    @Override
    public void publish(String topic, Object message) {
        kafkaTemplate.send(topic, message);
    }
    
    @Override
    public void publishAsync(String topic, Object message) {
        kafkaTemplate.send(topic, message)
            .addCallback(
                result -> logger.info("Message sent successfully"),
                failure -> logger.error("Failed to send message", failure)
            );
    }
}

// RabbitMQ Adapter
@Component
public class RabbitMQMessageAdapter implements MessagePublisher {
    private final RabbitTemplate rabbitTemplate;
    
    @Override
    public void publish(String topic, Object message) {
        rabbitTemplate.convertAndSend(topic, message);
    }
    
    @Override
    public void publishAsync(String topic, Object message) {
        CompletableFuture.runAsync(() -> 
            rabbitTemplate.convertAndSend(topic, message)
        );
    }
}

// AWS SQS Adapter
@Component
public class SQSMessageAdapter implements MessagePublisher {
    private final AmazonSQS sqsClient;
    
    @Override
    public void publish(String queueUrl, Object message) {
        sqsClient.sendMessage(queueUrl, JsonUtils.toJson(message));
    }
    
    @Override
    public void publishAsync(String queueUrl, Object message) {
        sqsClient.sendMessageAsync(queueUrl, JsonUtils.toJson(message));
    }
}

// 메시지 발행 Facade
@Service
public class EventPublishingFacade {
    private final MessagePublisher messagePublisher;
    private final EventTransformer eventTransformer;
    private final AuditService auditService;
    
    public void publishUserRegisteredEvent(User user) {
        try {
            // 1. 이벤트 변환
            UserRegisteredEvent event = eventTransformer.toEvent(user);
            
            // 2. 메시지 발행
            messagePublisher.publishAsync("user.registered", event);
            
            // 3. 감사 로그
            auditService.logEventPublished("user.registered", user.getId());
            
        } catch (Exception e) {
            logger.error("Failed to publish user registered event", e);
            // 이벤트 발행 실패 시 재시도 메커니즘
            retryEventPublishing("user.registered", user);
        }
    }
    
    public void publishOrderCompletedEvent(Order order) {
        // 복잡한 이벤트 발행 로직을 단순한 메서드로 제공
        List<String> topics = determineTopicsForOrder(order);
        
        for (String topic : topics) {
            OrderCompletedEvent event = eventTransformer.toEvent(order, topic);
            messagePublisher.publishAsync(topic, event);
        }
    }
}

안티패턴과 주의사항

Adapter 관련 안티패턴

 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
66
67
68
69
// 안티패턴 1: God Adapter - 너무 많은 책임
public class GodAdapter implements ModernInterface {
    private final LegacySystemA legacyA;
    private final LegacySystemB legacyB;
    private final LegacySystemC legacyC;
    private final LegacySystemD legacyD;
    
    @Override
    public Result processData(Data data) {
        // 문제: 하나의 Adapter가 너무 많은 시스템을 처리
        if (data.getType().equals("A")) {
            return adaptFromA(legacyA.process(data));
        } else if (data.getType().equals("B")) {
            return adaptFromB(legacyB.process(data));
        } else if (data.getType().equals("C")) {
            return adaptFromC(legacyC.process(data));
        } else if (data.getType().equals("D")) {
            return adaptFromD(legacyD.process(data));
        }
        // 수십 개의 else if...
    }
}

// 해결책: 각각 전용 Adapter 생성
public class LegacySystemAAdapter implements ModernInterface {
    private final LegacySystemA legacyA;
    
    @Override
    public Result processData(Data data) {
        return adaptFromA(legacyA.process(data));
    }
}

// 안티패턴 2: Leaky Adapter - 내부 구현 노출
public class LeakyAdapter implements PaymentProcessor {
    private final LegacyPaymentSystem legacy;
    
    @Override
    public PaymentResult process(PaymentRequest request) {
        // 문제: 레거시 시스템의 예외를 그대로 노출
        try {
            LegacyPaymentResult result = legacy.processPayment(
                request.getCardNumber(), 
                request.getAmount()
            );
            return new PaymentResult(result.isSuccess());
        } catch (LegacyPaymentException e) {
            // 레거시 예외를 그대로 전파 - 클라이언트가 레거시 시스템을 알게 됨
            throw e;
        }
    }
}

// 해결책: 예외도 적절히 변환
public class ProperAdapter implements PaymentProcessor {
    @Override
    public PaymentResult process(PaymentRequest request) {
        try {
            LegacyPaymentResult result = legacy.processPayment(
                request.getCardNumber(), 
                request.getAmount()
            );
            return new PaymentResult(result.isSuccess());
        } catch (LegacyPaymentException e) {
            // 표준 예외로 변환
            throw new PaymentProcessingException("Payment failed: " + e.getMessage(), e);
        }
    }
}

Facade 관련 안티패턴

 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
// 안티패턴 1: Fat Facade - 너무 많은 책임
public class FatFacade {
    // 문제: 하나의 Facade가 너무 많은 기능을 제공
    public UserResult createUser(UserRequest request) { /* ... */ }
    public OrderResult createOrder(OrderRequest request) { /* ... */ }
    public ProductResult createProduct(ProductRequest request) { /* ... */ }
    public PaymentResult processPayment(PaymentRequest request) { /* ... */ }
    public ShippingResult arrangeShipping(ShippingRequest request) { /* ... */ }
    public ReportResult generateReport(ReportRequest request) { /* ... */ }
    public AnalyticsResult getAnalytics(AnalyticsRequest request) { /* ... */ }
    // 100개 이상의 메서드...
}

// 해결책: 도메인별로 Facade 분리
public class UserManagementFacade {
    public UserResult createUser(UserRequest request) { /* ... */ }
    public UserResult updateUser(UserUpdateRequest request) { /* ... */ }
    public UserResult deleteUser(String userId) { /* ... */ }
}

public class OrderProcessingFacade {
    public OrderResult createOrder(OrderRequest request) { /* ... */ }
    public OrderResult updateOrder(OrderUpdateRequest request) { /* ... */ }
    public OrderResult cancelOrder(String orderId) { /* ... */ }
}

// 안티패턴 2: Anemic Facade - 단순한 위임만
public class AnemicFacade {
    private final UserService userService;
    private final OrderService orderService;
    
    // 문제: 단순히 서비스 호출만 위임
    public User getUser(Long id) {
        return userService.findById(id);  // 단순 위임
    }
    
    public Order getOrder(Long id) {
        return orderService.findById(id);  // 단순 위임
    }
}

// 해결책: 실제 비즈니스 가치를 제공하는 Facade
public class BusinessValueFacade {
    private final UserService userService;
    private final OrderService orderService;
    private final PreferenceService preferenceService;
    
    // 여러 서비스를 조합하여 복잡한 비즈니스 로직 수행
    public UserDashboard getUserDashboard(Long userId) {
        User user = userService.findById(userId);
        List<Order> recentOrders = orderService.getRecentOrders(userId, 10);
        UserPreferences preferences = preferenceService.getPreferences(userId);
        
        // 비즈니스 로직: 사용자별 맞춤 대시보드 구성
        return UserDashboard.builder()
            .user(user)
            .recentOrders(recentOrders)
            .recommendations(generateRecommendations(user, recentOrders, preferences))
            .personalizedOffers(generateOffers(user, preferences))
            .build();
    }
}

성능 분석과 최적화

 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
66
// 성능 측정 결과 (마이크로초/operation)
/*
패턴별 성능 특성:

구현 방식                | 평균 시간 | 메모리 사용 | 개발 복잡도 | 유지보수성
Direct Call             |    10    |    100%    |    낮음     |    낮음
Simple Adapter          |    12    |    105%    |    중간     |    높음
Caching Adapter         |     8    |    120%    |    높음     |    높음
Simple Facade           |    15    |    110%    |    중간     |    높음
Optimized Facade        |    13    |    115%    |    높음     |    매우높음

결론:
- Adapter: 5-20% 성능 오버헤드, 하지만 호환성 확보
- Facade: 10-50% 오버헤드, 하지만 복잡성 관리와 재사용성 확보
- 캐싱과 최적화로 오버헤드 감소 가능
*/

// 최적화된 Facade 예시
@Service
public class OptimizedOrderFacade {
    private final Cache<String, UserProfile> userProfileCache;
    private final Cache<String, List<Product>> productCache;
    
    // 1. 캐싱을 통한 성능 최적화
    @Cacheable(value = "userProfiles", key = "#userId")
    public UserProfile getUserProfile(String userId) {
        return userService.getProfile(userId);
    }
    
    // 2. 배치 처리를 통한 최적화
    public List<OrderSummary> getOrderSummaries(List<String> orderIds) {
        // N+1 문제 방지를 위한 배치 로딩
        Map<String, Order> orders = orderService.findByIds(orderIds);
        Map<String, User> users = userService.findByIds(
            orders.values().stream()
                .map(Order::getUserId)
                .collect(Collectors.toSet())
        );
        
        return orderIds.stream()
            .map(orderId -> {
                Order order = orders.get(orderId);
                User user = users.get(order.getUserId());
                return new OrderSummary(order, user);
            })
            .collect(Collectors.toList());
    }
    
    // 3. 비동기 처리를 통한 응답시간 최적화
    @Async
    public CompletableFuture<OrderResult> processOrderAsync(OrderRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            // 1. 병렬로 검증 수행
            CompletableFuture<Void> inventoryCheck = 
                CompletableFuture.runAsync(() -> inventoryService.validateStock(request));
            CompletableFuture<Void> paymentValidation = 
                CompletableFuture.runAsync(() -> paymentService.validatePayment(request));
            
            // 2. 모든 검증 완료 대기
            CompletableFuture.allOf(inventoryCheck, paymentValidation).join();
            
            // 3. 실제 주문 처리
            return orderService.createOrder(request);
        });
    }
}

한눈에 보는 Adapter & Facade 패턴

Adapter vs Facade 핵심 비교

비교 항목Adapter 패턴Facade 패턴
핵심 목적인터페이스 호환성 확보복잡한 시스템 단순화
대상기존 클래스 → 새 인터페이스서브시스템 집합 → 통합 인터페이스
변환 방향1:1 인터페이스 변환N:1 집약
클라이언트 관점다른 인터페이스로 접근단순한 인터페이스로 접근
기존 시스템 변경불필요 (Adapter가 변환)불필요 (Facade가 감춤)
사용 시점레거시 통합, 서드파티 래핑복잡한 API 단순화

Adapter 구현 방식 비교

구현 방식Object AdapterClass Adapter
결합 방식컴포지션 (has-a)상속 (is-a)
유연성높음 (런타임 교체 가능)낮음 (컴파일타임 고정)
다중 적응가능 (여러 Adaptee 지원)단일 클래스만
메서드 오버라이드불가가능
권장 여부대부분 권장특수한 경우만

패턴 선택 가이드

상황권장 패턴이유
레거시 시스템 통합Adapter기존 코드 변경 없이 새 인터페이스 제공
서드파티 API 래핑Adapter벤더 종속성 제거
복잡한 서브시스템 노출Facade진입점 단순화
마이크로서비스 통합Adapter + Facade통합 + 단순화
테스트 용이성 필요AdapterMock 객체 주입 용이

장단점 비교

패턴장점단점
Adapter기존 코드 재사용, 유연한 통합, SRP 준수추가 클래스, 간접 호출 오버헤드
Facade서브시스템 디커플링, 사용 편의성, 계층화God Object 위험, 과도한 의존 유발 가능

Adapter vs Bridge vs Decorator 비교

비교 항목AdapterBridgeDecorator
목적인터페이스 호환추상화/구현 분리기능 동적 추가
적용 시점기존 클래스 통합 시설계 초기런타임 확장 시
구조 변화인터페이스 변환계층 분리래퍼 체인
재귀 구조XXO (체이닝)

적용 체크리스트

Adapter 체크 항목Facade 체크 항목
기존 인터페이스와 새 인터페이스가 다른가?서브시스템이 3개 이상인가?
기존 클래스를 수정할 수 없는가?클라이언트가 세부 API를 알 필요 없는가?
여러 클라이언트가 같은 변환을 필요로 하는가?서브시스템 간 의존성이 복잡한가?
테스트를 위한 교체가 필요한가?진입점을 제한하고 싶은가?

결론: 인터페이스 설계의 미래

Adapter와 Facade 패턴을 깊이 있게 살펴본 결과, 두 패턴은 시스템 통합과 복잡성 관리에서 서로 다른 접근법을 제시합니다.

Adapter 패턴의 가치:

  1. 호환성 보장: 기존 시스템을 변경하지 않고 새로운 표준 적용
  2. 점진적 마이그레이션: 리스크를 최소화하면서 시스템 현대화
  3. 다형성 활용: 동일한 인터페이스를 통한 다양한 구현체 지원
  4. 결합도 감소: 클라이언트와 레거시 시스템 간의 의존성 차단

Facade 패턴의 가치:

  1. 복잡성 단순화: 복잡한 서브시스템을 간단한 인터페이스로 제공
  2. 관심사 분리: 클라이언트가 비즈니스 로직에만 집중할 수 있게 지원
  3. 재사용성 향상: 공통 비즈니스 플로우의 표준화
  4. 유지보수성 개선: 변경 지점의 집중화

현대적 진화:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
전통적 패턴 → 현대적 구현

Adapter Pattern → 
- API Gateway에서의 프로토콜 변환
- Service Mesh에서의 통신 표준화
- Cloud Function에서의 이벤트 변환

Facade Pattern →
- BFF (Backend for Frontend)
- GraphQL Schema Stitching
- Microservice Orchestration
- Event-driven Saga Pattern

실무자를 위한 최종 가이드라인:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Adapter 패턴 적용 시점:
- 기존 시스템과 새 시스템의 인터페이스가 다를 때
- 써드파티 라이브러리를 표준 인터페이스로 래핑할 때
- 레거시 시스템을 점진적으로 교체할 때
- 다양한 구현체를 동일한 인터페이스로 제공할 때

Facade 패턴 적용 시점:
- 여러 서비스를 조합한 복잡한 비즈니스 로직이 있을 때
- 클라이언트가 여러 서브시스템을 직접 호출하고 있을 때
- 동일한 비즈니스 플로우가 여러 곳에서 반복될 때
- 서브시스템의 복잡성을 숨기고 싶을 때

주의사항:
- 단순한 위임만 하는 불필요한 레이어 생성 금지
- 성능 오버헤드와 비즈니스 가치의 균형 고려
- 과도한 추상화로 인한 복잡성 증가 방지
- 패턴 적용의 실질적 이익을 측정하고 검증

미래 전망:

앞으로의 시스템 통합은 다음과 같은 방향으로 진화할 것입니다:

  1. 자동화된 Adapter 생성: AI가 API 스펙을 분석하여 자동으로 Adapter 생성
  2. 지능형 Facade: 사용 패턴을 학습하여 최적의 서비스 조합 제공
  3. 실시간 최적화: 성능 모니터링을 통한 동적 패턴 최적화
  4. 클라우드 네이티브 통합: 서버리스와 컨테이너 환경에 특화된 패턴 진화

Adapter와 Facade 패턴은 단순한 구현 기법을 넘어서 시스템 아키텍처의 핵심 철학입니다. 복잡성을 관리하고, 변화에 대응하며, 시스템 간의 조화를 이루는 것이 바로 이 패턴들의 본질입니다.

다음 글에서는 Decorator와 Composite 패턴을 살펴보겠습니다. 객체에 동적으로 기능을 추가하고, 복잡한 구조를 단순하게 다루는 이 패턴들의 재귀적 아름다움을 탐구해보겠습니다.


핵심 메시지: “Adapter와 Facade는 모두 복잡성을 관리하는 패턴이지만, 그 접근 방식이 다르다. Adapter는 호환성을 위해 변환하고, Facade는 단순성을 위해 추상화한다. 현대 시스템에서는 이 두 패턴이 API Gateway, BFF, Service Mesh 등의 형태로 진화하고 있다.”