자바와 객체 지향

JP·2021년 12월 29일
0

자바

목록 보기
4/10

스프링 입문을 위한 자바 객체지향의 원리와 이해

이 책을 고른 이유

나는 자바, 스프링을 이용하여 백엔드 개발을 1년 2개월을 해왔다.
하지만 자바의 특성, 스프링이라는 프레임워크를 이용하는 이유조차 제대로 알 지 못하고 있기 때문에 이 책이 나에게 조금의 도움이 될까하여 읽기 시작했다.

오늘은 내가 이 책에서 배운 내용을 정리하고자 한다.

자바와 객체 지향

객체지향을 이해하기 위한 전제 조건

  • 세상에 존재하는 것은 모두 객체이다
  • 객체는 고유하다
  • 객체는 속성을 갖는다
  • 객체는 행위를 한다

객체 지향을 공부하기 위해
1. 클래스, 객체 - 추상화
2. 상속
3. 다형성
4. 캡슐화
를 공부하였다.

1. 클래스, 객체: 추상화 모델링

객체를 이해하기 위해선 클래스를 알아야 한다.

클래스 객체명 = new 클래스();
인간 JP = new 인간(); // JP는 인간이다. 논리적으로 ok
붕어빵틀 붕어빵 = new 붕어빵틀(); // 붕어빵은 붕어빵틀이다? 논리적으로 not ok

클래스란 객체를 만들어 내기 위한 추상화의 도구로만 보면 논리적인 오류가 생길 수 있다.

이 책에서 말하는 클래스란 추상화(공통 속성의 추출)를 통한 분류, 집합, 같은 속성과 기능을 가진 객체를 총칭하는 개념이다.

객체란 클래스라는 추상화 모델링을 통해 설계된 것을 토대로 만들어지는 유일무이한 것이다.

ex ) 클래스 : 객체 = 인간 : JP

클래스명은 분류스럽게, 객체 참조 변수명은 유일무이한 사물처럼 작성하는게 좋다
ex) 조류 bird = new 조류(); 와 같이 객체 참조명을 지을 경우 bird라는 이름은 객체보다는 분류 즉 클래스에 가깝기 때문에 추천하지 않는다.
동물 bird = new 조류();
조류 펭귄 = new 조류();
와 같은 이름은 오해의 소지가 없다.

2. 상속: 재사용 + 확장

상속은 부모 클래스의 속성과 기능을 그대로 이어받아 사용할 수 있게하고 기능의 일부분을 변경해야 할 경우 자식 클래스에서 재정의하여 사용할 수 있게 하는 것이다.

이를 통해 재사용과 개념의 확장을 할 수 있게 된다.

객체 지향에서의 상속은 Inheritance 보다 extends의 개념이 더 맞다.
왜냐하면, 할아버지->아버지->아들 과 같은 계층형 상속은 개념/논리적으로 맞지 않기 때문이다.

할아버지 우리아빠 = new 아버지(); // 우리아빠는 할아버지다?

올바른 상속을 하려면 분류에 의한 개념의 확장, 재사용을 하여야 한다.

동물 포유류 = new 포유류(); //포유류는 동물이다!

위의 그림을 보면 동물은 포유류의 조상이 아니지만 분류에 의한 확장 개념의 상속을 하였기 때문에 올바른 예로 들 수 있다.

상속은 is a 관계보다는 is a kind of가 좀 더 정확한 표현이다.

동물 펭귄 = new 펭귄(); // 펭귄 is a 동물

위의 표현을 클래스의 상속관계 즉 상위 클래스와 하위 클래스로 보자면

하위클래스 is a 상위클래스

라고 표현 될 것이다. 위의 표현은 논리적으로 맞지 않다.
더 정확한 표현은

하위 클래스 is kind of 상위클래스

가 될 것이다.

동물 펭귄 = new 펭귄(); // 펭귄 is a kind of 동물
동물 조류 = new 조류(); // 조류 is a kind of 동물
동물 고래 = new 고래(); // 고래 is a kind of 동물
  • 객체 지향의 상속은 상위 클래스의 특성을 재사용하는 것
  • 객체 지향의 상속은 상위 클래스의 특성을 확장하는 것
  • 객체 지향의 상속은 is a kind of 관계를 만족하는 것

