객체 지향 프로그래밍이란, SOLID

2022. 8. 17. 20:54C#/C#에 대한 다양한 공부

[객체 지향]

객체 지향 프로그래밍이란?(OPP : Object Oriented Programming)

객체 지향 프로그래밍은 컴퓨터 프로그래밍 페러다임  하나 프로그래밍에서 필요한 데이터를 추상화시켜 상태의 행위를 가진 객체를 만들고  객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.  그대로 객체 지향은 기능이 아닌 객체가 중심 되어 "누가 어떤 일을  것인가?"  핵심이 된다.  객체를 도출하고 각각의 역할을 정의해 나가는 것에 초점을 맞춘다.

객체 지향에 대한 이해를 위해 절차 지향 프로그래밍과 비교하면서 설명하겠다.

객체 지향 프로그래밍은 객체가 중심이 된다면 절차 지향 프로그래밍은 무엇이 중심이 될까?

절차 지향은 기능 중심으로 바라보는 방식으로 "무엇을 어떤 절차로  것인가?"  핵심이 된다. , 어떤 기능을 어떤 순서로 처리하는가에 초점을 맞춘다.

절자지향과 객체지향 방식의 차이

-> 절자 지향은 어떠한 순서대로 흘러가느냐 포인트고, 객체 지향은 돈을_넣는다, 돈을_받는다  객체가 어떤 일을 하는가 초점을 둔다

객체 지향 프로그래밍이 객체가 중심이라는건 알겠다. 이제 객체 지향의 장단점 절차 지향과 비교하며 알아보자

 

객체 지향 vs 절차 지향

객체 지향 프로그래밍의 장점

  • 모듈화, 캡슐화로 인해 유지보수에 용이하다.
  • 상속, 다향성등으로 인해 재사용성이 증가한다.
  • 모듈화등 객체 단위로 분업이 가능하여 여러 명이 같이 개발하는 대형 프로젝트에 적합하다.

객체 지향 프로그래밍의 단점

  • 캡슐화와 격리구조등으로 인해 절차지향 프로그래밍과 비교하면 상대적으로 실행 속도가 느리다.
  • 객체를 어떻게 처리할 것인가에 대한 정확한 이해가 필요하기에 설계단계부터 많은 시간이 소모된다.

 

절차 지향 프로그램밍의 장점

  • 객체나 클래스를 만들 필요없이 바로 프로그램을 코딩할  있다.
  • 컴퓨터의 처리구조와 유사해 실행속도가 빠르다.

절차 지향 프로그래밍의 단점

  • 모든 구성요소가 유기적으로 연결되어 있다는 말은, 하나가 고장났을때 시스템 전체가 고장난다는 말과 같다.  유지보수가 어렵다.
  • 코드가 길어지면 가독성이 매우 떨어진다.

 

객체지향 프로그래밍의 반대는 절차지향 프로그래밍이다?

이말은 틀리다. 왜냐하면 절차지향 프로그래밍이라고 해서 객체를 다루지 않는 것이 아니다. 마찬가지로 객체 지향이라고 해서 절차가 없는것이 아니다. 어디까지나 프로그래밍 접근 방법이 조금 상이할 뿐이다. 대개 절차지향 프로그래밍은' 데이터를 중심으로 함수' 만들고 객체지향은 '데이터와 기능(함수)들을 묶어 하나의 객체' 만들어 사용한다. 그리고 절차지향이라는 말도 정확한 표현은 아니고 절차적 프로그래밍이라고 하는데, 기본적인 절차라는 틀을 깨지 않는 선에서 객체 지향적 프로그래밍을 하느냐 마느냐의 차이라고   있다.

-> 사실 웃긴게 애초에 프로그래밍이 절차적인 기반을 두고 있는데 절차를 지향한다는게 웃긴 표현이다...

 

그럼 객체지향과 절차적 언어를 어떤 기준으로 구분하는가?

  1. 캡슐화, 다향성, 클래스 상속을 지원하는가?
  2. 데이터 접근 제한을   있는가?

보통  기준을 만족하면 객체지향, 만족하지 않으면 절차적 성격이 강해진다.

*객체지향 프로그래밍의 특징

이제 객체지향 프로그래밍이 먼지를 이해했다. 그렇다면 좀더 구체적으로 객체지향 프로그래밍은 어떤 특징을 가지고 있나 알아보자

 

*캡슐화

데이터와 코드의 형태를 외부로부터   없게하고 데이터의 구조와 역할, 기능을 하나의 캡슐형태로 만드는 것을 말한다. , 객체 내부 구현을 외부로부터 감추는 (정보 은닉) 말한다. 그렇다면  외부로부터   없게 감추는 걸까?  이유는 내부구현을 숨김으로 인하여 내부구현의 변화가 발생하더라도 외부 객체에 변화의 영향이 퍼져나가지 않도록 막기 위함이다.

