우아한 테크코스/테크코스

[프로그래밍 패러다임] 객체 지향 프로그래밍

jamie. 2020. 3. 14. 15:31
반응형

객체의 핵심 - 기능 제공

 객체(Object) : 객체 지향의 기본

 - 물리적인 특징 : 데이터와 그 데이터를 조작하는 프로시저(오퍼레이션, 메서드, 함수)로 구성

 - 실제로 객체를 정의할 때 사용되는 것 : 객체가 제공해야 할 기능, 내부적으로 어떤 데이터를 갖고있는지는 대상이 아님

 

 예시) 음량 조절 기능 객체

 - 제공하는 기능 : 음량 증가, 음량 감소, 음소거

 - 내부적으로 소리 크기를 어떤 데이터 타입이나 값으로 보관하고 가지고 있는지는 중요하지 않음

 - 또한 기능이 어떻게 구현되어있는지, 즉 어떻게 소리가 증감되는지는 알 수 없음

 

 보통 객체가 제공하는 기능을 오퍼레이션(operation)이라 함, 즉 객체는 오퍼레이션으로 정의

인터페이스와 클래스

 객체가 제공하는 기능을 사용하는 것 : 객체의 오퍼레이션을 사용한다는 의미

 사용하려면, 오퍼레이션의 사용법을 알아야 함

 

 오퍼레이션의 사용법은 일반적으로 아래 세 개로 구성되며, 세 가지를 합쳐 시그니처(Signature)라고 부름

- 기능 식별 이름

- 파라미터 및 파라미터 타입

- 기능 실행 결과 값

인터페이스

 객체의 인터페이스(interface) : 객체가 제공하는 모든 오퍼레이션 집합

 타입(type) : 서로 다른 인터페이스를 구분할 때 사용되는 명칭

- 여기서 말하는 인터페이스는 자바 등의 인터페이스가 아닌, 객체 지향에서 오퍼레이션 집합을 표현할 때 사용되는 용어. 인터페이스는 객체를 사용하기 위한 일종의 명세나 규칙이라고 생각하면 됨

- 자바 등의 언어는 인터페이스와 클래스가 각 언어에서 클래스로 부르는 것으로 함께 정의됨. 이런 언어들은 개념상의 인터페이스나 클래스를 더 충실하게 반영할 수 있도록 구현을 포함하지 않는 인터페이스를 따로 제공. 하지만 객체 간의 인터페이스를 맞춘다는 의미 = 개념적인 의미의 인터페이스임을 유의

 

 객체가 제공하는 기능에 대한 명세서일 뿐, 실제 객체가 기능을 어떻게 구현하는지에 대한 내용은 포함하고 있지 않음

클래스

 실제 객체의 구현을 정의하는 것

 오퍼레이션을 구현하는데 필요한 데이터 및 오퍼레이션의 구현이 포함

 - 자바 등의 언어에서 말하는 클래스와 유사

객체 - 인터페이스 - 클래스 간의 관계

 인터페이스(음량 조절 타입) : 오퍼레이션명, 파라미터, 결과 정의

                  클래스를 이용하여 인터페이스 구현 제공

 클래스 : 구현된 클래스

               ↓ 클래스를 이용하여 메모리에 객체 생성, 메모리에 생성된 객체를 인스턴스라고 부름

 객체(소리 크기 제어 객체)

                ↓ 음량 조절 타입에 정의된 기능을 제공

인터페이스(음량 조절 타입) ... (순환)

메시지

 메시지(message) : 오퍼레이션의 실행을 요청(request)하는 것을 메시지를 보낸다고 함

 

 예시) 파일을 꾸며서 보여주는 객체에서, 파일을 읽어와 꾸며서 보여준 후, 꾸민 것을 파일 쓰기 객체에 저장해줌

 - 파일 읽기 객체가 제공하는 인터페이스의 오퍼레이션

  > 오퍼레이션명 : read / 파라미터 : 없음 / 리턴 타입 : byte배열

