Priv's Blog

2. 객체지향 원리 본문

Dev. Study Note/Design Pattern

2. 객체지향 원리

Priv 2022. 7. 3. 23:06


 

 

1. 추상화

사람의 속성과 행동을 표현한 책을 쓰고자 할 때, 그 분량은 상상하기 힘들 정도로 엄청나게 방대하고 복잡할 것이다.

하지만 사람의 속성과 행동 중에서 필요와 조건에 따라 선별한 요소들을 골라내 사용할 수는 있다.

예를 들어 학생을 표현한다고 가정하면, 학교에서는 항상 교복을 입으며, 가방을 멘다는 속성이 있고, 월~금에는 학교에 등교한다는 행동이 존재한다.

하지만 이는 병원에서 진료를 보는 상황에서는 쓸데없는 정보일 뿐이다.

병원에서는 그 사람의 이름, 혈액형, 과거 진료 내역 등에 더 집중할 것이다.

추상화란, 사물들의 공통적인 특징을 파악해 인식의 대상으로 삼는 것을 말한다.

즉, 매우 복잡한 개념을 처음부터 끝까지 모두 설명하는 것 대신, 상황과 조건에 따라 파악한 공통점들로 집합을 구성하는 '일반화'이다.

이러한 개념은 객체지향 프로그래밍에서도 매우 중요한 역할을 담당한다.

예를 들어 자동차 종류마다 엔진 오일을 교환하는 방식이 모두 다르다고 가정하자.

이를 추상화 개념을 사용하지 않고 코드로 표현한다면 다음과 같다.

// 코드 2-1

switch(CarType) {
    case (Audi) :
        // CODE //
        break;
    
    case (BMW) :
        // CODE //
        break;
    
    case (Benz) :
        // CODE //
        break;
}

만약 자동차 브랜드 종류가 더 늘어난다면 그에 따라 case 분기 또한 추가될 것이다.

이는 매우 복잡하고 비효율적인 코드이며, 예상치 못한 변수에 대응하기 힘들게 만들 수 있다.

그럼 이제 추상화를 이용해 구현해보자.

// 코드 2-2

void ChangeEngineOil(Car car) {
    car.ChangeEngineOil();
}

코드 2-2를 보면 메서드의 매개변수로 자동차를 전달해주고, 그 자동차의 종류에 따라 엔진 오일을 교환하는 메서드를 호출해주고 있음을 알 수 있다.

하지만 코드 2-1과 달리 어느 곳에서도 구체적으로 자동차 종류와 연관되어 있는 코드를 찾아볼 수 없다.

또한 자동차의 종류가 무엇이든지 간에 ChangeEngineOil() 메서드를 호출하는 것은 변하지 않는다.

이는 즉, 차량의 종류에 따라 수행되는 세부적인 코드 내용을 몰라도 '엔진 오일 교환'이라는 작업이 이루어진다는 것을 프로그래머가 '추상적'으로 인지할 수 있다는 것이다.

즉, 각 개체의 구체적인 개념에 의존하지 않고, 추상적 개념에 의존해야 프로그램 설계를 보다 유연하게 변경할 수 있게 되었다.

 


 

2. 캡슐화

SW 공학에서 요구사항 변경에 대처하는 고전적인 설계 원리로는 응집도, 결합도가 있다.

응집도는 클래스, 모듈 안의 요소들이 얼마나 밀접한 연관성을 띄는가를 나타낸다.

결합도는 어떤 기능을 실행할 때, 다른 클래스, 모듈에 얼마나 의존적인지를 나타낸다.

높은 응집도와 낮은 결합도를 유지하는 것은 요구사항 변경에 대처하기 쉽게 만들어준다.

캡슐화는 낮은 결합도를 유지할 수 있도록 도와주는 OOP 설계 원리이다.

캡슐화를 사용하면 정보 은닉을 통해서 높은 응집도와 낮은 결합도를 유도할 수 있다.

정보 은닉이란, 굳이 노출할 필요가 없는 정보들은 숨겨서 외부에서 접근할 수 없도록 제한하는 것이다.

예를 들어, 자동차가 가속 페달을 밟을 때 현재 속도가 몇인지를 아는 것이 중요하지, 어떤 과정을 거쳐서 속도가 올라가는가는 몰라도 되는 정보일 것이다.