캡슐화에 대해 얘기할때 가장 많이 사용하는 예시

getter/setter 생각해보자 어떤 변수를 private 선언하고 getter/setter 사용한 것을 캡슐화 했다고 한다. 정확히는 데이터 캡슐화다. 하지만 이러한 데이터 캡슐화만으로는 캡슐화의 목적을 달성하지 못한다. getter 있다는  어떤 로직을 수행하기 위해 해당 데이터가 필요하다는 의미고,  로직 또한 또다른 객체에서 수행될 것이다. 그렇다면 getter return type  바뀐다면?  다른 객체 역시 내용이 변경될  밖에 없다. 이런 케이스를 '객체의 내부구현이 드러난다'  한다. 캡슐화의 목적이 내부구현을 숨기는 건데 데이터 캡슐화만으로는 완벽하게 숨기는게 힘든 상황이 발생한다.

결국 변경될  있는 모든 것을 감추는 것이 궁극적인 캡슐화이다. 중요한 것은 이를 지키기 위해 고민해야 하며 응집도는 높이고, 결합도는 낮추려고 끊임없이 고민하는 것이 중요하다.

 

*추상화

인터넷에 검색해보면 '객체의 공통적인 속성과 기능을 추출하여 정의하는  말한다' 나온다. 이게 무슨말일까?

강아지 클래스가 있고 고양이 클래스가 있을때 이들의 공통점을 묶어서 상위 클래스를 만든다면 이는 추상화 한것이다. 그런데 만약 강아지만 하는 행동이 있고, 고양이만 하는 행동이 있다고 해보자.  행동들은 추상화하지 않고  클래스안에만 존재하게끔 하면된다. 이러한 방식으로 객체의 공통적인 속성과 기능들을 추출하고  과정에서 불필요한 것을 지우고 핵심을 남겨두는 과정을 추상화라고 한다.

결국 중요한것은 불필요한 것을 지우고 핵심을 남겨둔다는 추상화의 기본 사고를 기억하면서 여러 가지 상황에 적용해야한다.

 

*상속

상위 클래스의 변수 혹은 메소드와 같은 것들을 하위 클래스가 이어 받는 것을 말한다. 상속으로 인해 코드를 재사용할  있다는 장점이 존재한다.

*상속의 문제점

뭐든 장점만 있을리가 없지...

  • 상위 클래스에 강하게 결합한다

예를 들어 상위 클래스에 string 반환하는 메서드가 있다고 해보자. 이를 갑자기 char 수정한다면 하위 클래스에서 해당 메서드를 사용하고 있다면 모조리 고쳐야 한다. 이런식으로 하위 클래스는 상위 클래스와 강하게 결합하게 되고 그로인해 응집력은 낮아지고 수동적인 객체가 된다는 단점 존재한다.

  • 캡슐화를 깨트린다.

뿐만 아니라, 기능 확장을 위해 하위 클래스에서 오버라이딩했다면 캡슐화를 깨는것이다. 캡슐화를 위해서는 외부에서 메서드를 가져다가 사용만 해야하는데, 오버라이딩은 구현을 수정하는 일이기 때문이다. 이렇게 오버라이딩할때, 문제가 없는지 상위 클래스의 내부 구현을 반드시 확인해야한다. 만약 내부구현을 모른채 재정의한다면 문제가 발생할  있다

-> 사실 상위클래스의 내부 구현을 알아야 한다는 사실 자체가 캡슐화를 깬다…

대표적으로 '정사각형 - 직사각형 문제' 있다.

상속은 is-a관계가 성립할때 사용하라고 많이들 하는데 -> 개는 동물이다(o). 물고기는 광어다(x)

'정사각형은 직사각형이다' 그냥 봤을때는 is-a관계다. (틀린말은 아니니깐…) 그래서 정사각형 클래스가 직사각형 클래스를 상속받아 구현되었다고 가정해보자. 가로-세로의 길이를 변경하는 메소드를 상속받아 사용중이라면 직사각형은 가로-세로의 길이가 달라도 되기 때문에 각각 다른 값으로 가로-세로를 변경할 것이다. 이는 정사각형을 정의가 아니기 때문에 따로 재정의를 해서 사용해야  것이다.

이렇듯 상위 객체의 내부 구현을 제대로 확인하지 않고 대충 메소드만 받아온다던지 하면 문제가 생길  있다. -> 정확하게 알아야 한다는 것은 캡슐화를 깨트리는 ..

 

