본문 바로가기
Server System/Software Architecture

객체 지향적 사고, 객체 지향적 프로그래밍 Part 1.

by 알파해커 2020. 10. 24.
반응형

객체 지향적인 프로그래밍을 한다고 했을 때 떠오르는 것들

SOLID

Single Responsibility Principle

Open Closed Principle

Liskov Substitution Principle

Interface Segregation Principle

Dependency Inversion Principle

 

GoF

Factory Pattern

Strategy Pattern

Decorator Pattern

Visitor Pattern



구조적(절차지향적) 프로그래밍 vs 객체지향 프로그래밍

이 둘의 가장 큰 차이점은, 객체 지향 프로그래밍에서는 “Shift of responsibility”(책임의 이동) 라는 개념이 적용되었다는 것이다. 그것이 핵심이다. 예를 들어, “모든 학생들에 대해서, 각 학생들이 다음 수업을 듣기 위해 어디로 가야하는지 알려주는" 프로그램은 짠다고 가정해보자.

 

구조적 프로그래밍에서는 다음과 같이 구현할 수 있을 것이다.

 

Main에 해당하는 어떠한 주체가, 

1. 학생들을 모두 불러와서

2. 각 학생의 스케쥴을 가져오고,

3. 스케쥴에 따라 다음 수업이 무엇인지 가져오고,

4. 해당 수업의 위치를 가져오고,

5. 그 위치로 가기 위한 방향을 제공한다.

 

구조적으로 각 기능에 대한 메소드가 나누어져 있지만, Main에게 그 모든 것을 총괄하는 책임이 있다.

 

 

같은 내용을 객체 지향적으로 설계하면 어떻게 될까.

객체 지향의 핵심은 “책임의 이동"이라고 했다. 다시 말해, 각 객체에게 어떠한 책임이 존재하고, 각자 책임에 맞는 행위를 수행하면 된다.

 

그럼 책임에 따라 객체를 나누어 보자.

 

- Instructor

학생들에게 다음 클래스로 가라고 알려주는 객체

 

- Student

자기들이 현재 어떤 클래스에 있는지 아는 객체

다음 클래스로 어디로 가야하는지 아는 객체

이전 클래스에서 다음 클래스로 가는 객체

 

- Classroom

클래스의 위치를 가지고 있는 객체

 

- Direction Giver

두 개의 클래스가 주어지면 (현재 클래스, 다음 클래스), 다음 클래스로 가는 방향을 알려주는 객체

 

 

나누어진 각 객체의 역할에 따라, 위와 같이 구현될 수 있을 것이다.

 

Student는 다음 클래스로 가는데, Direction Giver가 방향을 알려고, 각 ClassRoom 객체는 위치를 가지고 있고.. 

객체는 자신의 책임에 따라 수행하고, 그 책임들이 모여 협력하면서 프로그램이 돌아간다.



객체 지향의 목표

객체 지향적인 설계의 목표는 OCP를 만족하는 것이라고 해도 과언이 아니다.

OCP는 SOLID 원칙 중 하나로, “새로운 요구사항에 의해 새로운 것을 추가할 수는 있지만(Open), 기존에 작성되어 있는 코드를 수정하는 것에는 닫혀있어야 한다(Close)”는 것이다.

 

즉, 코드를 추가하는 것은 좋지만, 코드를 수정하는 것은 허용하지 않겠다는 것이다.

 

구조적 프로그래밍의 경우에는 일반적으로 분기문(if 문)을 이용해서, 코드를 작성하기 때문에, 요구사항이 추가되었을때 코드의 수정을 피하기 힘들다. 그러나, 객체 지향에서는 각 객체가 가지고 있는 책임과 객체들의 협력을 통해서, OCP를 구현해 내는 것이 가능하다.

 

객체의 책임은 어떻게 만들어지고, 협력은 어떻게 할까

 

 

객체의 책임은 해당 객체가 가장 잘할 수 있는, 이른바, ‘주특기'에 의해서 결정된다.

그리고 각 객체들이 다른 객체에게 그 ‘주특기'를 수행해달라고, ‘메세지'를 보내면서 협력 하는 것이다.

 

프로그램이라는 것은 실제 세계의 문제를 가상세계로 가져와서 푸는 것이라고 할 수 있다. 이때, 구조적 프로그래밍 보다, 객체 지향적인 프로그래밍이 실제 세계의 것을 가상 세계로 '맵핑' 하기 더 용이하다. (우리 주변의 사물이나 각자의 역할을 객체라고 떠올려보자.)



객체(Object or Entity) vs 속성(Attribute or Value object)

클래스를 이용해서 만든 인스턴스들은 모두 객체인가? 사실 객체지향적인 설계에서는 그렇다고 보지 않는다. 다음의 예제(Color, Beauty, Hungry, …)는 객체인가?

 

 

속성(Attribute라고 하거나 DDD에서는 Value object라고 하는 것)은 단독으로 사용될 수 없고, “객체 안에 들어가서 존재" 함으로써, 객체의 상태를 설명해주는 역할을 한다.

 

