OOP/Fundamentals

🪗 OOP Fundamentals (1)

metamong 2025. 3. 2.

목차

❤️ OOP 이전: 중심이 컴퓨터. 컴퓨터가 사고하는 대로 프로그래밍.

 

❤️ OOP: 인간 중심적 프로그래밍 패러다임. 현실 세계를 프로그래밍으로 옮겨와 프로그래밍. 객체 지향의 가장 기본은 객체이며, 객체의 핵심은 기능을 제공하는 것. 실제로 객체를 정의할 때 사용하는 것은 객체가 제공해야 할 기능(오퍼레이션(Operation))이며, 객체가 내부적으로 어떤 데이터를 갖고 있는 지로는 정의되지 않는다. 즉, 객체는 오퍼레이션으로 정의된다.

 

(1) 추상화) 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 APP에 필요한 특징들을 뽑아와 프로그래밍 진행.

 

(2) 이미 작성한 코드에 대한 재사용성이 높다. 자주 사용되는 로직을 라이브러리로 만들어 두면 계속해서 사용 가능, 신뢰성 확보

 

(3) 라이브러리를 각종 예외 상황에 맞게 잘 만들어 두면 개발자가 사소한 실수를 하더라도 그 에러를 컴파일 단계에서 잡아낼 수 있으므로 버그 발생이 줄어든다 & 라이브러리 내부 동작 몰라도 개발자는 쉽게 사용 가능하므로 생산성 높음 & 객체 단위로 코드가 나누어져 작성되므로 디버깅 쉽고 유지보수에 용이

 

(4) 데이터 모델링할 때 객체와 매핑이 수월하기 때문에 요구사항을 보다 명확하게 파악하여 프로그래밍 가능

 

(5) 객체 간 정보 교환이 모두 메시지 교환을 통해 일어나므로 실행 시스템에 많은 overhead 발생(but, 하드웨어의 발전으로 많이 보완)

 

(6) 단점) 객체가 상태를 갖고, 예측하지 못한 상태로 바뀔 수 있기 때문에 '함수형 프로그래밍(FP; Functional Programming)' 패러다임을 도입해 순수함수와 불변성을 사용하여 예측 가능한 코드를 작성할 수 있다. (함수형 프로그래밍 별도 포스팅 참조)

 

❤️ 객체 지향적 설계 원칙

(1) SRP(Single Responsibility Principle): 단일 책임 원칙

: 클래스는 단 하나의 책임을 가져야 하며, 클래스를 변경하는 이유는 단 하나의 이유여야 한다. 단일 책임 원칙을 지키지 않았을 때, 한 책임의 구현 변경에 의해 다른 책임과 관련된 코드가 변경될 가능성이 높다. 

 

(2) OCP(Open-Closed Principle): 개방-폐쇄 원칙

: 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

 

(3) LSP(Liskov Substituion Principle): 리스코프 치환 원칙

: 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

 

(4) ISP(Interface Segregation Principle): 인터페이스 분리 원칙

: 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다. 각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 각 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 해야 한다.

(* 인터페이스에 여러 클라이언트 사용 및 분배 가능)

 

(5) DIP(Dependency Inversion Principle): 의존 역전 원칙

: 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 즉, 저수준 모듈이 변경되더라도 고수준 모델은 변경할 필요 x. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

 

❤️ 용어 정리

(1) 시그니처

: 객체 지향으로 설계하기 위해서는 오퍼레이션의 사용법을 알아야 한다. 오퍼레이션의 사용법 3가지로 구성. 아래 3가지를 시그니처(signature)

 

① 기능 식별 이름

② 파라미터 및 파라미터 타입

③ 기능 실행 결과 값 및 타입

 

(2) 인터페이스

: 객체가 제공하는 모든 오퍼레이션 집합. 객체를 사용하기 위한 명세를 의미한다고 보면 된다.

 

(3) 메시지