즉, 이때 사용자에게 노출해야 하는 정보는 현재 속도에 대한 값일 것이고, 숨겨야 하는 정보는 속도가 올라가는 과정에 관한 코드가 된다.

SW는 결합이 많을수록 문제도 많아질 수밖에 없다.

한 클래스에서 코드 변경이 발생하면 해당 클래스와 엮여 있는(의존하는) 다른 클래스들도 모두 영향을 받는다.

이 때문에 예상 밖의 오류가 새로 발생하거나, 따로 코드를 건드리지도 않았는데 기존의 버그가 모두 사라져 버리는 문제가 발생할 수도 있다.

// 코드 2-3

public class ArrayStack {
    public int top;
    public int[] itemArray;
    public int stackSize;
    
    
    public ArrayStack(int stackSize) {
        itemArray = new int[stackSize];
        top = -1;
        this.stackSize = stackSize;
    }    
    
    
    public boolean isEmpty() {
        // CODE //
    }
    
    
    public boolean isFull() {
        // CODE //
    }
    
    
    public void push(int item) {
        // CODE //
    }
    
    
    public int pop() {
        // CODE //
    }
    
    
    public int peek() {
        // CODE //
    }
}
    
    

public class StackClient {
    public static void main(String[] args) {
        ArrayStack st = new ArrayStack(10);
        st.itemArray[++st.top] = 20;
        System.out.print(st.itemArray[st.top]);
    }
}

 ArrayStack 클래스는 배열을 사용해 스택을 구현하는 코드이다.

여기서 주목할 점은 변수 및 메서드 모두에 public 접근자를 사용했다는 것이다.

이는 외부에서 어떠한 제약 없이 ArrayStack 객체를 생성한 뒤부터는 자유롭게 클래스 변수 및 메서드에 접근할 수 있다는 것을 의미한다.

즉, StackClient 클래스처럼 push 메서드, pop 메서드를 사용하는 것 대신, 직접 클래스 변수에 접근해 값을 바꿔버리는 것도 가능하다는 것이다.

이러한 경우, ArrayStack과 StackClient 사이에는 강한 결합이 발생한다.

ArrayStack 클래스 내에서 새로운 메서드를 생성했을 때 변수의 값이 바뀌었다면, 이러한 변경 사항들이 StackClient 클래스의 코드 실행에 영향을 주지 않을 것이라는 보장이 없다.

만약 ArrayList 클래스를 사용해 스택 구현이 변경되었다면, StackClient 클래스의 코드 또한 변경되어야 할 것이다.

이 또한 StackClient 클래스가 은닉된 정보(변수)를 직접 사용하는 강한 결합 때문이다.

이러한 문제점들을 예방하려면 캡슐화, 즉 정보 은닉이 필요하다.

이제 코드 2-3에 캡슐화를 적용해보자.

자료구조 형태와 관련이 있는 top, itemArray, stackSize 클래스는 자료구조가 변경될 가능성이 크므로, 외부 접근을 막아야 한다.

private int top;
private int[] itemArray;
private int stackSize;

위와 같이 접근 제어자를 private로 바꾸면 push, pop, peek 메서드로만 스택을 사용할 수 있다.

하지만 push, pop, peek 메서드의 연산 과정에서 어떤 자료구조를 사용하는지, 작업 과정에서 어떤 변수가 어떤 값으로 변화하는지는 외부에서 알아낼 수 없다.

즉, 코드의 결합이 낮아진 것이다.

이제 main 메서드의 코드도 다음과 같이 수정해야 한다.

//코드 2-5

public class StackClient2 {
    public static void main(String[] args) {
        ArrayStack st = new ArrayStack(10);
        st.push(20);
        System.out.print(st.peek());
    }
}

 


 

3. 일반화 관계

3.1) 일반화와 캡슐화

일반화는 또 다른 캡슐화라고 말할 수 있다.

과일은 배, 사과, 바나나 등이 가진 공통된 개념이며, 이것들이 가지고 있는 공통 개념을 일반화한 것이다.

또한 사과, 배, 바나나 등은 과일의 한 종류이므로, 과일을 특수화한 개념이다.

위의 사진처럼 일반화 관계를 구축하면, 각각의 과일 종류에 신경 쓰지 않고 '과일'이라는 개념 하나를 통째로 다룰 수 있는 수단이 생긴다.

