Justin의 개발 로그

 

개방-폐쇄의 원칙, OCP, Open-Closed Principle

자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀있어야 한다. 

 

내부 책임(정보, 기능, 로직...)의 확장성은 확보하되, 다른 클래스 및 메소드 변경에 대해서는 영향을 받지 않도록 설계해야 한다.

 

SOLID 제1 원칙인 단일 책임의 원칙(Single Responsibility Principle)에 맞게 설계된 클래스와 메소드라면 개방-폐쇄의 원칙(OCP)도 준수될 가능성이 높다. 그만큼 단일 책임의 원칙(SRP)가 중요함

 

개방-폐쇄의 원칙이 말로는 이해되기 쉽지만, 실제로 클래스 설계에 어떻게 반영을 해야 하는지를 이해하려면 아래 항목을 이해하고, 클래스 설계 시에 이를 준수하면 된다. 

 

방향성 제어 

  • 컴포넌트간 의존성 방향은 단방향이어야 한다.
  • 예)
    • 클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존하면, 클래스 A는 클래스 B와 C를 모두 의존하게 됨
    • 클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존하는데,  클래스 C는 클래스 A에 의존하게 되면, 순환 참조 발생
    • 클래스 A가 클래스 B의 메소드를 사용하는데 B의 메소드에서 파라미터로 입력받지 않은 클래스A의 멤버변수(public)를 사용하면 순환 참조로 인해 로직 이해가 어려워짐
    • 클래스 A가 클래스 B의 메소드를 이용하는데 파라미터로 입력받은 참조 객체(refObj)를 B의 메소드에서 상태를 바꾸면, 클래스 A에서 refObj가 왜 상태가 바뀌는지를 이해할 수 없게되며, 역시 순환참조 발생

정보 은닉

  • 클래스 및 메소드 내부에서 사용되는 멤버 변수는 외부(메소드 외부, 클래스 외부)의 접근, 변경을 차단해야 한다. (private)
  • 내부 정보의 변경은 반드시 책임있는 메소드를 통해 이뤄져야 한다. (단일책임의원칙,SRP 재탕)
    • 메시지(메소드 Call) 이외의 방법으로 다른 클래스, 메소드에 영향을 가해서는 안된다.
  • 참조 형태로 받은 파라미터(특히 객체 참조의 경우)에 변경을 가하면 안된다.
  • 참조 형태로 받은 파라미터에 변경을 가해야 하는 피할 수 없는 상황의 메소드인 경우에는 반드시 변경된 정보를 return을 통해 명시적으로 반환하라.
    • 책임의 범위 내에서 변경을 가하는 경우는 가능하지만, 이 경우에도 변경된 정보를 명시적으로 전달되어야 한다.
    • 변경된 객체 Return, Observer 패턴(객체의 상태 변경 전달)
  • 값을 변경할 필요가 없는 변수의 경우에는 final을 습관적으로 사용하자.
  • 클래스 정보의 제공은 return(메소드), getter(VO), message(observer 패턴 참고) 등 명시적인 방법으로 값(또는 상태)를 제공해야 한다.
  • 메시지(메소드)를 기반으로 한 두 객체 사이에는 낮은 결합도를 유지해야 한다.
    • 메시지(파라미터) 최소화
    • MSA 전환을 고려해도 파라미터는 명확하고, 최소화 되어야 함

 

[AS-IS]

/**
 * 감독 클래스 : 춘향뎐 영화를 찍고 있다.
 */
public class MakingMovie {

    public MainActor prepairMainActor() {

        MainActor mainActor = new MainActor();

        mainActor.setHero("이목룡");
        mainActor.setHeroine("성춘향");

        Cosmetic cosmetic = new Cosmetic();

        // 메이크업 팀에서 주인공을 분장한다.
        MakeupTeam makeupTeam = new MakeupTeam();
        makeupTeam.makeUp(mainActor, cosmetic);

        /*
        스타일리스트는 주인공에게 의상을 입힌다.
        조명팀은 주인공 분장과 의상에 맞춰 조명을 조절한다.
        카메라 감독은 장면 등에 맞춰 카메라 동선을 점검한다.

        화장품을 보관함에 정리한다. => 무슨 일이 벌어질까?

        중략...

        */

        return mainActor;	//왜 배트맨, 캣우먼이야?!!!
    }
}

/**
 * 분장사 클래스
 *    특징 헐리우드 영웅 분장 경력이 많다.
 */
public class MakeupTeam {