객체는 다른 객체에 요청(메시지)을 보냄

 자바에서는 메서드를 호출하는 것이 메시지를 보내는 과정에 해당

객체의 책임과 크기

 객체 : 객체가 제공하는 기능으로 정의 = 객체마다 자신만의 책임(responsibility)이 있다는 의미 = 객체가 역할(role)을 수행

 타입/인터페이스 : 한 객체가 갖는 책임을 정의한 것

책임의 예

 객체 지향 설계의 출발점 - 객체가 갖는 책임을 결정하는 것

- 책임 할당은 처음부터 바로 결정되는 것이 아님

- 1) 프로그램을 만들기 위해 필요한 기능 목록을 정리(머리로든, 테스트 코드 작성이든, 기능목록 나열이든 완벽하지 않더라도 정리가 필요)

- 2) 정리한 기능 목록으로 객체에 어떻게 할당할지 고민, SRP(단일 책임 원칙)을 유념하며 할당

 

 상황에 따라 객체가 가져야할 기능의 종류와 개수가 달라지기 때문에, 모든 상황에 들어맞는 객체-책임 구성 규칙이 존재하는 것은 아님

 객체가 얼마나 많은 기능을 제공할 것인가에 대한 확실한 규칙 : 객체가 갖는 책임의 크기는 작을수록 좋다 = 객체가 제공하는 기능의 개수가 적을수록 좋다.

 

 객체가 제공하는 기능의 개수가 적을수록 좋은 이유

- 한 객체에 많은 기능이 포함되면, 그 기능에 관련된 데이터들도 한 객체에 모두 포함됨 = 데이터를 중심으로 개발되는 절차 지향 방식과 동일한 구조가 됨, 객체가 갖는 책임이 커질수록 절차 지향적으로 구조가 변질되며, 가장 큰 단점인 기능 변경의 어려움 문제(= 경직성 문제)가 발생

- 책임의 크기가 작아질수록 객체지향의 장점인 변경의 유연함을 얻을 수 있음

- 단일 책임 원칙(Single Responsibility Principle; SRP) : 객체는 단 한 개의 책임만을 가져야 한다는 원칙, 해당 원칙을 따르면 자연스럽게 기능의 세부 내용이 변경될 때, 변경해야할 부분이 한 곳으로 집중됨. 즉 변경의 유연함을 얻기 위한 가장 기본 원칙

의존(dependency)

 한 객체가 다른 객체를 생성하거나, 다른 객체의 메서드를 호출할 때, 그 객체에 의존한다고 표현.(이용하는 것)

 파라미터로 객체를 전달받는 경우에도 의존한다고 볼 수 있음 = 파라미터로 전달받은 객체를 메서드 구현 과정에서 사용할 가능성이 높기 때문

 

 의존한다는 것 : 의존하는 타입에 변경이 발생할 때 나도 함께 변경될 가능성이 높다는 것

 의존은 상호간에 영향을 줌

- 내가 변경되면 나에게 의존하고 있는 코드에 영향

- 나의 요구가 변경되면 내가 의존하고 있는 타입에 영향

 

 의존의 영향은 꼬리에 꼬리를 문 것처럼 전파되는 특징이 있음

 C 클래스가 B클래스에 의존하고, B클래스가 A클래스에 의존하는 경우, A클래스의 변경은 B클래스에 영향(변경)을 줄 가능성이 높고, 이는 다시 C클래스에 영향을 주게 됨

 만약에 A클래스가 다시 C클래스를 의존한다면,(순환 의존) C클래스에 변화시 A클래스도 영향이 올 수 있음. 이런 경우 자신이 변경한 여파가 자신에게 또 다른 변화를 유발할 수 있다는 것을 뜻함.

 순환 의존 발생시 적극적으로 해소해야 함

 순환 의존이 발생하지 않도록 하는 원칙 중 하나 : 의존 역전 원칙(Dependency inversion priciple;DIP)