그렇다면 무엇을 기준으로 객체와 속성을 구분할 수 있을까?

 

첫 번째는 앞서 설명한 ‘주특기'를 가지고 있는지 여부이다. 객체는 자신이 수행할 수 있는 behavior, 즉 주특기가 있어야 한다. (상태를 가지는 것의 여부는 중요한 요소가 아니다. 상태는 있을 수도 있고 없을 수도 있는 것이다. 상태에 따라 주특기의 결과가 달라지는 것이지, 그것으로 객체 여부를 따질 수 없다. 상태는 그저 그 객체의 그 시점의 스냅샷일 뿐이다.)

 

두 번째는 Identity의 여부이다. 사람을 예로 들어보자. ‘나' 라는 Identity는 이름이 바뀐다고 달라지는 것일까? 키가 크고, 살이 찌고, 나이를 먹는다고 달라지는 것일까? 그렇지 않다.

 

같은 논리로 아래의 예제를 보자. “b1”, “b2”는 Identity인가? 그리고 b1, b2는 같은 Identity를 가지는가?

 

 

“b1”, “b2”라는 이름은 Identity가 아니다. 객체의 이름일 뿐. 그러나 이 둘은 같은 Identity를 가진다. 그러니까 자바에서는 주소값이 같으면 같은 Identity로 본다는 것이다. 

 

그래서 자바에서는 객체 Identity 를 비교할때는 “==”를 쓰고, 객체 상태를 비교할때는 “equals”를 쓴다. 

 

객체 지향의 객체(object)는 DDD에서는 Entity고 객체 지향의 속성(attribute)는 DDD에서 value object이다. DDD에서 Entity와 value object를 나누는 것은 Identity로 한다. Identity를 가져야 하느냐 아니냐가 유일한 기준이다. behavior를 가지느냐 아니냐는 부수적인 요소이다.



예를 들어, Color라는 것이 있고 속성으로 r,g,b 값을 가진다면 이걸 Identity로 구분할 이유가 있나? 똑같은 r,g,b 값을 가졌지만 서로 다른 Identity를 가질 필요가 있나? 일반적으로는 없다. 그런 Usecase를 찾기 힘들다. 그러면 속성(attribute)이다. 

 

(객체는는 mutable, 속성은 immutable이다. 코딩할 때도 그러한 특성을 고려해서 작성해야 한다.

속성은 immutable이기 때문에 상태 값을 변경시키는 행위가 있으면 안되고, 필요하다면 새로운 속성을 생성하는 방식으로 구현 해야 한다.)

 

이때 Color는 operation을 가질수없나? 있다. 

그렇기 때문에 operation(즉, behavior)으로 객체와 속성을 구분 할 수 없는 것이다.

 

아래의 예제를 보자. Color는 속성임에도 불구하고 operation(mixWith 메소드)를 가지고 있으며, mixWith를 통해 상태를 변경시키는 것이 아니라, 새로운 속성을 만들어내고 있다.

 

 

객체는 상태가 변해도 Identity가 같아야 된다.

속성은 크레파스 같은거다. 빨간색 크레파스를 잃어버려서 애가 우는데, 똑같이 생긴 다른 빨간색 크레파스 갖다 주면 걔가 다르다고 울까? 아니다. 똑같은거다. 



협력의 방법

1. 다형성

클라이언트 입장에서 똑같은 메세지를 보냇는데, Receiver 입장에선 서로 다른 동작을 할 수 있다.

 

 

2. 요청

데이터를 가져와서 처리할려고 하지말고, 데이터를 가지고 있는 객체에게 처리해달라고 요청해라.

 

3. 인터페이스

외부에 공개된 인터페이스를 통해서만 요청(메세지)을 주고 받는다. 

내부가 어떻게 되있는지는 알 필요도 없고, 알려고도 하지마라. 

이게 Testability에서도 중요한 역할을 한다.

 

다형성이 왜 중요한가? 

이것을 통해 프로그램을 indirection하게 구현할 수 있다. 

객체 지향의 진수이며, 이것을 통해 OCP가 가능해진다. 

 

객체 지향에서 가장 중요한 개념은 “is-a” 관계이다. 

generalization / specialization 의 개념이며, 즉 포함 관계를 의미한다. 

부모가 더 포괄적인 것이다. 

 

개념적으로, Mammal "is an" Animal이므로 is-a 관계다. 

포유류"는" 동물이다. 역은 성립하지 않는다. one way 다.

 

 

잘모르겠으면 이것만 기억하자.

“부모가 하는건 자식도 다할수있다”



상속

상속 관계를 설계할 때 일반적으로는 서로 다른 클래스를 만들었다가 공통 부분을 추출해서 뽑아 올리고, 뽑아 올리고, .. 하면서 Bottom-Up 방식으로 만들어진다.

 

 

