O. 개방-폐쇄 원칙 (Open-Closed Principle)
소프트웨어 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다!
문제 상황 1 (잘못된 개방)
비교적 최근에 쇼핑몰 연동 개발을 진행한 적이 있어, 상품 조회하는 기능이 하나의 메서드에서 분기처리되어 있더라고
public class ShopItemService {
public ShopItemResponse search(ShopType type, ShopItemSearchRequest request) {
if (ShopType.Amazon.equals(type)) {
// 아마존 상품 조회
} else if (ShopType.Cafe24.equals(type)) {
// 카페24 상품 조회
} else if (ShopType.Shopify.equals(type)) {
// 쇼피파이 상품 조회
}
return convertShopItemResponse(response, type);
}
}
여기서 새로운 쇼핑몰을 추가하거나, 특정 쇼핑몰만 조회 조건을 바꾼다면
- else if 라인도 추가하고 enum 추가하고 convert도 비슷하게 추가하고
- request 값을 변경한게 다른 api 조회에 영향을 주지않는지 확인해야하고
- 테스트가 전체 쇼핑몰에 영향 주기 시작함, 실수의 가능성이 증가하지
이게 변경이 열려있는 상태라고 볼 수 있어, 작은 작업이 구조 때문에 확인해야하는 범위가 계속 커지는거야
OCP 위반은 생각보다 쉽게 찾아볼 수 있어
- 한 메서드에 if else, switch가 나열된 코드
- 기능 추가시 연관성 낮은 다른 코드의 수정 필요
이와 같은 상황은 잘못된 코드로 판단해야하고,
복잡한 기능이 얽힌 코드가 아니라면 일반적으로
변경에 영향이 가지 않도록 잘 닫아주는게 중요하다고 생각해!
해결방안, 개방-폐쇄 1 : 패턴 활용
어떻게 닫고, 어디를 열 것인가? 이에 대한 답은 패턴으로 잘 정리되어 있어
의존성 주입, Starategy, Factory Method 와 같은 패턴을 적용하는거야 (템플릿 메서드 패턴 활용)
abstract class AbstractShopItemSearch {
public void process() {
validate();
search();
convert();
}
protected abstract void validate();
protected abstract void search();
protected abstract void convert();
}
public class amazonShopSearchService extends AbstractShopItemSearch {
protected void validate() { /* 아마존 검증 로직 */ }
protected void search() { /* 아마존 조회 */ }
protected void convert() { /* 변환 로직 */ }
}
자 변경하는데 있어서 잘 닫혀있지? 끝
이라고 생각할 수도 있지만...
문제 상황 2 (잘못된 폐쇄)
추상 메서드로 넣을 수 없는 기능을 추가하는 경우 문제가 발생해 (template method 패턴의 한계)
Cafe24에는 인증 토큰의 시간이 다른 쇼핑몰보다 짧아, 그래서 조회 전 토큰 재발급을 해야하는 작업이 필요해졌어
- process 메서드에 if ( Cafe24 ) 토큰 발급을 넣을건가? (잘못된 개방의 반복)
- validate 메서드에 살짝 끼워서 발급할까? (절대 하지말자..)
- 그러면 search 안에 토큰 재발급을 넣을거야? (그나마 나은 방법이나 SRP 위반, aop를 쓰는 수도 있겠지?)
이와 같은 문제들은 단일 패턴만으로 문제를 해결할 때 흔히 발생할 수 있어
해결방안, 개방-폐쇄 2 : 여러 패턴 활용
그래서 조합(Composition) 패턴을 추가해서 처리했어,
중간에 관련 추상 클래스나, 인터페이스를 추가해서 유연하게 대처하는 방법이야
public interface TokenRefresher { // 토큰 갱신 인터페이스
void refresh();
}
public class Cafe24TokenRefresher implements TokenRefresher {
public void refresh() {
// Cafe24 토큰 갱신 로직
}
}
public class Cafe24ShopSearch extends AbstractShopItemSearch {
private final TokenRefresher refresher;
public Cafe24ShopSearch(TokenRefresher refresher) {
this.refresher = refresher;
}
protected void validate(ShopItemSearchRequest request) { }
protected ShopItemRawResponse search(ShopItemSearchRequest request) {
refresher.refresh(); // 구성 객체로 위임
return callApi(request);
}
protected ShopItemResponse convert(ShopItemRawResponse response) {
return parse(response);
}
}
이렇게하면 각 쇼핑몰 마다 중간에 추가하는 로직이 있더라도 다른 쇼핑몰 코드에 영향을 주지 않으면서 수정할 수 있겠지?
조금만 더 하자
문제 상황 3 (잘못된 폐쇄)
새로 카카오쇼핑몰을 연동했어, 근데 여기는 validate가 필요가 없어 그래도 강제로 한다면.. (LSP위반)
public class KakaoShopSearch extends AbstractShopItemSearch {
protected void validate(ShopItemSearchRequest request) {
notSupported("kakaoShop은 validate 없이 진행합니다");
}
...
}
동작은 하지만 설계에 의해 강제된 메서드가 발생하게 되는거지 (보기에도 이상하다는 느낌이 많이 온다)
( LSP :상위 타입을 사용하는 클라이언트는 하위 타입으로 대체해도 프로그램의 동작이 바뀌면 안 된다! )
해결 방안, 개방-폐쇄 3 : 인터페이스 분리
그래서 검증 인터페이스를 추상 클래스에서 분리해서 처리했지 (점진적인 개선, 리팩토링)
public interface ShopItemSearch {
ShopItemResponse process(ShopItemSearchRequest request);
}
public interface Validatable {
void validate(ShopItemSearchRequest request);
}
public abstract class AbstractShopItemSearch implements ShopItemSearch {
public final ShopItemResponse process(ShopItemSearchRequest request) {
if (this instanceof Validatable validatable) {
validatable.validate(request);
}
var raw = search(request);
return convert(raw);
}
protected abstract ShopItemRawResponse search(ShopItemSearchRequest request);
protected abstract ShopItemResponse convert(ShopItemRawResponse response);
}
검증이 필요한 경우 (접근 제한자에 주목할 것)
public class AmazonShopSearch extends AbstractShopItemSearch implements Validatable {
public void validate(ShopItemSearchRequest request) {
// 검증 로직
}
protected ShopItemRawResponse search(ShopItemSearchRequest request) {
// 조회
}
protected ShopItemResponse convert(ShopItemRawResponse response) {
// 컨버팅
}
}
검증이 불필요한 경우
public class KakaoShopSearch extends AbstractShopItemSearch {
protected ShopItemRawResponse search(ShopItemSearchRequest request) {
// 조회
}
protected ShopItemResponse convert(ShopItemRawResponse response) {
// 컨버팅
}
}
이 밖에도 DI를 통해 의존성을 통해 관리하는 방법도 있으니 찾아보면 좋아
실 사례를 바탕으로 작성하다보니 OCP에서 패턴까지 다양한 글이 되어버렸지만,
CS 지식이 실제 코드에 녹여지는 감각이 중요하다고 생각해
OCP의 핵심은
“확장할 수 있게 만들되, 기존 코드는 건드리지 않도록” 하는 거야.
"클래스를 늘리자!"가 아니라
"변화에 유연하면서도, 기존 기능은 안정적으로 지키자"는 거지.
확장을 고려한 구조는 유지보수를 덜 위험하게 만들고,
코드를 더 읽기 쉽게! 더 오래 잘 버티게! 팀의 효율성을 높게! 만들어준다
점진적 리팩토링은 필수!
'Idea' 카테고리의 다른 글
SOLID, S:단일 책임 원칙 (SRP) (0) | 2025.05.21 |
---|---|
객체지향 이해하기 (0) | 2025.04.16 |
CS 따위는 업무 스킬을 넓혀주지 않았다 (2) | 2025.04.07 |