
우리는 인터페이스 사용과 결합도에 대한 기본적인 고찰을 진행했습니다.
이번에는 실제 현장에서 마주할 수 있는 더 복잡한 상황들을 살펴보고, 인터페이스의 실질적인 가치와 활용 방안을 탐구해보고자 합니다.
실제 프로젝트에서 가장 빈번하게 마주치는 상황 중 하나는 외부 시스템과의 연동입니다.
예를 들어, 결제 시스템을 연동하는 상황을 생각해보겠습니다.
// 결제 처리를 위한 인터페이스
public interface PaymentProcessor {
PaymentResult processPayment(PaymentRequest request);
PaymentStatus checkPaymentStatus(String paymentId);
RefundResult processRefund(RefundRequest request);
}
// 토스 결제 구현체
@Component
@Primary
public class TossPaymentProcessor implements PaymentProcessor {
private final TossPayClient tossClient;
private final PaymentProperties properties;
@Override
public PaymentResult processPayment(PaymentRequest request) {
TossPayRequest tossRequest = TossPayRequest.builder()
.amount(request.getAmount())
.orderId(request.getOrderId())
.customerName(request.getCustomerName())
.build();
TossPayResponse response = tossClient.requestPayment(tossRequest);
return PaymentResult.builder()
.success(response.isSuccess())
.paymentId(response.getPaymentId())
.build();
}
// 나머지 구현...
}
// 카카오페이 결제 구현체
@Component
public class KakaoPayProcessor implements PaymentProcessor {
private final KakaoPayClient kakaoClient;
@Override
public PaymentResult processPayment(PaymentRequest request) {
KakaoPayRequest kakaoRequest = new KakaoPayRequest();
kakaoRequest.setTotalAmount(request.getAmount());
kakaoRequest.setOrderNumber(request.getOrderId());
// ... 카카오페이 특화 설정
KakaoPayResponse response = kakaoClient.pay(kakaoRequest);
return PaymentResult.builder()
.success(response.getStatus().equals("SUCCESS"))
.paymentId(response.getTid())
.build();
}
// 나머지 구현...
}
여기서 주목할 점은 다음과 같습니다.
실제 서비스에서 자주 발생하는 캐시 전략 변경 상황을 살펴보겠습니다.
public interface CacheManager {
Optional<Object> get(String key);
void put(String key, Object value);
void remove(String key);
void clear();
}
@Component
@Primary
public class LocalCacheManager implements CacheManager {
private final Cache<String, Object> cache;
public LocalCacheManager() {
this.cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
}
@Override
public Optional<Object> get(String key) {
return Optional.ofNullable(cache.getIfPresent(key));
}
// 나머지 구현...
}
@Component
@Conditional(RedisEnvironmentCondition.class)
public class RedisCacheManager implements CacheManager {
private final RedisTemplate<String, Object> redisTemplate;
@Override
public Optional<Object> get(String key) {
return Optional.ofNullable(redisTemplate.opsForValue().get(key));
}
// 나머지 구현...
}
비즈니스 정책이 자주 변경되는 경우의 예시를 살펴보겠습니다.
public interface DiscountPolicy {
BigDecimal calculateDiscount(Order order);
boolean isApplicable(Order order);
}
@Component
public class WeekendDiscountPolicy implements DiscountPolicy {
@Override
public BigDecimal calculateDiscount(Order order) {
if (!isApplicable(order)) {
return BigDecimal.ZERO;
}
return order.getTotalAmount().multiply(BigDecimal.valueOf(0.1));
}
@Override
public boolean isApplicable(Order order) {
DayOfWeek dayOfWeek = order.getOrderDate().getDayOfWeek();
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
}
@Component
public class FirstOrderDiscountPolicy implements DiscountPolicy {
private final OrderRepository orderRepository;
@Override
public BigDecimal calculateDiscount(Order order) {
if (!isApplicable(order)) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(5000);
}
@Override
public boolean isApplicable(Order order) {
return orderRepository.countByCustomerId(order.getCustomerId()) == 0;
}
}
@Service
public class OrderService {
private final List<DiscountPolicy> discountPolicies;
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
BigDecimal totalDiscount = discountPolicies.stream()
.map(policy -> policy.calculateDiscount(order))
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.applyDiscount(totalDiscount);
return order;
}
}
다양한 알림 채널을 지원해야 하는 경우의 예시입니다.
public interface NotificationSender {
void send(NotificationRequest request);
boolean supports(NotificationType type);
}
@Component
public class EmailNotificationSender implements NotificationSender {
private final JavaMailSender mailSender;
private final TemplateEngine templateEngine;
@Override
public void send(NotificationRequest request) {
Context context = new Context();
context.setVariable("user", request.getUser());
context.setVariable("content", request.getContent());
String emailContent = templateEngine.process("email-template", context);
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(request.getUser().getEmail());
message.setSubject(request.getTitle());
message.setText(emailContent);
mailSender.send(message);
}
@Override
public boolean supports(NotificationType type) {
return type == NotificationType.EMAIL;
}
}
@Component
public class SlackNotificationSender implements NotificationSender {
private final SlackClient slackClient;
private final ObjectMapper objectMapper;
@Override
public void send(NotificationRequest request) {
SlackMessage message = SlackMessage.builder()
.channel(request.getChannel())
.text(request.getContent())
.attachments(createAttachments(request))
.build();
slackClient.sendMessage(message);
}
@Override
public boolean supports(NotificationType type) {
return type == NotificationType.SLACK;
}
}
@Service
public class NotificationService {
private final List<NotificationSender> senders;
public void sendNotification(NotificationRequest request) {
NotificationSender sender = senders.stream()
.filter(s -> s.supports(request.getType()))
.findFirst()
.orElseThrow(() -> new UnsupportedNotificationTypeException(request.getType()));
sender.send(request);
}
}
실제 프로젝트에서 인터페이스의 가치는 다음과 같은 상황에서 빛을 발합니다.
이러한 상황들에서 인터페이스를 활용하면 다음과 같은 실질적인 이점을 얻을 수 있습니다.
중요한 것은 무조건적인 인터페이스 사용이 아닌, 실제 변경이나 확장이 예상되는 부분에 대해 선별적으로 적용하는 것입니다.