자바는 다중상속을 지원 안한다.

다중 상속은 상속관계에서 혼란을 줄 수 있기에 자바에서는 다중 상속을 포기했다. 대신 우리에겐 인터페이스가 있다!

상속이 is a kind of 라면
인터페이스는 is able to 라고 생각하면 된다.

정확히 말하면, 구현클래스 is able to 인터페이스 가 된다.

위의 사진에서 날수 있는 기능과 헤엄칠 수 있는 기능에 대한 분류를 interface로 할 수 있다.

코드로 풀자면

public class 고래 extends 포유류 implements 헤엄칠수있는{
	고래(){}
    @Ovderide
    public void swim(){
    	//헤엄치는 로직
    }
}

public interface 헤엄칠수있는{
	void swim();
}

이 되겠다.

인터페이스로 고래를 분류하면

헤엄칠수있는 헤엄치는동물 = new 고래();

가 되겠다.

상위클래스는 하위 클래스에게 물려줄 특성이 많을 수록 좋다.
인터페이스는 구현을 강제할 메서드가 적을수록 좋다.
이는 LSP(리스코프 치환 원칙)과 ISP(인터페이스 분할 원칙)에 따른 이유라고 할 수 있다.

3. 다형성: 사용 편의성

객체 지향에서 다형성이라 하면 가장 기본은 오버라이딩과 오버로딩이라 할 수 있다.
오버로딩은 화물에 짐을 계속 실고 있는 트럭을 생각해보자.(중복 정의)
오버라이딩은 기존에 실려있던 화물을 내리고 새로운 화물을 실는 것이다.(재정의)

오버로딩과 오버라이딩의 편의성은 역으로 생각하면 된다.
만약 오버라이딩이(중복 정의)가 존재하지 않는다면?
add(int,int) 라는 두 정수를 입력받아 값을 반환받는 메서드를 떠올려보자.
그런데 정수와 부동소수점수를 입력받을 수 있어야 한다는 요구 사항이 추가 되었다.
우리는 이를 addIntDouble(int,double) 이라고 정의하였다.
그런데 정수보다 부동소수점을 먼저 넣어야 한다고 한다.
다시 addDoubleInt(double,int)라는 메서드를 추가하였다.
이런식으로 하자면 자바에서 사용되는 정수형은 byte, short, int , long , char 로 5가지, 보둥소수점 자료형으로는 float, double 2가지가 있다.
7*7의 경우의 수 = 49, 중복 7가지를 제외한다쳐도 42가지의 두 숫자를 더하는 다른 이름의 메서드를 만들어야 한다. 이러한 고통을 오버라이딩이 해결해주는 것이다.

오버로딩의 필요성은 동물 클래스를 예로 들 수 있다.
동물들의 울음소리는 모두 다를것이 아닌가?
고래.울다()//우어어
쥐.울다()//찍찍
고양이.울다()//야옹

위와 같이 상위 클래스의 메서드를 하위 클래스가 오버라이딩 하여 재정의하여 사용할 수 있는 것이다.

4. 캡슐화: 정보 은닉

캡슐화의 목표는 정보은닉과 접근의 허용을 위해 사용한다.
객체가 외부에 노출하지 않아야 할 정보 또는 기능을 접근제어자를 통해 제어 권한이 있는 객체에서만 접근할 수 있도록 한다.
이를 통해 책임이 있는 객체만 수정하면 되기에 코드 수정의 영향 범위를 예측하는데 더 쉬워졌고, 관련된 기능과 특성을 한 곳에 모아 분류하기때문에 객체 재활용이 원할해졌다.

접근제어자

  • private - 같은 클래스의 객체끼리 접근 가능
  • default - 같은 패키지 내의 클래스에서 접근 가능
  • protected - 상속 / 같은 패키지 내의 클래스에서 접근 가능
  • public - 모두가 접근 가능

위의 설명을 코드로 이해해보자.

위와 같이 두개의 패키지가 있다.
하나는 패키지1, 또 하나는 패키지2 이다.

패키지1에는 Human 클래스가 있다.

package package1;