즉, "현재 냉장고에 과일이 몇 개 있는가?"라는 질문에 답할 수 있게 된다는 것이다.

만약 이러한 일반화 관계가 존재하지 않는다면, 사과, 배, 바나나, 오렌지 등 개별적인 질문들을 반복해야 할 것이다.

이러한 개념을 프로그래밍에도 적용할 수 있다.

장바구니에 담긴 과일의 가격 총합을 구하는 코드를 작성해보자.

// 코드 2-6

while(장바구니에 과일이 있음) {
    switch(과일 종류) :
        case 사과 :
            가격 총합 += 사과 가격;
            break;
        case 오렌지 :
            가격 총합 += 오렌지 가격;
            break;
        // CODE //
}

코드 2-6 디자인으로 코드를 작성하면, 추후에 '키위'가 장바구니에 추가될 경우, case 문을 1개 더 추가해야 한다.

이는 매우 번거로운 작업이 될 수 있으며, 장바구니에 담긴 과일의 종류가 늘어날수록 코드는 비효율적이게 된다.

즉, 새로운 과일이 추가되더라도 코드를 따로 수정할 필요가 없는 유연한 코드가 필요하다.

// 코드 2-7

int ComTotalPrice(LinkedList<Fruit> fruit) {
    int total = 0;
    Iterator<Fruit> iter = fruit.iterator();
    
    while(itr.hasNext()) {
        Fruit curFruit = itr.next();
        total += curFruit.calculatePrice();
    }
    
    return total;
}

ComTotalPrice 메서드는 실제 과일 객체의 종류에 따라 다르게 실행된다.

이는 추후에 살펴볼 다형성 개념에 따른 것이다.

지금까지 살펴본 일반화 관계는 외부에 자식 클래스를 은닉하는 개념으로 볼 수 있었다.

이때 캡슐화 개념은 클래스 내부에 있는 속성, 연산들을 캡슐화하는 것에서 벗어나 일반화 관계를 바탕으로 클래스 자체를 캡슐화하는 것으로 확장될 수 있다.

이러한 서브 클래스 캡슐화는 외부 클라이언트가 개별적인 클래스들과 무관하게 프로그래밍할 수 있도록 도와준다.

위 사진에서 '사람' 클래스 관점을 보면, 자동차의 구체적인 종류는 숨겨져 있다.

대리 운전을 맡는 기사는 자동차의 종류에 따라 운전에 영향을 받지 않을 것이다.

즉, 자동차의 종류가 무엇이 되었든 간에, 자동차를 운전한다라는 행동은 영향을 받지 않는다.

또한 반대로 자동차 클래스도 사람 클래스의 종류에 영향을 받지 않는다.

이처럼 일반화 관계는 자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이라고 볼 수 있다.

 

3.2) 일반화 관계와 위임

일반화 관계를 속성이나 기능의 상속, 즉 재사용을 위해 존재한다고 오해할 수 있다.

그림 2-7에서는 ArrayList 클래스의 메서드들을 상속받아 재사용하기 위한 의도로 Stack 클래스를 생성했다.

기능의 재사용이라는 측면으로만 보면, 이는 성공적인 구현이라고 볼 수 있다.

하지만 이렇게 구현하면 Stack과는 관련이 없는 ArrayList 클래스의 수많은 연산, 속성들까지 전부 상속받게 된다는 단점이 있다.

이러한 문제점은 아래와 같이 push, pop 메서드를 통하지 않고 스택의 자료구조에 직접 접근하게 만들면 해결할 수 있다.

// 코드 2-10

class MyStack<String> extends ArrayList<String> {
    public void push(String element) {
        add(element);
    }
    
    public String pop() {
        return remove(size() - 1);
    }
}

하지만 이 또한 스택의 무결성 조건인 LIFO(Last in First Out)에 위배된다는 단점이 있다.

일반화 관계는 기본적으로 "is a kind of" 관계가 성립되어야 한다.

두 클래스 사이에 일반화 관계가 성립되는 지를 확인할 수 있는 가장 간단한 방법은 아래와 같이 문장을 만들어보면 된다.

Stack "is a kind of" ArrayList.

ArrayList 클래스 대신 Stack 클래스를 사용할 수 있는지를 평가해보면 배열 목록 대신 스택을 사용할 수는 없으므로, 명제는 거짓이 된다.