*다향성

상속과 연관이 있는 개념으로  객체가 다른 여러형태 재구성되는 것을 말한다.

*오버라이드 : 하위 클래스(자식)가 상위 클래스(부모)에서 만들어진 메서드를 자신의 입맛대로 다시 재창조해서 사용하는 것을 말한다.

-> virtual / override 구현

-> base 부모 메서드 접근도 가능

*오버로드 : 하나의 클래스 안에서 같은 이름의 메서드를 사용하지만 각 메서드마다 다른 용도로 사용되며 그 결과물도 다르게 구현하는 것을 말한다.

-> 매개변수를 달리하거나, 리턴타입을 달리하거나..

 

총정리 : 결국 객체 지향 프로그래밍이란 어떤 대상을 추상화 공통점을 찾고, 그것을 캡슐화 과정을 통해 정보은닉등을 하고, 이를 새로운 객체가 상속 받아 재사용이 가능하게 해준다. 그리고 상속받은 객체는 다향성 통해 기능을 수정 또는 추가하여 재사용할  있다.

 

객체 지향 프로그래밍의 특징과 장단점에 대해 알아보았다. 이번에는 객체지향 개발 5 원리에 대해 알아보겠다. 우리 삶에도 원리/원칙이 있듯이 객체지향을 어떻게 하면   만들  있을까에 대한 입증된 원리들에 대해 알아보자

 

*SOLID

SRP(단일책임의 원칙 : Single Responsibility Principle) :  작성된 클래스는 하나의 기능 가지며 클래스가 제공하는 모든 서비스는  하나의 책임을 수행하는  집중되어 있어야 한다는 원칙이다.

-> serialNumber 변화요소라 할수 없고 고유정보에 해당

-> 나머지 요소는 모두 기타의 특성과 관련된 정보이므로 변경이 발생할  있는 부분으로,  부분은 변화요소라   있다. 따라서 특정 정보에 변화가 발생하면 항상 해당 클래스를 수정해야하는 부담이 발생할  있으므로 이부분이 SRP적용의 대상이 된다.

-> 이렇게 분리하면 특성 정보에 변경이 일어나면 GuitarSpec 클래스만 변경하면 된다.

OCP(개방폐쇄의 원칙 : Open Close Principle) : 확장에는 열려있고, 변경에는 닫혀있어야 한다 원리이다. , 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화한다 의미로 OCP 가능케하는 주요 메커니즘은 추상화와 다향성이다.

-> 만약 기타가 아니라 바이올린처럼 새로운 악기가 생기다면 똑같은 구조의 클래스가 추가로 생긴다.

-> 기타뿐 아니라 새롭게 추가될 악기에 대한 추상화

-> 변경에는 비용을 줄이고 확장은 용이하게..

 

LSP(리스코프 치환의 원칙 : The Liskov Substitution Principle) : 상속되는 객체는 반드시 부모 객체를 완전히 대체해도 아무런 문제가 없도록 권고한다. , 올바른 상속을 위해 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙이다. 대표적인 예로 정사각형-직사각형 문제가 있으며 이러한 올바르지 못한 상속관계를 제거하고 부모 객체의 동작을 완벽하게 대체할  있는 관계만 상속하도록 설계한다.

-> 가급적 부모 객체의 일반 메서드를  의도와 다르게 오버라이딩 하지 않는 것이 중요…

 

ISP(인터페이스 분리의 원칙 : Interface Segregation Principle) : 객체가 반드시 필요한 기능만을 가지도록 제한하는 원칙이다.

예를 들어 왼쪽, 오른쪽의 클래스가 가운데 클래스를 상속받는다고 해보자. 이때 왼쪽 클래스는 가운데 클래스의 모든 메서드를  사용하지만 오른쪽은 메서드1 사용한다. 그런데 만약에 메서드2,3 추상클래스라 반드시 재정의를 해줘야한다면 오른쪽 클래스 입장에서는 사용하지도 않는 메서드를 굳이 재정의해줘야 한다. 따라서 각각의 메서드를 인터페이스로 만들어 필요한것만 골라서 사용하도록 하면 된다.

-> 불필요한 기능의 상속/구현을 최대한 방지함으로써 객체의 불필요한 책임을 제거한다.

 

DIP(의존성 역전의 원칙 : Dependency Inversion Principle) : 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 또한 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야한다는 원칙이다. 쉽게 말하면 "자신보다 변하기 쉬운 것에 의존하지 마라"라고 말할  있다. DIP하면 대표적인 예제가 바로 '자동차와 스노우 티이어 이야기'.

 

*자동차와 스노우 타이어 이야기