: operation의 실행을 요청하는 것을 '메시지를 보낸다'라고 표현. java에서는 메서드를 호출하는 것을 '메시지를 보낸다'

 

(4) 책임

: 객체가 자신이 제공하는 기능으로 정의된다는 것은 객체마다 자신만이 제공할 수 있는 기능에 대한 책임이 있다. 객체가 갖는 책임의 크기가 작을수록 좋다(= 객체가 제공하는 기능의 개수가 적은 것이 좋다). 한 객체에 많은 기능이 포함되면, 그 기능과 관련된 데이터들도 한 객체에 모두 포함. 이 경우, 객체에 정의된 많은 operation들이 데이터들을 공유하는 방식으로 프로그래밍 되나, 절차지향 방식과 다를 바 없다. 따라서 책임의 크기는 작을수록 유연해진다. (= 단일 책임 원칙(SRP))

 

(4) 의존성

: 한 객체가 다른 객체를 의존한다는 것은, 실제 구현에서는 한 객체의 코드에서 다른 객체를 생성하거나, 다른 객체의 메서드를 호출한다는 것. 의존의 영향은 꼬리에 꼬리를 문 것처럼 전파된다. 이러다가 변경한 영향이 다시 자기 자신 객체까지 변화시킬 수 있으며, 이를 '순환 의존'이라 한다. 이것을 해결하기 위해 사용하는 방법을 'Dependency Inversion Principle(DIP)'

 

(5) 캡슐화

① Tell, Don't Ask

: 데이터를 물어보지 않고, 기능을 실행해 달라고 말하라는 규칙. 데이터를 읽는 것은 '데이터를 중심'으로 코드를 작성하게 만드는 원인. 데이터를 private으로 클래스 내부에 숨기고, 메소드를 통해 데이터에 접근

 

데미테르 법칙

: 메서드에서 생성한 객체의 메서드만 호출 / 파라미터로 받은 객체의 메서드만 호출 / 필드로 참조하는 객체의 메서드만 호출

 

❤️ 객체 지향 설계 과정

(1) 제공해야 할 기능을 찾고, 또는 세분화하고 그 기능을 알맞은 객체에 할당

 

(1)-1 기능을 구현하는 데 필요한 데이터를 객체에 추가

(1)-2 객체에 데이터를 먼저 추가하고, 그 데이터를 이용하는 기능을 삽입

(1)-3 기능은 최대한 캡슐화하여 구현

 

(2) 객체 간에 어떻게 메시지를 주고 받을 지 결정

 

❤️ 상속을 통한 재사용의 단점

(1) 상위 클래스 변경의 어려움

: 어떤 클래스를 상속받는다는 것은, 그 클래스에 의존한다는 것. 따라서, 의존하는 클래스의 코드가 변경되면 영향을 받을 수 있음. 변경의 여파가 계층도를 따라 전파.

 

(2) 클래스의 불필요한 증가

: 유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가

 

(3) 상속의 오용

: 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면 잘못된 사용으로 인한 문제 발생.

ex) 상속을 받는 클래스가 상위 클래스와 IS-A의 관계가 아닌 경우

 

* 이러한 문제를 해결하는 방법: 객체 조립(Composition)

: 객체 조립이란. '필드에서 다른 객체를 참조하는 방식'으로 구현. 상속에 비해 조립을 통한 재사용의 단점은 상대적으로 '런타임 구조가 복잡' / '상속보다 구현이 어려움'

: 하지만, 구현/구조의 복잡함보다 변경의 유연함을 확보하는 데서 오는 장점이 크기 때문에, 상속보다 조립하는 방법을 먼저 고려.

 

* 그렇다면 상속은 언제 사용?

: 상속을 사용할 때는 재사용이라는 관점이 아닌, 기능의 확장이라는 관점에서 상속 적용 & 추가로 명확한 IS-A 관계가 성립되어야 함.

댓글