    public void makeup(MainActor mainActor, Cosmetic cosmetic) {
        mainActor.setHero("배트맨");		//영웅하면 배트맨이지~!!!
        mainActor.setHeroine("캣우먼");		//배트맨하면 캣우먼~!!!

        cosmetic = new ExpensiveCosmetic();

        //주인공을 아이라인을 그린다.
        makeupEyeline(mainActor, cosmetic);

        //주인공의 코를 화장한다.
        makeupNose(mainActor, cosmetic);

        //...생략...

    }
    
    private void makeupEyeline(MainActor mainActor, Cosmetic cosmetic) {
        //히어로의 눈썹 모양을 그린다.
        if( mainActor.Type == ActorType.무관 ) {
            if(mainActor.PERIOD == "조선시대") {
                // 조선시대 계층에 따른 눈썹 모양 분기 처리 
            } else if (mainActor.PERIOD == "20세기") {
                
            } // ...생략
            
        } else if(mainActor.Type == ActorType.평민 ) {    
            
            if(mainActor.PERIOD == "조선시대") {

            } else if (mainActor.PERIOD == "20세기") {

            } // ...생략
            
        } // ...생략
    }
}
  • MakingMovie.prepairMainActor 메소드
    • 연기자(Actor)와 관련이 없는 화장품을 생성해서 메이크업 팀에 넘기고 있다.
    • Hero, Heroine 등 영화에 필요한 연기자가 확장하기 어렵게 정의되어 있다.
    • 매이크업 팀에서 화장품이 바뀌는데, 이 화장품을 조연들에게 보내면 대환장 파티의 시작~
  • MakeupTeam.makeup 메소드
    • MakingMovie에서 받은 연기자를 바꿀 수 있다. 주변의 변화에 열려있음
    • cosmetic을 재정의(객체 생성)하고 있다. 주변의 변화에 열려있음
    • MainActor에 연기자가 추가되면 makeupEyeline, makeupNose 등등 모든 메소드에 로직이 추가되는 대환장 파티의 시작

 

[TO-BE]

/**
 * 영화 클래스 : 춘향뎐 영화를 만들고 있다.
 */

public abstract class MakeupArtist {
    Actor actor;

    public Actor prepair(Actor actor) {
        this.actor = actor;

        makeupEyeline();
        makeupNose();

        return this.actor;
    }

    protected void makeupEyeline() {
        System.out.println("평범하게 눈썹을 그린다.");
    }

    protected void makeupNose() {
        System.out.println("아무 것도 안한다.");
    }
}

public class MakeupArtist_남성조선메인Actor extends Makeup {

    protected void makeupEyeline() {
        System.out.println("조선시대 남자 주인공급으로 눈썹을 그린다.");
    }

    protected void makeupNose() {
        System.out.println("조선시대 남자 주인공의 코 화장을 한다.");
    }
}

public class MakeupFactory {

    public Makeup createMakeupArtist(ActorLevel actorLevel, GenterType genderType) {
        MakeupArtist makeupArtist = null;

        if (genderType == GenderType.MAIL) {
            Switch(actor.actorLevel) {
                case ActorLevel.MAIN:
                    makeupArtist = new MakeupArtist_남성메인Actor();
                break;
                case ActorLevel.SUB:
                    makeupArtist = new MakeupArtist_남성서브Actor();
                break;
                default:
                    MakeupArtist = new MakeupArtist();
            }
        } else if(genderType == GenderType.FEMAIL) {
            Switch(actor.actorLevel) {
                case ActorLevel.MAIN:
                    makeupArtist = new MakeupArtist_여성메인Actor();
                    break;
                case ActorLevel.SUB:
                    makeupArtist = new MakeupArtist_여성서브Actor();
                    break;
                default:
                    makeupArtist = new MakeupArtist();
            }
        } else {
            throw new Exception("메이크업이 정의되지 않은 GenderType 오류"); //없는 성별값이 들어왔을 때 예외 발생
        }

        return makeupArtist;
    }
}


public class MakingMovie {

    Actor hero;
    Actor heroine;