-> 자동차(고수준 모듈) 여러 스노우타이어(서수준 모듈) 의존하는 경우

 겨울이 되면 타이어를 스노우 타이어로 변경해야 할것이다. 이러면 나중에 타이어가 추가 혹은 변경될 경우 계속해서 코드를 수정해야 할것이다. 이는 개방폐쇄원칙에 어긋난다.

 

위와 같이 수정하면 자동차는 타이어라는 인터페이스만 가지고 있기 때문에 변화에 민감하게 반응할 필요가 없다.

=> 의존성역전의 원칙은 중요도가  떨어지는데  이유는  원칙의 하위호환 격이기 때문이다. 다른 원칙을 지키다보면 자연스럽게 준수할  있기 때문이다.

 

요약 : 객체 지향 프로그래밍을 잘하기 위해 원칙과 특성을 이해하고 사용하면  멋진 코딩이 가능하다

 

[추가내용]

*Virtual Table

어떤  클래스가 해당 클래스 혹은 Base클래스에서 하나 이상의 가상메서드를 갖는다면, 해당 클래스의 Method Table metadata내에 virtual table 갖게 된다.

Vtable 객체 레퍼런스가 가리키는 heap 객체의 type Handle이라는 곳의 Method table내에 위치한다.

기본적으로 모든 클래스는 System.Object 클래스로부터 상속 받기 때문에 모든 클래스에는 Vtable 존재한다. 여기에서 base클래스에 가상메서드가 없다면 VTable base클래스의 메서드를 추가하지 않는다.

 

C#에서 파생클래스가 Base클래스의 메서드와 동일한 이름을 가지게 하는 방법으로는 오버라이딩과 hiding 있다.

  • Method Overriding

오버라이딩을 이해하기 위해선 2가지를 먼저 알야아한다.

  1. 클래스로부터 객체를 생성되어 어떤 변수에 할당되었을때, 변수의 타입에 상관없이 해당 클래스의 Method Table 사용
  2. 만약 파생클래스의 객체가 보다 상위의 base클래스의 변수에 할당된다면  변수는 base클래스의 안에 있는 변수나 메서드만 사용 가능하다.
  • A a = new B();

a라는 변수가 base클래스 변수이므로 Run1() base 메서드로 할당된다.

  • B b = new B();

B클래스의 Run1() 메서드가 할당된다.

 

-> A.Run1()자리에 B.Run1() 들어간 것을   있음

 

Base클래스의 virtual 메서드를 파생클래스에서 오버라이드하면 파생클래스의 vtable 별도로 추가하는게 아니라 base클래스 메서드 포인터에 넣는다.

 

, [base클래스] [변수] = new [파생클래스]();  선언하면 base클래스의 메서드가 호출되지만 우리가 평소에 쓰는 형태, [파생클래스] [변수] = new [파생클래스](); 선언하면 base클래스의 메서드가 저장되어있을 자리에 파생클래스의 메서드가 들어가 override 된것이다.

 

  • Method Hiding

만약 오버라이딩을 하지 않고 그냥 base 클래스와 메서드명이 같으면 어떻게 될까? 아까 오버라이딩하게 되면 base클래스의 메서드를 덮어씌운다는 것을 알았다. 하지만 하이딩은 그건 그대로 두고 새로운 메서드를 추가해서 해당 메서드가 나오게끔 하는 것이다.

-> 기존 메서드를 덮어씌우는 오버라이딩과 방식이 다름

 

*IDisposable

dispose 단어 자체의 의미가 '제거하다, 처분하다' 라는 뜻을 가지고 있다.  제거 가능한, 사용  버리게 되어있는, 일회용의  

 

C#에서  사용한다는 거고  제거한다는 것일까?

메모리다. 메모리를 썼으면 해제도 해야한다. 그런데 GC 알아서  이상 사용하지 않는 개체들을 소거해가는데  Dispose 필요할까?

 

  • GC 관리되지 않는 리소스들은 인식하지 못한다.
  • GC 어느 시점에 일어나는지를 모른다.
  • GC 너무 자주 발생하게 되면 오버헤드가 일어난다.

 

IDisposable 상속 받고 Dispose() 구현하면 TestClass 종료될때 Dispose 호출된다.

 

근데 using 멀까?

using 추가하고 괄호 묶어야 t 언제까지 사용할지를 알려주지 않기 때문에 Dispose() 실행되지 않는다. 때문에 using 추가한다.

 

사람은 언제나 실수를   있다. 개발자가 Dispose 명시적으로 호출해준다는 보장만 있다면 괜찮으나, 언제든 실수를   있다.  때문에 좀더 안정적인 클래스 구현을 위해 종료자가 필요한 것이다.