public class Human {
    private int eye;
    int nose;
    protected int arm;
    public int leg;

    void init(){
        eye = 2;
        nose = 1;
        arm = 2;
        leg = 2;
        this.eye = 2;
        this.nose = 1;
        this.arm = 2;
        this.leg = 2;
    }
}

위에서 본인은 모든 접근제어자에 다 접근 할 수 있다.
같은 패키지1에 Human을 닮고 싶은 HumanRobot이 있다.

package package1;

public class HumanRobot extends Human {

    void init(){
        //eye = 2; error
        nose = 1;
        arm = 2;
        leg = 2;
        //this.eye = 2; error
        this.nose = 1;
        this.arm = 2;
        this.leg = 2;
    }

    void wantToBeHuman(){
        Human humanoid = new Human();
        //humanoid.eye = 2; error
        humanoid.nose = 1;
        humanoid.arm = 2;
        humanoid.leg = 2;
    }
}

같은 패키지에서 사람을 상속받은 로봇은 private 접근제어자를 제외하고 모두 접근 가능하다.

사람이 되고싶지 않은 RealRobot이 같은 패키지1에 존재한다고 치자.

package package1;

public class RealRobot {
    void wantToBeHuman(){
        //eye = 2; error
        //nose = 1; error
        //arm = 2; error
        //leg = 2; error
        //this.eye = 2; error
        //this.nose = 1; error
        //this.arm = 2; error
        //this.leg = 2; error
    }

    void init(){
        Human JP = new Human();
        //JP.eye = 2; error
        JP.nose = 1;
        JP.arm = 2;
        JP.leg = 2;
    }
}

Human을 상속받지 않았기에 접근을 하려면 new를 통해 객체를 생성하고 접근할 수 밖에 없다.
new를 통해 객체를 생성하고 같은 패키지에 존재한다고 해도 private 접근 제어자에는 접근 할 수 없다.

바로 옆동네인 패키지2에서도 로봇을 만들고싶어한다.

package package2;

import package1.Human;

public class HumanRobot extends Human {
    
    void wantToBeHuman(){
        // eye = 2; error
        // nose = 1; error
        arm = 2;
        leg = 2;
        // this.eye = 2; error
        // this.nose = 1; error
        this.arm = 2;
        this.leg = 2;
    }

    void init(){
        Human humanoid = new Human();
        // humanoid.eye = 2; error
        // humanoid.nose = 1; error
        // humanoid.arm = 2; error
        humanoid.leg = 2;
    }
}

패키지2의 HumanRobot은 패키지1의 Human을 상속받았다.
그리하여 객체를 만들지 않더라도 protected와 public 에는 접근 할 수 있었다.
하지만 private과 default는 접근 불가능 했다.
객체를 만들고 난 후에는 public 접근제어자에만 접근 가능했다.

패키지2의 RealRobot은 패키지1의 Human과 아무 관계도 없다.

package package2;

import package1.Human;

public class RealRobot {
    
    void wantToBeHuman(){
        //eye = 2; error
        //nose = 1; error
        //arm = 2; error
        //leg = 2; error
        //this.eye = 2; error
        //this.nose = 1; error
        //this.arm = 2; error
        //this.leg = 2; error
    }

    void init(){
        Human JP = new Human();
        //JP.eye = 2; error
        //JP.nose = 1; error
        //JP.arm = 2; error
        JP.leg = 2;
    }
}

Human을 상속받지 않았으니 객체를 생성해서 사용하여야 한다.
객체를 생성하더라도 public에만 접근 가능하다.

위의 코드에서 Human이라는 클래스를 패키지1과 패키지2에서 Human을 각각 접근해보았다.
다시 한번 정리하자면

public 접근제어자는 상속과 관계없이 모두가 접근 가능했다.
protected 접근제어자는 상속받은 클래스와 같은 패키지에 있는 클래스에서 접근 가능했다.
default 접근 제어자는 같은 패키지내에서 접근 가능했다.
private 접근 제어자는 본인만 접근 가능 했다.