자식에서는 사용하지 않는 메소드를 상속받는 것은 나쁜 구조이 다. 예를 들어 새와 펭귄의 상속 관계가 있고 새에는 fly라는 메소드가 있다고 했을때 펭귄은 fly를 하지 못할때 펭귄.fly() 는 abort() 하거나, do nothing 하도록 비워놓거나 해 야하는데 이런 걸 refused request라고 하고. 이것은 사실 객체지향적인 관점에선 별로 좋은 어프로치가 아니다. 

 

 

이것을 해결할 수 있는 방법이 몇가지 있을 수 있는데 일단, 위에 언급했듯이 refused request를 할 수도 있는거고 다른 하나는 펭귄과 새의 부모자식 관계를 뒤집는 거다. 

 

펭귄이 부모고 새가 자식이면 fly를 새만 가지게 했을때 문제가 안된다. 그런데 이렇게 되는 경우 모든 새는 펭귄이라는 이상한 관계가 되니까 이것도 문제가 될 수있다. 

 

다른 하나는 중간에 클래스 레이어를 하나 더 두는 것이다. 날 수 있는 새와 날 수 없는 새를 두면, 날 수 없는 새를 펭귄이 상속하면 된다.

 



LSP

부모가 사용되는 곳이면 자식도 사용될 수 있어야 한다. 역은 성립하지 않는다. 

이 용도로만 상속을 이용해야한다. 그것이 LSP가 의미하는 것이다.

 

 

부모로 부터 어떤 것을 물려받을 것인지 선택할 수 있나? 인터페이스를 물려받을 것인가 구현(implementation)을 물려받을 것인가? 인터페이스를 물려받으려면 그것을 명시적으로 표현해야하고 implementation도 마찬가지다. 아래는 인터페이스를 상속하는 것이 목적이다 이걸 서브타이핑이라고도 한다 퍼블릭 상속의 목적은 인터페이스를 상속하는 것이다. 자바에서는 이러한 목적으로만 상속을 이용해야한다.

 

위와 다르게 프라이빗 상속은 implementation을 물려받는게 목적 이다. implementation 상속은 자식이 부모의 코드를 재사용하는 것이 목적이다. 자식의 클라이언트 들을 위한게 아니고. 인터페이스 상속과는 다르게 is-a 같은 '관계'가 없다. 이건 서브타이핑이라고 하지 않는다. LSP를 만족하지 않으므로. 상속을 받은 목적이 오직 처음부터 구현하지 않고 부모의 구현체 를 받아오기 위한거다.

 

*프라이빗은 상속이 안된다고 착각하는데, 사실은 그렇지 않다. 프라이빗도 상속이 되어 메모리에 잡히고 다만 액세스가 안될 뿐이다. 그래서 아래의 예제에서 bar의 사이즈는 8이다. (다만, 특정 언어에서는 그렇지 않을 수도 있는데.. 일반적으론 상속되서 메모리에 다 잡힌다고 생각하면 된다)

 

자바에서 이걸 쓰게 되면 아래와 같은 문제가 생긴다. C++에서는 프라이빗 상속을 허용하기 때문에, 부모에서 퍼블릭 인 애들도 자식에선 프라이빗이 된다. 그런데 자바에서는 퍼블릭 상속만 가능하므로, 부모로 부터 상속 받은 것들이 자식에서도 퍼블릭이다. 그럼 아래와 처럼 기존 부모 의 구현 내용을 이용해서 스택을 조금의 코드로 구현 하는 상황이 라면, 자식인 Stack클래스는 push pop top만 제공하게 되어있 는데, 부모로 부터 물려받은 거 때문에 add도 코드적으로는 문제 없는게 되버린거다. 이렇게 되면 안된다. 의도하지 않은 동작이 가능해지기 때문에. 이렇기 때문에 자바에서는 상속을 구현 상속 으로 사용하면 안된다는 거다. 씨쁠쁠에서는 프라이빗하게 메소 드가 상속이 되서 저런 문제는 안생기게 할수는 있지만, 또 그걸 우회해서 호출하려고 맘먹으면 호출이 가능하기도 하다. 자바에 서는 그래서 아예 그런 기능을 제공하지 않고 퍼블릭 상속만 되게 한거고. 때문에 철학적으로 is-a 관계가 아닌 영역에서는 사용하 지 않아야 한다.

 

 

코드 재사용이 목적이라면 상속이 아니라 aggregation이나 composition aggregation (즉, 사용하는 관계로 만들어라)을 이용해야 한다.

 



상속 남용

아래의 Point, Circle 예제를 보자. is-a 관계가 성립한다고 볼 수 있나? LSP가 성립하나?

그렇지 않다. Circle은 Point가 아니다. 

 

해당 코드는 단순히 x라는 속성 값을 재사용하고 싶어서 상속을 이용한 것이다.

이런 것은 상속 남용이다. 

 

공통인 중복된 코드를 사용하고 싶으면 Composition으로 표현해야 한다.

 

LSP를 성립하지 않는다는 것은 문법적으로 따지는 것(부모의 자리를 자식으로 대체해도 코드 동작에 오류가 생기지 않는지) 뿐만 아니라, 의미적으로 맞는지도 따져봐야 한다. 

 



반응형

댓글