Idea

SOLID, O:개방-폐쇄 원칙 (OCP)

문쿼리 2025. 5. 21. 21:49

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