그 외 정적멤버(static)는 클래스명.정적멤버 형식으로 접근하는 걸 추천한다.
이유는
1. 일관된 형식의 접근을 위해
2. 객체참조변수명.정적멤버는 클래스명.정적멤버 보다 이상하다.
ex) 톰.다리개수 보다 고양이.다리개수 가 더 나음
3. 메모리의 물리적 접근에 따른 이유

5. 번외

참조 변수의 복사

  • call by value : 값에 의한 호출
int a = 10;
int b = a;
b = 20;
System.out.println(a); //10
System.out.println(b); //20

기본 자료형 변수는 저장하고 있는 값을 그 자체로 해석한다.
따라서 a와 b 두 개의 변수는 서로에게 영향을 주지 않는다.

  • call by reference : 참조에 의한 호출
Animal a = new Animal();
Animal b = a;

a.age = 10;
b.age = 20;

System.out.println(a.age); //20
System.out.println(b.age); //20

객체 참조 변수는 객체 참조변수가 가지고 있는 값을 주소로 해석한다.
위의 코드에서 보면 a와 b가 같은 주소값을 가지고 있기에 값은 주소의 참조변수를 복사해 같은 값이 나오는 것을 확인 할 수 있다.

자바가 확장한 객체 지향

1. abstract : 추상메서드와 추상 클래스

  • 추상메서드는 인터페이스와 같이 구현부가 없는 메서드를 말한다.
    - 인터페이스, 추상클래스
  • 추상메서드를 하나 이상 포함하고 있는 클래스는 추상클래스로 선언해야 한다.
    - 물론, 추상메서드가 없는 추상 클래스를 선언할 수 있다.

2. 생성자

3. final 키워드

final 키워드는 클래스, 메서드, 변수 앞에 붙을 수 있다.

  • final + 클래스 : 상속을 허락하지 않는다.
  • final + 메서드 : 오버라이딩을 허락하지 않는다.
  • final + 변수 : 재정의를 허락하지 않는다(상수).

4. instanceof 연산자

  • 객체참조변수 instanceof 클래스명; 으로 사용한다. true 와 false를 반환한다.
    - 톰 instanceof 고양이 //true
    - instanceof 는 리스코프 치환원칙을 어기는 코드에서 주로 나타나기에 의심해야한다.

    리스코프 치환원칙이란, 밑에서 더 자세히 설명하겠지만 자세히 말하자면
    "하위 클래스의 인스턴스는 상위형 객체참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야한다" 라는 의미이다.

5. interface 와 implements

  • 인터페이스는 public 추상메서드와 public 정적상수를 가질 수 있다.
    - 자바8 이후에는 default method를 가질 수 있게 되었다.
    • default method란 몸체가 있는 메서드를 말한다.
    • 오버라이딩이 가능하다
    • 인터페이스가 변경되면 이를 구현한 모든 구현체들에게 변경점이 생기게 되는데 이런 문제점을 해결하기 위해서 나왔다.
interface Shootable{
	double eyesight = 1.0;
    final double length = 20.5;
    void shoot();
}

위의 코드는 자바에 의해 아래와 같이 변하게 된다.

interface Shootable{
	public static final double eyesight = 1.0;
    public static final double length = 20.5;
    public abstract shoot();
}

6. this 키워드

  • 지역변수와 속성(객체변수,정적변수)의 이름이 같은 경우 지역변수가 우선한다.
  • 객체변수와 이름이 같은 지역변수가 있는 경우, 객체변수를 사용하려면 this를 접두사로 사용한다.
  • 정적변수와 이름이 같은 지역변수가 있는 경우 정적변수를 사용하려면 클래스명을 접두사로 사용한다.
class Gun{
	int length = 10;
    static int bullet = 10;
    void shoot(){
    	int length = 20;
        int bullet = 20;
        System.out.println(length); //20 , length는 지역변수를 뜻한다.
        System.out.println(this.length); //10 , this를 붙혀 객체변수를 사용했다.
      	System.out.println(bullet); //20 , bullet은 지역변수를 뜻한다.
        System.out.println(Gun.bullet); //10 , 클래스명을 붙혀 정젹변수를 사용했다.  
    }
}

객체 지향 설계 5원칙