즉, 두 자식 클래스 사이에 "is a kind of" 관계가 성립되지 않을 때 상속을 사용하면 불필요한 속성이나 연산도 물려받게 되므로 주의해야 한다.

만약 어떤 클래스의 일부 기능만 재사용하고 싶을 경우에는 어떻게 해야 할까?

이때 사용할 수 있는 개념이 바로 위임(Delegation)이다.

위임은 자신이 직접 기능을 실행하지 않고, 다른 클래스의 객체가 기능을 실행하도록 위임하는 것이다.

즉, 일반화 관계클래스 사이의 관계지만, 위임객체 사이의 관계라고 볼 수 있다.

위임을 사용해 상속을 대신하는 과정은 다음과 같다.

  • 자식 클래스에 부모 클래스의 인스턴스를 참조하는 속성을 만든다. 이 속성 필드를 this로 초기화한다.
  • 자식 클래스에 정의된 각 메서드에 만든 위임 속성 필드를 참조하도록 변경한다.
  • 자식 클래스에서 일반화 관계 선언을 제거, 위임 속성 필드에 부모 클래스의 객체를 생성하여 대입한다.
  • 자식 클래스에 사용된 부모 클래스의 메서드에도 위임 메서드를 추가한다.
  • 컴파일 후 동작을 확인한다.

아래 코드 2-11 ~ 코드 2-14까지는 코드 2-10을 위임을 사용하는 코드로 변환한 것이다.

// 코드 2-11

public class MyStack<String> extends ArrayList<String> {
    private ArrayList<String> arList = this;

    public void push(String element) {
        add(element);
    }
    
    public String pop() {
        return remove(size() - 1);
    }
}

MyStack 클래스에 ArrayList 클래스의 인스턴스를 참조하는 arList 객체를 만든 뒤, 이를 this로 초기화하였다.

// 코드 2-12

public class MyStack<String> extends ArrayList<String> {
    private ArrayList<String> arList = this;

    public void push(String element) {
        arList.add(element);
    }
    
    public String pop() {
        return arList.remove(arList.size() - 1);
    }
}

MyStack 클래스의 push, pop 메서드에서 arList 객체를 참조하도록 변경하였다.

// 코드 2-13

public class MyStack<String> {
    private ArrayList<String> arList = new ArrayList<String>();

    public void push(String element) {
        arList.add(element);
    }
    
    public String pop() {
        return arList.remove(arList.size() - 1);
    }
}

일반화 관계를 제거하고, arList를 ArrayList 객체로 생성해 초기화하였다.

// 코드 2-14

public class MyStack5<String> {
    private ArrayList<String> arList = new ArrayList<String>();

    public void push(String element) {
        arList.add(element);
    }
    
    public String pop() {
        return arList.remove(arList.size() - 1);
    }
    
    public boolean isEmpty() {
        return arList.isEmpty();
    }
    
    public int size() {
        return arList.size();
    }
}

이제 모든 변환이 끝났다!

코드 2-14에서는 예제를 좀 더 풍성하게 만들기 위해 arList 객체에 있는 isEmpty와 size 메서드를 새로 추가해주었다. 

이로써 불필요한 기능들까지 모두 가져오는 상속 기능 대신, 필요한 메서드(기능)들만 따로 불러와 재사용할 수 있게 되었다.

이제 특정한 기능들만 재사용하고 싶을 경우에는 상속 대신 위임을 사용해보자.

 

3.3) 집합론 관점에서 본 일반화 관계

일반화 관계는 수학에서 등장하는 집합과 밀접한 관계가 있다.

물론, 일반화 관계를 해석하는 데 복잡한 수학 개념이 필요하다는 것은 아니다.

그림 2-8을 보면, 부모 클래스 A는 전체 집합 A에 해당하고, 그 부분 집합 A1, A2, A3는 각각 A 클래스의 자식 클래스에 해당한다.

이때 다음 관계가 성립해야 한다.

다음과 같은 제약 조건도 존재한다.

위 제약 조건을 일반화 관계에 적용하려면, 제약 조건 {disjoint, complete}를 사용한다.

제약 {disjoint}는 자식 클래스 객체가 동시에 두 클래스에 속할 수 없다는 의미이다.

