관리 메뉴

김종권의 iOS 앱 개발 알아가기

[Clean Architecture] 5. 객체 지향 프로그래밍 (Object-Oriented Programming), 의존성 역전 본문

Clean Architecture/Clean Architecture 기초

[Clean Architecture] 5. 객체 지향 프로그래밍 (Object-Oriented Programming), 의존성 역전

jake-kim 2021. 3. 12. 02:31

객체 지향 프로그래밍

  • 3가지 요소를 반드시 지원하는 프로그래밍 방법
  • 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism), 다형성을 이용한 의존성 역전(dependency inversion)
  • 사실생 캡슐화, 상속은 객체 지향 언어가 탄생하기 전부터 C에서도 존재
  • 객체 지향 프로그래밍이란 다형성을 이용하여 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력
    (모듈의 독립성, 배포 독립성)

캡슐화 (= 은닉화)

  • 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있게끔 하는 개념
  • 사용하는 쪽에서 멤버 변수들의 정보를 모르고 있어야 완전한 캡슐화
  • private, public으로 표현: 사용쪽에서 특정 구조체의 멤버에는 접근하지 못하지만 함수를 호출할 수 있는 형태
  • 만약 결합도가 커지는 상황이 된다면 (A가 B, C를 사용할 때, 만약 B와 C도 서로 참조하고 있다면 B나 C 중 하나만 걷어내고 싶어도 걷어내기가 힘듦 -> 교체하기 힘듦 -> 유지보수가 힘듦)

ex) c언어 에서의 완벽한 캡슐화(은닉화): point.h를 사용하는 측에서 strcut Point의 멤버에 접근할 수 없지만 makePoint(), distance()함수를 호출만 가능 (Point구조체의 데이터 구조와 함수가 어떻게 구현되어 있는지 모르는 상태)

// point.h

struct point;
struct point* makePoint(double x, double y);
double distance (struct Point* p1, strcut Point* p2);
// point.c

#include "point.h"
#include <stdlib.h>
#include <math.h>

struct Point {
    double x,y;
}

struct Point* makePoint(double x, double y) {
    struct Point* p = malloc(sizeof(struct Point));
    p->x = x;
    p->y = y;
    return p;
}

double distance(struct Point* p1, struct Point* p2) {
    double dx = p1->x - p2->x;
    double dy = p1->y - p2->y;
    return sqrt(dx*dx + dy*dy);
}
  • 객체 지향 언어가 완전한 캡슐화가 아닌 이유: 사용하는 쪽에서 멤버 변수들의 정보를 모르고 있어야 완전한 캡슐화이므로 아래는 완전한 캡슐화가 불가 -> 사용하는 쪽에서 x, y멤버에 접근할 수 있지만 사용자는 멤버변수가 존재한다는 것을 알고 있으므로
// 객체지향 언어 C++ 예시
// point.h

class Point {
public:
    Point(double x, double y);
    double distance(const Point& p) const;

private:
    double x;
    double y;
};

상속

  • 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 것
  • C언어에서도 상속을 구현할 수 있지만, 객체 지향 언어에서 조금 더 편리한 방식을 제공하는 것

ex) C언어에서 상속 구현: main에서 NamedPoint구조체가 마치 Point로 부터 파생된 형태 (두 변수 x, y의 순서가 Point와 동일하기 때문)

// namedPoint.h

struct NamedPoint;

struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamedPoint* np, char* name);
char* getName(struct NamedPoint* np);
// namedPoint.c

#include "namedPoint.h"
#include <stdlib.h>

struct NamedPoint {
    double x,y;
    char* name;
};

struct NamedPoint* makeNamedPoint(double x, double y, char* name) {
    struct NamedPoint* p = malloc(sizeof(struct NamedPoint));
    p->x = x;
    p->y = y;
    p->name = name;
    return p;
}

void setName ...

char* getName ...
// main.c

#include "point.h"
#include "namedPoint.h"
#include <stdio.h>

int main(int ac, char** av) {
    struct NamedPoint* origin = makeNamedPoint(0.0, 0.0, "origin");
    struct NamedPoint* upperRight = makeNamedPoint(1.0, 1.0, "upperRight");
    printf("distance = %f\n", distance((struct Point*) origin, (struct Point*) upperRight));
}

다형성

  • 아이디어는 프로그램의 '장치 독립적' (어떤 기기에서도 실행)을 위함
  • 같은 자료형에 여러 가지 객체를 대입하여 다양한 결과를 얻어내는 성질
  • 주의 - 상속이 아닌 다형성의 키워드: Overriding, Overloading, casting

다형성이 중요한 이유 - 의존성 역전

  • 객체 지향의 꽃은 '의존성 역전' 개념
  • 객체 지향은 소스코드의 의존성을 어디에서든 역전시킬 수 있는 것
  • 아이디어는 ''배포 독립성(independent deployability)': 특정 컴포넌트의 소스 코드가 변경되면, 해당 코드가 포함된 컴포넌트만 다시 배포
  • 모듈을 독립적으로 배포할 수 있게 되면 서로 다른 팀에서 각 모듈을 독립적으로 개발하는 것이 가능

* 의존성 역전이 없는 경우: 비즈니스 로직은 UI와 DataBase에 의존하고 있기 때문에, UI나 DataBase가 바뀌면 BusinessLogic을 변경해주어야 하며, UI와 DB둘 중 하나만 바뀌어도 영향이 가는 구조

의존성 역전을 안한 상태

* 의존성 역전이 된 경우: BusinessLogic에서의 소스코드에서는 UI나 DB를 호출하지 않으며 UI나 DB와는 무관하게 BusinessLogic을 독립적으로 배포 가능

의존성 역전

  • 객체 지향 언어에서만 의존성 역전이 가능한 이유?: 

* 객체 지향 언어가 아닌 경우, 의존성 역전이 불가능: 제어흐름(의존성, 함수 호출 순서)가 모듈의 단계별로 필요한 수준에 맞게 일방적으로만 가능

객체 지향이 아닌 경우 main부터 모듈 호출 흐름 (= 제어 흐름)

* 객체 지향 언어인 경우:
HL1모듈은 I인터페이스를 참조하고 있지만 오버라이딩 된 ML1의 F()사용

-> 객체 지향이 아니라면 HL1은 I를 가리키고, I는 ML1을 가리키는 형태이지만 overriding에 의하여 의존성 역전

-> 소스 코드의 의존성(상속관계)가 제어 흐름(함수 호출)과는 반대
-> 만약 I도 ML1에 의존하고 있으면, ML1이 바뀔 때 I도 같이 바꾸어야 하는 상황 존재(아래 상황은 ML1에 수정사항이 생기면 ML2, ML3로 구현하여 기존 코드에 주입해서만 사용하면 해결)

의존성 역전된 경우

swift에서의 의존성 역전 적용 방법

  • Interface개념을 가지고 있는 protocol 사용
  • 사용하는 HL1모듈에서는 protocol의 함수를 호출 -> protocol을 구현한 ML1 모듈이 호출 -> ML1이 바뀌어도 쉽게 다른 모듈로 갈아치우기 쉬움
  • 테스트 코드에 이점: protocol을 참조하고 있는 HL1 입장에서 F()에 대한 테스트 코드를 만들고 싶을 때, ML1_test로 새로 만들어 해당 구현체만 바꾸어서 주입해주면 테스트가 용이한 이점 존재
  • 주의할 점은 HL1입장에서 protocol로 구현된 모듈을 참조하고 있어야하며, 그 모듈의 구현체는 바꾸기 쉽도록 '주입'하는 형태가 되어야 함

* 출처: Clean Architecture

Comments