개방 폐쇄 원칙(OCP)
개방 폐쇄 원칙은 자신의 확장에는 열려 있고, 주변의 변경에 대해서는 닫혀 있어야 한다는 뜻이다.
상위 클래스 또는 인터페이스를 중간에 둠으로써 자신은 변화에 대해서는 폐쇄적이지만, 인터페이스는 외부의 변화에 대해서 확장을 개방해 줄 수 있다.
JDBC 와 Mybatis, Hibernate 등 JAVA 에서는 Stream(Input, Out) 에서 확인할 수 있다.
확장이란?
새로운 타입을 추가함으로써 새로운 기능을 추가할 수 있다. 즉, 확장이란 새로운 타입을 추가함으로써 새로운 기능을 구현한다.
확장에는 열려 있다는 것은 새로운 타입(클래스) 을 추가함으로써 기능을 확장하는 것이다.
변경이란?
확장이 발생했을 때 상위 레벨이 영향을 받지 않아야 한다. 확장(새로운 클래스) 이 발생했을 때 해당 코드를 호출하는 쪽에서 변경이 발생하지 않았다면 변경에 닫혀 있다는 것이다.
- 카드 결제 시스템을 구축
- 현재 지원되는 카드 결제 서비스는 신한 카드 뿐이다
- 앞으로 카드사들이 추가되고 카드 결제 서비스가 추가된다.
하나의 컨트롤러에서 각 각의 은행사 결제 서비스를 DI 하는 방식으로 구현했는데, 이 방식은 OCP 를 만족하는 방식이 아니다.
카드사가 추가될 때 마다 API 를 추가적으로 만들어줘야 하기 때문이고, 이는 확장에 좋지 않은 코드일 뿐 만 아니라 올바른 카드사의 API 를 찾기 위한 코드가 필요하게 된다.
public static class PaymentRequest {
private String cardNumber;
private String csv;
private CardType type;
}
@RequestMapping(value = "/ocp/anti/payment", method = RequestMethod.POST)
public void pay(@RequestBody CardPaymentDto.PaymentRequest req){
if(req.getType() == CardType.SHINHAN){
shinhanCardPaymentService.pay(req);
}else if(req.getType() == CardType.WOORI){
wooriCardPaymentService.pay(req);
}
}
혹은 위와 같이 Request body 로 해당하는 카드사 정보를 가져와서 결제 로직을 처리할 수 있다. 하지만 이와 같은 방식도 OCP 를 준수하지 않는다.
확장이 발생했을 때 해당 코드를 호출하는 쪽에서 변경이 발생하지 않는다면 변경에 닫혀 있다는 것이다.
하지만 위와 같은 코드의 경우 카드사가 추가될 경우 컨트롤러에 if 문이 추가된다. 이는 변경이 발생하는 경우이다.
또한, 추가될 카드의 결제를 담당하는 XXXPaymentService 클래스들이 지속해서 DI 가 된다.
그 결과 해당 컨트롤러에 너무나도 많은 책임을 갖게 되며 확장에 어렵고 변경에 취약한 구조가 된다.
OCP 를 준수하여 어떻게 코드를 작성해야 할까?
public class PaymentController {
@RequestMapping(value = "/payment", method = RequestMethod.POST)
public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
cardPaymentService.pay(req);
}
}
public class ShinhanCardPaymentService implements CardPaymentService {
@Override
public void pay(CardPaymentDto.PaymentRequest req) {
final ShinhanCardDto.PaymentRequest paymentRequest = buildPayment(req);
shinhanCardApi.pay(paymentRequest);
}
}
public class WooriCardPaymentService implements CardPaymentService {
@Override
public void pay(CardPaymentDto.PaymentRequest req) {
final WooriCardDto.PaymentRequest paymentRequest = buildPayment(req);
wooriCardApi.pay(paymentRequest);
}
}
의존관계를 인터페이스를 통해서 역전시킨다. 새로운 카드가 추가되더라도 컨트롤러 수정 없이 결제 서비스를 확장하고 있다.
새로운 카드가 추가(새로운 클래스) 되더라도(확장에 열림) 카드 결제를 호출하는 코드 쪽(컨트롤러) 에서 변경이 발생하지 않는다.
인터페이스를 사용하면 클래스 간의 강한 결합 관계를 느슨한 관계로 만들 수 있다.
중요한 것은 캡슐화, 객체의 올바른 책임, 역할을 부여하여 예측 변경 시점에 OCP 를 쉽게 적용할 수 있도록 하는 것이 중요하다.
참조