제약 {complete}는 자식 클래스의 객체에 해당하는 부모 클래스의 객체와 부모 클래스의 객체에 해당하는 자식 크래스의 객체가 1개만 존재한다는 의미이다.

여기서는 따로 제약 조건을 명시하지 않아도 위 2가지 제약을 적용한다고 가정한다.

집합론 관점에서 일반화 관계를 만들면, 연관 관계를 단순하게 만들 수 있다.

어떤 웹 쇼핑몰에서 구매액을 기준으로 VIP 회원, 일반 회원으로 분류한다고 가정하자.

VIP 회원과 일반 회원 각각을 자식 클래스로 생각해 물건과 연관 관계를 맺게 만들 수는 있지만, 기본적으로 회원은 등급과 무관하게 물건을 구매할 수 있어야 한다.

즉, 물건 클래스와 연관 관계는 모든 자식 클래스에서 가지는 공통적인 연관 관계이므로, 부모 클래스인 회원 클래스로 연관 관계를 이동시키는 것이 클래스 다이어그램을 간결하게 만들어준다.

집합론적인 관점에서 일반화는 상호 배타적인 부분 집합으로 나누는 과정으로 볼 수 있다.

예를 들어, 학생은 '놀기'와 '공부하기' 중 한 상태에만 있을 수 있다고 가정하자.

이때, 학생이 '공부하기' 상태라면 '책'만 사용할 수 있고, '놀기' 상태라면 '장난감'만 사용할 수 있다.

특수화는 일반화의 역관계에 해당한다.

즉, 부모 클래스에서 자식 클래스를 추출하는 과정을 말한다.

특수화가 필요한 경우는 어떤 속성, 연관 관계가 특정 자식 클래스에만 관련이 있는 경우이다.

그림 2-10에서 VIP 회원만이 할인 쿠폰을 받을 수 있는 특수성이 있다고 가정해보자.

이 경우, VIP 회원과 일반 회원 사이에 차이가 생기게 되고, VIP 회원만이 할인 쿠폰이라는 속성과 관련이 있게 된다.

이를 그림으로 표현하면 다음과 같다.

집합을 여러 기준에서 분류할 수도 있다.

그림 2-9에서 집합 A를 A1, A2, A3로 분류했지만, 경우에 따라 B1, B2, B3로 분류해야 할 수도 있다.

UML에서는 이러한 분류 기준을 변별자라고 하며, 일반화 관계를 표시하는 선 옆에 작성한다.

여기서 주의할 점은 변별자 여러 개를 사용해 집합을 부분 집합으로 나누는 경우이다.

그림 2-10에서 회원을 '구매액'과 '지역 주민'이라는 변별자에 따라 분류하면, 어떤 회원은 VIP와 일반 회원 중 하나의 자식 클래스에 속함과 동시에 지역 주민과 비 지역 주민 중 하나의 자식 클래스에도 속할 수 있다.

이처럼 한 인스턴스가 동시에 여러 클래스에 속할 수 있는 것을 다중 분류라고 부르며, '<<다중>>'이라는 스테레오 타입을 사용해 표현한다.

여기서 요구사항의 변경, 새로운 요구사항의 추가에 따라 두 일반화 관계가 더 이상 독립적이지 않는 상황도 고려해야 한다.

예를 들어 "VIP 회원에게만 할인 쿠폰을 지급한다."라는 말의 의미를 더 분석해보자.

이는 회원이 지역 주민이든, 비 지역 주민이든, 일단 VIP 회원이라면 할인 쿠폰을 지급한다는 뜻이 된다.

그런데 만약 일반 회원이지만 지역 주민일 경우에는 경품을 제공하도록 바꾸면 어떻게 될까?

이를 처리하는 1가지 방법으로, 모든 분류 가능한 조합에 대응하는 클래스를 만드는 방법이 있다.

그림 2-12에서 Member 클래스의 자식 클래스로 다음과 같은 4개의 클래스를 만드는 것이다.

집합론 관점에서 클래스 관계를 표현하면 다음과 같다.

그림 2-13의 클래스는 다음과 같이 분류할 수 있다.

이를 통해 일반화는 자식 클래스들의 적절한 합집합과 교집합으로 이루어진다는 것을 알 수 있다.

 


 

4. 다형성

다형성이란, '서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 행동하는 능력'이다.

