📌 객체 지향
소프트웨어의 가치
소프트웨어의 가치는 사용자가 요구하는 기능을 올바르게 제공하는 데 있다.
요구 사항은 언제나 변한다. 시간이 흐름에 따라 이전에 필요 없다고 생각했던 기능이 필요해질 수도 있고, 기존에 구현된 기능의 일부를 변경해야 할 수도 있다.
요구 사항이 바뀔 때, 그 변화를 더 수월하게 적용할 수 있는 장점을 얻기 위해 사용된 것이 바로 객체 지향 기법이다.
객체 지향 기법을 적용하면 소프트웨어를 더 쉽게 변경할 수 있는 유연함을 얻을 수 있다.
📌 절차 지향
- 절차 지향의 '절차' 의 의미는 Procedual 즉 함수를 의미한다.
- 절차적 프로그래밍이란 단순히 순차적인 명령 수행이 아니라 루틴, 서브루틴, 메소드, 함수 등(이를 통틀어 프로시저라고 한다.)을 이용한 프로그래밍 패러다임을 뜻한다. 명령형 프로그래밍의 일종이다
- 절차지향은 프로그램의 순서를 먼저 결정하고 데이터와 함수를 설계하는 반면, 객체지향은 데이터와 프로시저를 하나로 묶는 객체라는 단위를 먼저 설계한 후 프로그램의 순서를 결정한다는 차이가 있다.
절차 지향의 장점
- 비교적 유지, 보수가 덜 중요한 개인 프로젝트에 적합할 수 있다
- 객체지향 프로그래밍에 비해 빠르다.
절차 지향의 단점
- 데이터의 수정(타입 변경, 의미 변경)이 요구될 때 여러 프로시저에서 수정이 요구된다. 따라서 프로그램의 규모가 커질수록 코드의 수정과 기능의 추가에 대한 비용이 크다.
- 실행 순서(프로시저 호출 순서)가 정해져 있음으로, 변경될 경우 동일한 결과를 보장하지 않는다.
- 같은 데이터를 프로시저들이 서로 다른 의미로 사용하는 경우가 발생한다.
📌 객체 지향
- 객체 지향은 데이터 및 데이터와 관련된 프로시저를 객체(object)라고 불리는 단위로 묶는다
- 객체는 프로시저를 실행하는데 필요한 만큼의 데이터를 가지며, 객체들이 모여 프로그램을 구성한다
- 작은 문제를 해결하는 객체를 생성해 큰문제를 해결하는 Bottom-Up 방식이다.
객체 지향의 장점
- 객체끼리 데이터를 공유하지 않기 때문에, 요구사항의 변경 즉 데이터의 수정(타입 변경, 의미 변경)이 요구될 때 해당 데이터를 가지는 객체만 수정하면 된다.
- 기능이 객체 별로 분리되어 있기 때문에 디버깅, 분석이 비교적 용이하다.
- 유지보수가 쉽기 때문에 대형 프로젝트에서 유용하다.
객체 지향의 단점
- 객체 지향적으로 설계하고 개발하는데 비용이 크다. (객체끼리의 관계와 재사용성을 높이기 위한 상속, 다형성 등의 고민)
- 절자지향에 비해 비교적 느리다.
<절차지향, 객체지향>
- 객체지향의 반대가 절차지향도 아니며, 절차지향의 반대가 객체지향도 아니다
- 객체지향은 기능(operation) 중심, 절차지향은 데이터 중심의 패러다임이다.
📌 객체
- 객체 지향의 기본은 객체다.
- 객체가 내부적으로 데이터를 어떤 타입으로 보관하며 그 데이터를 어떻게 처리하는지 보다 객체가 어떤 기능을 제공하냐가 중요하다.
인터페이스
- 객체가 제공하는 모든 오퍼레이션의 집합이다.
- 객체를 사용하기 위한 일종의 명세이다.
- 실제 기능을 포함하고 있지 않다.
- 자바나 C#에서 제공하는 인터페이스 타입과 개념적인 인터페이스는 다르다.
클래스
- 인터페이스에 명시된 오퍼레이션을 실질적으로 구현한다.
- 객체를 생성하기위한 청사진
- 오퍼레이션을 구현하기 위한 속성 (property), 메소드(method)를 포함함
- 클래스를 이용해 메모리에 객체 생성이 가능하며, 이렇게 생성된 객체를 인스턴스라고 한다.
메세지
- 메시징의 의미는 객체가 또 다른 객체의 인터페이스를 통해 어떠한 행위를 하라고 명령하는 것으로 필요하다면 데이터를 담아 보낼 수 있다.
- 이것을 행위의 책임을 위임했다 해서 위임이라 하기도 한다.
- 각 객체는 기능을 요청받으면, 해당 기능을 실행한뒤, 응답을 전달한다.
코틀린, 자바 같은 언어에서는 메서드를 호출하는 것이 메시지를 보내는 과정에 해당된다.
// kotlin
val input: FileInputStream = FileInputStream()
val data = ByteArray(512)
val readByte = input.read(data)
이 코드에서 input 변수는 FileInputStream 타입의 객체를 참조하는데, input.read(data) 코드는 input 이 참조하는 객체에 read() 오퍼레이션을 실행해 달라는 메세지를 전송한다고 생각하면 된다.
📌 객체의 책임과 크기
- 객체는 그 기능들로서 정의되고 이에 따라 객체 각각은 책임을 갖게된다.
- 객체의 책임은 작으면 작을수록 좋다. 더 나아가 SOLID 정신의 단일책임의 원리와도 일맥상통하다.
- 책임을 세분화 할 수록 유지보수에 유리해진다.
- 책임을 어떻게 분배할 것인가 == 기능을 어떻게 분배 할 것인가 == 객체 지향 설계의 시작
카페의 바리스타를 예로 들어보자. 바리스타가 가져야 할 능력은 우선 커피를 만들 수 있어야 한다는 것이다. 또 커피에 대한 설명을 할 수 있어야 하고, 크기가 작은 카페라 서빙까지 해야한다고 해보자.
class Barista{
fun makeCoffee(order: Order){
// 커피를 만들고 반환하는 로직
}
fun explainAboutCoffee(coffee: Coffee){
// 커피에 대해 정보를 설명하는 로직
}
fun serving(coffee: Coffee, orderNum: Int){
// 주문 번호에 맞는 손님에게 커피를 서빙하는 로직
}
}
현재 바리스타가 가진 책임은 몇가지 일까? 커피를 만들 수 있는 능력, 설명할 수 있는 능력, 서빙할 수 있는 능력 이렇게 3가지로 보인다.
(사실 아닐 수도 있다. 책임의 가짓수는 메서드의 개수와는 다르다.)
책임의 기준은 개발자의 의도된 설계에 따라 다를 수 있다. 중요한 것은 수정상황에 대비하여 적당한 크기로 책임을 정해 클래스에 부여 하는 것이다.
당연히 바리스타 혼자 커피도 만들고 서빙도 하는 것은 힘들다. 서빙과 커피에 대한 설명은 종업원에게 맡겨야 한다.
class Barista {
fun makeCoffee(order: Order) {
// 커피를 만들고 반환하는 로직
}
}
class Employee{
fun explainAboutCoffee(coffee: Coffee){
// 커피에 대해 정보를 설명하는 로직
}
fun serving(coffee: Coffee, orderNum: Int){
// 주문 번호에 맞는 손님에게 커피를 서빙하는 로직
}
}
종업원에게 서빙과 설명을 맡곁다. 이제 바리스타의 클래스가 수정되어야 한다면 어떤 이유일까? 커피를 만드는 것에 대한 이유일 것이다. 수정의 이유 또는 변경하려는 이유는 커피를 만드는 것에 변화 뿐일 것이다.
따라서 Barista는 하나의 책임을 가진다고 할 수 있다.
📌 의존
- 객체가 구현에 있어서 다른 객체를 생성하거나 , 다른 객체의 메서드를 호출하는 경우가 있다면 -> 그 객체에 의존한다 라고 한다.
- 의존관계에서 한 객체의 변화는 다른 객체의 변화로 전이 될 수 있다.
- 의존관계중 자신을 의존하고 있는 객체의 의존관계를 따라가면 다시 자기자신이 의존하는 관게에 있는 경우를 순화의존 관계(순환참조)라고 한다.
- 순환 의존은 객체 지향 설계 원칙 중 의존 역전의 원칙(DIP)를 이용해 연결고리를 끊을 수 있다.
- 의존 역전의 원칙(DIP)는 추상화(인터페이스 등)에 의존해야하며 구체화에 의존하면 안된다는 뜻으로, 클래스에 의존하게 되면 위와 같이 순환 참조문제나 클래스의 변경이 발생했을 때 유연하게 대응하기가 어렵다.
- 추상화(인터페이스)에 의존하게 되면 클래스의 변경이 발생하거나 동작은 동일하지만 아예 다른 기능을 사용해야 할 때에도 단순히 코드의 전체를 바꿀 필요없이 클래스만 교체해서 사용할 수 있다.
📌 캡슐화
- 캡슐화는 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것이다.
- 캡슐화를 통해 캡슐 내부의 변경이 외부의 변경을 가하지 않게 하여 유지보수 쉽게한다.
- 기능의 캡슐화를 잘 지킨다면 내부의 변경으로부터 그 기능을 사용하는 외부의 변경을 막을 수 있다.
캡슐화의 방법
방법1 : Tell Don't Ask
- 데이터를 묻지 않고 실행해달라는 규칙이다.
- 데이터가 아닌 기능을 요청할때 그 기능을 어떻게 구현했는지 여부는 자연스럽게 캡슐화되어 감춰지게 된다.
- ex] if (member.getExpireDate() != null) -> if(member.isExpired())
방법2 : 데이테르 법칙
- 메서드에서 생성한 객체의 메서드만 호출
- 파라미터로 받은 객체의 메서드만 호출
- 필드로 참조하는 객체의 메서드만 호출
- 데미테르 법칙의 핵심은 캡슐 내부로 침투함을 방지함으로써 캡슐화를 이루는 것이다.
- [ 위반 1 ] 연속된 get 메서드를 호출하는 것을 피한다.
- [ 위반 2 ]임시 변수의 할당된 객체의 get을 호출하는 코드가 많은 상황을 피한다. : [ 위반 1]의 연장선 상이다.
// 위반 2 예시
A a = someObject.getA();
B b = a.getB();
value = b.getValue();
📝 객체지향 설계과정 (정리)
- 프로그램을 구현하는데 필요한 기능들을 정리하고, 세분화한다
- 정리,세분화된 기능들을 알맞은 객체에 할당한다. 각 객체가 가지는 책임을 최소화 하도록 한다. (SRP 원칙 기억)
- 기능을 구현하는데 필요한 데이터를 객체에 추가. ( 데이터를 추가하고 데이터를 이용해 기능을 구현해도 무관)
- 기능은 가능한한 캡슐화 원칙을 지키면서 구현한다. (Tell Don't ask, 데미테르 법칙 기억)
- 객체 간의 어떻게 메시지를 주고 받을지 결정 , 이 때 순환의존 관계가 발생하지 않도록 주의한다.
- 1~5 과정을 프로그램 구현 도중 계속 반복해서 진행한다.
'OOP' 카테고리의 다른 글
OOP - DI(Dependency Injection)와 서비스 로케이터 (0) | 2023.02.26 |
---|---|
OOP - SOLID 원칙 (0) | 2022.10.27 |
OOP - 재사용: 상속보다 조립 (0) | 2022.04.07 |
OOP - 다형성과 추상 타입 (0) | 2022.02.14 |