캡슐화(encapsulation)

 객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것, 이를 통해 내부의 기능 구현이 변경되더라도 그 기능을 사용하는 코드는 영향을 받지 않도록 만들어 줌. 즉, 내부 구현 변경의 유연함을 주는 기법. 객체지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화 함

절차 지향 코드 - 요구사항이 바뀌어서, 데이터 구조가 변경되었을 때

 데이터를 직접적으로 사용하는 코드는 데이터의 변화에 직접적인 영향을 받기 떄문에, 요구 사항의 변화로 인해 데이터의 구조나 쓰임새가 변경될 경우, 이를 이용하는 코드들도 연쇄적으로 수정

 수정을 놓칠 경우 버그로 직결되고, 특정 버그는 오랜 잠복기를 거쳐 발견되기도 해서, 이 경우 코드를 수정한 시기와 버그가 발견된 시기에 차이가 발생해서 버그의 원인을 빠르게 찾기 힘들게 됨

캡슐화한 객체 지향 코드 - 요구사항이 바뀌어서, 데이터 구조가 변경되었을 때

 구현을 캡슐화하면 내부 구현이 변경되더라도, 기능을 사용하는 곳의 영향을 최소화할 수 있음, 즉 캡슐화를 통해 내부 기능 구현 변경의 유연함을 얻을 수 있음

캡슐화를 위한 두 개의 규칙 1. Tell, Don't Ask

 데이터를 물어보지 않고, 기능을 실행해달라고 말하는 것

 데이터를 읽는 것은 데이터를 중심으로 코드를 작성하게 만드는 원인, 따라서 절차지향적인 코드를 유도하게 됨

// user.getCards() : 카드를 가져옴
if (user.getCards() !== null &&
    user.getCards().getScore() > 90) {
    // 90점 이상일 때의 처리   
}

 기능 실행을 요청하는 방식으로 코드를 작성하면, 해당 기능을 어떻게 구현했는지 여부가 감춰짐. 즉, 기능 구현이 캡슐화 됨

if (users.isMoreThanNinety()) {
    // 90점 이상일떄의 처리
}

캡슐화를 위한 두 개의 규칙 2. 디미터의 법칙(Law of Demedter) 

 Tell, Don't Ask 규칙을 따를 수 있도록 만들어주는 또 다른 규칙

- 메서드에서 생성한 객체의 메서드만 호출

- 파라미터로 받은 객체의 메서드만 호출

- 필드로 참조하는 객체의 메서드만 호출

 예시) 디미터의 법칙을 위반, 파라미터로 전달받은 user의 getCard()를 호출한 후, 다신 getCard()가 리턴한 Card 객체의 getScore() 객체를 호출했기 때문

public String getPrize(User user) {
    // user.getCards() : 카드를 가져옴
    if (user.getCards() !== null &&
        user.getCards().getScore() > 90) { // 디미터의 법칙 위반
        // 90점 이상일 때의 처리   
    }
}

  디미터의 법칙을 지키려면 user.getCards().getScore()가 아닌 user.someMethod()로 변경해야 함

객체 지향 설계 과정

  1. 제공해야 할 기능을 찾거나 세분화하고, 그 기능을 알맞은 객체에 할당

     A. 기능을 구현하는데 필요한 데이터를 객체에 추가, 객체에 데이터를 먼저 추가하고 그 데이터를 이용하는 기능을 넣을 수도 있음

     B. 기능은 최대한 캡슐화하여 구현

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

 3. 과정1과 과정2를 개발하는 동안 지속적으로 반복

 

 기능을 찾으면 기능을 제공할 객체 후보를 찾고, 각 객체가 어떻게 연결되는지 그려보는 과정에서 객체가 기능을 제공할 때 사용할 인터페이스가 도출

 객체의 크기는 한 번에 완성되기 보다는 구현을 진행하는 과정에서 점진적으로 명확해짐, 구현 과정에서 한 클래스에 여러 책임이 섞여있다는 것을 알게 되면, 객체를 새로 만들어서 책임을 분리

반응형