일반화 관계와 함께 자식 클래스를 개별적으로 다룰 필요 없이, 한 번에 처리할 수 있게 하는 수단을 제공해준다.

예를 들어 애완동물이 있다고 가정하자.

이때 고양이, 앵무새, 강아지 등 여러 종류의 동물은 모두 울기는 하지만 서로 다른 울음소리를 낸다.

즉, 모두 동일한 연산은 실행하지만, 그 결과는 모두 다르다는 것이다.

이 다형성 개념이 상속과 연계되면 어떻게 될까?

// 코드 2-15

public abstract class Pet {
    public abstract void talk();
}

public class Cat extends Pet {
    public void talk() {
        System.out.println("야옹");
    }
}

public class Dog extends Pet {
    public void talk() {
        System.out.println("멍");
    }
}

여기서 다음과 같이 Dog 클래스 객체를 Pet 클래스 타입으로 지정한다.

Pet p = new Dog();

이제 p.talk()으로 메서드를 호출하면 어떻게 될까?

이 경우, Dog 클래스 안에 있는 talk 메서드가 호출되어 "멍"을 출력하게 된다.

이처럼 다형성은 코드를 보다 단순하게 만들어주며, 코드 수정에 유연하게 대처할 수 있도록 만들어준다.

또한 다형성을 사용할 경우, 구체적으로 현재 어떤 클래스의 어떤 객체를 참조하는지를 따로 표현할 필요가 없다.

즉, 새로운 애완동물 클래스가 추가되더라도, p.talk()의 호출 코드는 변하지 않는다는 것이다.

이것이 가능한 이유는 일반화 관계에 있을 때, 부모 클래스의 참조 변수가 자식 클래스의 객체를 참조할 수 있기 때문이다.

단, 부모 클래스의 참조 변수가 접근할 수 있는 것은 부모 클래스가 물려준 변수와 메서드뿐이다.

 


 

5. 피터 코드의 상속 규칙

피터 코드의 상속 규칙은 상속의 오용을 막기 위해 상속의 사용을 제한하는 규칙들을 의미한다.

이는 다음과 같으며, 모든 규칙을 만족해야 상속을 사용할 수 있다.

  • 자식 클래스와 부모 클래스 사이는 역할 수행 관계가 아니어야 한다.
  • 한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다.
  • 자식 클래스가 부모 클래스의 책임을 무시하거나, 재정의하지 않고 확장만 수행해야 한다.
  • 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다.
  • 자식 클래스가 역할, 트랜잭션, 디바이스 등을 특수화해야 한다.

아래 그림 2-14는 사람이 운전자와 회사원의 역할을 수행하는 것을 표현한 클래스 다이어그램이다.

이 클래스 다이어그램을 바탕으로 피터 코드의 5가지 규칙을 적용해보자.

  • '운전자'는 어떤 순간에 '사람'이 수행하는 역할 중 하나이며, '회사원'도 마찬가지다.
  • '운전자'는 어떤 시점에서 '회사원'이 되어야 할 필요가 있으며, '회사원'도 '운전자'가 될 필요가 있다.
  • '사람', '운전자', '회사원' 클래스 등에 어떤 속성과 연산이 정의되었는지에 대한 정보가 없어 점검이 불가능하다.
  • 기능만 재사용할 목적으로 상속 관계를 표현한 것은 아니다.
  • 부모 클래스가 역할, 트랜잭션, 디바이스 등을 표현하지 않았다.

이에 따라 그림 2-14는 상속 대신 집약(혹은 연관) 관계를 사용해 클래스 사이의 관계를 표현하는 편이 좋다.

이렇게 표현하면 사람이 회사원과 운전자 역할을 모두 수행한다는 사실이 자연스럽게 드러나며, 어느 순간에는 아무 역할도 수행하지 않을 수 있다는 다중성도 표현할 수 있다.

이를 그림으로 표현하면 다음과 같다.

 


 


수고하셨습니다!


'Dev. Study Note > Design Pattern' 카테고리의 다른 글

6. 싱글턴 패턴  (0) 2022.07.03
5. 스트래티지 패턴  (0) 2022.07.03
4. 디자인 패턴  (0) 2022.07.03
3. SOLID 원칙  (0) 2022.07.03
1. 객체지향 모델링  (1) 2022.07.03
Comments