자바의 5원칙 SOLID에 대해 알아보자
SOLID는 각각의 문장의 앞글자를 따서 만든 단어이다.
S: Single Responsibility Principle, SRP 단일 폐쇄 원칙
O: Open Closed Principle, OCP 개방 폐쇄 원칙
L: Liskov Substitution Principle, LSP 리스코프 치환 원칙
I: Interface Segregation Principle, ISP 인터페이스 분리 원칙
D: Dependency Inversion Principle, DIP 의존 역전 원칙

SOLID 원칙은 응집도는 높이고 결합도는 낮춰라는 High Chhesion, Loose Coupling의 원칙을 객체지향의 관점에서 재정의한 것이다.

응집도?
하나의 모듈 내부에서 존재하는 구성 요소들의 기능적 관련성으로 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져서 재사용이나 기능의 수정, 유지보수가 용이하다. 높을수록 좋다.
기능 > 순차 > 통신 > 절차 > 시간 > 논리 > 우연 순으로 응집도가 높다.

결합도?
모듈간의 상호 의존 정도로서 결합도가 낮으면 모듈간의 상호의존성이 줄어들어 객체의 재사용이나 수정, 유지보수에 용이하다. 낮을수록 좋다.
내용 > 공유 > 외부 > 제어(컨트롤) > 스탬프 > 데이터 순으로 높다.

1.SRP

책에는 2000년대 초반 객체지향 프로그래밍 및 설계의 다섯가지 기본 원칙을 제시한 로버트.C.마틴 의 말로 Single Responsibilty Principal을 소개한다

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다"

클래스는 하나의 기능(책임)을 가지고 있다면 책임 영역이 확실해지기 때문에 코드의 가독성 향상, 유지보수 용이 등의 이점을 누릴 수 있다.
책임을 철저하게 분리하다보면 추상화 모델링, 기능의 확장 등 OOP와 다른 자바 객체 지향 설계의 원리들을 적용하는 기초가 된다.

나는 주로 설계를 하면서 책임이라는 단어에 대해 제대로 된 코딩을 못했던 것 같다.

아래의 설명을 통해 대략적인 책임의 분리에 대해 알아보자.
한 슈퍼마켓에서 과자들을 판다고 치자.

class 과자(){
	int 과자넘버;
    int 가격;
    String 브랜드;
    String 종류;
    String 브랜드;
    ..
    ..
    ..
}

위의 과자클래스를 보면 과자넘버는 과자의 고유정보를 뜻한다.
고유정보란 다른 클래스와 구분되는 정보다.
그 외의 가격과 브랜드, 종류, 브랜드 등은 모두 특성 정보군으로 변경이 발생할 수 있는 부분이다. 따라서 이를 책임에 따라 리팩토링 한다면 아래와 같이 될 것이다.

class 과자(){
	int 과자넘버;
    과자정보 과자정보;
}

class 과자정보(){
	int 가격;
    String 브랜드;
    String 종류;
    String 브랜드;
    ..
    ..
    ..
}

2.OCP

"소프트웨어 엔티티는 확장에 대해서 열려있어야 하지만 변경에 대해서는 닫혀 있어야 한다."

확장은 되는데 변경하면 안된다? 라는 의미가 아니라
요구사항의 변경이나 추가사항이 발생했을 때, 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻이다.

이를 가능하게 하려면 추상화모델링과 다형성의 메커니즘이 이뤄져야 한다.

예를 들면 운전자는 차종이 소나타에서 벤츠로 바뀌더라도 자동차를 운전 할 줄 아는 것과 같은 것이다.
소나타에서 벤츠로 바뀐다고 바퀴가 3개가 되는것도 아니고 브레이크가 뒷좌석에 달리는 것이 아닌 똑같은 자동차고 똑같이 굴러갈 뿐이다.
따라서 자동차를 운전할때
운전자 -> 소나타
운전자 -> 벤츠 를 운전하는 것이 아닌,
운전자 -> 자동차 -> 벤츠/소나타 가 되는 것이다.
자동차는 그저 굴러간다(), 브레이크한다() 와 같은 행위에 대한 정의로 구성된 자동차 인터페이스로 만들어져야 하고 그 행위를 구현한 소나타와 벤츠라는 클래스들이 존재해야 하는 것이다.