    public void prepairMainActor() {
        MakeupFactory makeupFactory = new MakeupFactory();

        hero = Actor.builder()
                .name("이몽룡")
                .actorLevel(ActorLevel.MAIN)
                .gender(GenderType.MALE)
                .period(PeriodType.조선)  //JoseonDynasty
                .build();


        heroine = new Actor.builder()
                .name("성춘향")
                .actorLevel(ActorLevel.MAIN)
                .gender(Gender.FEMAIL)
                .period(PeriodType.조선)   //JoseonDynasty
                .build();

        MakeupArtist makeupArtist4hero = makeupFactory.createMakeupArtist(hero.actorLevel, hero.gender);
        hero = makeupArtist.prepair(hero);

        MakeupArtist makeupArtist4heroine = makeupFactory.createMakeupArtist(heroine.actorLevel, heroine.gender);
        heroine = makeupArtist4heroine.prepair(heroine);

        /*
        스타일리스트는 주인공에게 의상을 입힌다.
        음향팀은 출연진에게 마이크를 채운다.
        ...대충 출연진을 준비시키는 로직이 더 많다는 얘기...

        ...중략
        */

    }
}
  • MakeupArtist 추상 클래스 
    • 각 화장 부위별 메소드 형식이 정의되어 있고, 이는 protected로 보호되어 직접적인 호출을 막는다.
    • prepair 메소드를 통해 메이크업을 하고, 메이크업이 된 배우를 리턴한다.
  • MakeupArtist을 확장한 구현 클래스 (참고1)
    • 실제 배우 종류별로 화장 방식(역할)에 대한 로직이 구현되어 있다.
    • 배우 종류가 확장되어도 전체 로직의 복잡도 증가하지 않는다.
  • MakeupFactory 클래스 
    • Makeup을 처리할 객체를 골라서 생성해 주는 역할만 한다.
    • 새로운 연기자 종류가 추가되면 구현 클래스(참고1)를 개발한 후 팩토리 클래스에 생성 부분을 추가하면 됨 
  • MakingMovie 클래스
    • 전체 준비를 총괄한다.
    • 그렇지만, 각 전문적인 영역(메이크업아티스트, 스타일리스트, 음향 등)에 대해서는 관여하지 않는다.
    • 신규 준비 상황 등의 기능 추가에는 개방되어 있으며, 각 전문 영역의 세부적인 변화에는 영향을 받지 않는다.
    • 파라미터의 전달이 최소화되며, 객체의 리턴도 명시적

 

 

 

[AS-IS]

protected void 장바구니상품별주문수량합산및배송가능일확인(HashMap<String, Integer> overlapBuyItem
, String ilItemCode, int buyQty
, List<ArrivalScheduledDateDto> arrivalSchedules) {
   int overlapQty = 0;
   String key = ilItemCode;
   if (overlapBuyItem.containsKey(key)) {
      overlapQty = overlapBuyItem.get(key);

      if (arrivalSchedules != null) {
         Iterator<ArrivalScheduledDateDto> iter = arrivalSchedules.iterator();
         while (iter.hasNext()) {
            ArrivalScheduledDateDto dto = iter.next();
            // 선주문 또는 무재한 재고는 패스
            if(dto.getStock() != 9999) {
               // 동일 주문건에 같은 품목 구매 수량 + 구매하려는 수량 비교
               if (dto.getStock() < overlapQty + buyQty) {
                  iter.remove();
               } else {
                  dto.setStock(dto.getStock() - overlapQty);
               }
            }
         }
      }
   }
   overlapBuyItem.put(key, overlapQty + buyQty);
}

문제점

  • 하나의 메소드에 두 가지 책임을 가지고 있음
    • 1번 장바구니 내 상품별 주문 수량 합계
    • 1번에서 계산된 주문 수량을 일자별 재고와 비교해서 배송 가능일 계산
  • 참조 객체(overlapBuyItem, arrivalSchedules)에 변경을 가함
    • 참조형 파라미터에 변경을 가하면 외부(클래스, 메소드)에 영향을 미침
    • 영향 범위 파악 불가 로직이 변경되면 외부 영향으로 인해 side effect 발생
    • 외부 의존성으로 인해 로직 파악 어려움. 복잡도 증가로 인해 유지보수 비용 증가

 

[TO-BE]

  • 역할에 따라 메소드를 분리
  • 외부에서 넘겨받은 파라미터를 조작하는 행위를 제한하거나, 명시적으로 리턴
  • 역할은 같지만(주문량과 재고로 배송가능일자 확인) 외부의 조건에 따라 내부 로직이 달라져야 한다면, 전략 패턴, 팩토리 패턴 등을 통해 역할별로 클래스를 분리하고, 역할에 맞는 클래스를 사용하도록 패턴을 적용하면 복잡도를 낮추고, 유지보수 수준을 유지할 수 있다.

 

 

 

 

개방-폐쇄 원칙 (OCP) 라는 용어는 1988년에 버트란드 마이어(Bertrand Meyer)가 만들었으며, 다음과 같은 사상을 가지고 있다.

소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

 

2000년대 초 로버트 마틴이 객체 지향 프로그래밍 설계의 5원칙(SOLID)을 정의할 때 마이어가 만든 OCP를 포함시킨 것

 

 

인터넷에서 OOP SOLID 원칙를 설명하는 글과 클래스 다이어그램이 많이 있지만, 내용 설명에 부족함을 느껴서 위와 같은 자료를 만들었습니다. 퍼가시는 것은 관계없으나 출처를 밝혀 주시면 감사하겠습니다.

profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!