그래야만 요구사항이 변경될 때 (벤츠에서 롤스로이스로 변경된다면) 힘들지 않게 요구사항을 적용할 수 있는 것이다.

이와 같은 예를 더 들자면 할인율 정책이 있을수도 있다.
어느 쇼핑몰의 신규 회원에 대해 쿠폰을 증정하기로 했는데 쿠폰이 -10000원 쿠폰이 될지 10% 할인 쿠폰이 될지 정해진 바가 없을 때,
아니면 우리가 자바애플리케이션에서 DB를 오라클에서 MS-SQL로 갈아 낄 때 와 같은 예가 더 있을 수 있다.

3.LSP

"서브 타입은 언제나 자신의 기반타입(base type)으로 교체할 수 있어야 한다."
-->"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다."

리스코프 치환원칙을 위반한 사례는 계층형 상속 구조에서 나타날 수 있다.

class 할아버지{}
class 아버지 extends 할아버지{}
class 아들 extends 아버지{}

아버지 아들객체 = new 아들();
아들이 태어났는데 갑자기 아빠역할을 할 줄 알아야한다?

리스코프 치환원칙을 잘 지킨 예는 아래와 같다.

class 동물{}
class 조류 extends 동물{}
class 펭귄 extends 조류{}

동물 뽀로로 = new 펭귄();
뽀로로라는 펭귄이 태어났는데 동물의 행동을 할 줄 안다.

위와 같이 추상화가 올바르게 이루어져 있다면, 리스코프 치환원칙을 준수할 수 있게 되고 올바른 상속구조를 제공하게 되어있다.

리스코프 치환원칙이 지켜지면 개방 폐쇄 원칙에서 확장 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있다.

이와 같이 각각의 원칙들은 서로 다른 원칙을 상호보완하고 더 완전하게 해주는 역할을 한다.

4.ISP

"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다."

ISP원리는 한클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리이다.
한 클래스는 다른 클래스에 종속될 때 필요한 메서드에 대해서(인터페이스)만 의존 관계를 맺어야 한다는 뜻이다.

따라서 한 클래스에 대해 여러 인터페이스를 두어서, 다른 클래스들과 의존관계를 맺을 때 필요한 인터페이스만을 사용하는 것이다.

앞서 살펴본 SRP(단일 책임 원칙)은 클래스가 단일책임을 가지고 있어야 한다고 말한다면 ISP(인터페이스 분리 원칙)은 클래스가 역할에 따른 여러 인터페이스를 두고 있는 것을 말한다.

책임이라는 것의 문제 해결법에 관한 두가지 다른 접근법이라고 생각 할 수 있다.

5.DIP

"고차원 모듈은 저차원 모듈에 의존하면 안된다.
"이 두 모듈 모두 다른 추상화된 것에 의존해야 한다"

"추상화된 것은 구체적인 것에 의존하면 안된다.
구체적인 것이 추상화 된 것에 의존해야 한다."

"자주 변경되는 구체 클래스에 의존하면 안된다."

어느 겨울 날 눈이 많이 와서 자동차의 타이어를 스노우 타이어로 교체해야 하는 일이 생겼다.
그리고 이틀 후 눈이 싹 녹아 타이어를 다시 일반 타이어로 교체해야만 했다.
그런데 일주일 후 폭설이 내려 다시 스노우 타이어로 교체를 했다.

위와 같이 자동차의 타이어를 스노우에서 일반, 일반에서 스노우로 자주 교체하는 일이 생기는 상황에서 일반적으로 우리는 구체화 된 클래스(일반, 스노우)에 의존하면 안된다.

자동차스노우 타이어에 의존하지 않고서도 잘 굴러가야 한다는 말이 되고,
자동차스노우 타이어 중간에 추상화된 타이어 클래스(인터페이스)를 놓아야 한다는 말이기도 하다.

이 말은 위에서 말한 계방폐쇄원칙(OCP)와 닮아 있기도 하다. 하나의 설계원칙에 여러 원칙들이 녹아 들어있는 경우라고 저자는 말한다.

이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게하는 것이 의존 역전 원칙이다.

profile
to Infinity and b

0개의 댓글