IT/기초 지식

오브젝트 지향의 삼대요소(계승(상속), 캡슐화, 다형성)를 활용한 구현 예시

개발자 두더지 2023. 4. 30. 21:12
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 

이 포스트에서 설명할 것


이 포스트에서는 서적 관리 어플리케이션을 만드는 것을 예제로 오브젝트 지향의 계승(상속)/캡슐화/다형성에 대해서 설명하고자한다. 서적 관리 어플리케이션은 서적의 대출, 반환이 되며, 대출시에 Mail을 송신하는 기능으로 한정하고 있다. 

 오브젝트 지향에 관련된 설명을 목적으로 하고 있기 때문에, 서적 관리 어플리케이션의 상세한 코드는 작성하지 않았다. 백엔드와 관련된 내용이다. 그러므로, 혼자서 독학 등으로 Web 어플을 개발 경험이 있는 사람이 이 포스트의 대상이 된다.

 코드는 PHP로 작성했지만, 다른 언어를 대입해서 봐도 무리 없이 이해할 수 있는 내용이라고 생각된다. 

 

 

어플리케이션의 개요


 이번 설명에서 사용할 서적 관리 어플의 요건은 다음과 같다.

  • 책의 대출이 가능할 것
  • 책의 반납이 가능할 것
  • 대출이 완료되면 메일을 보낼 것

 이 세 가지 뿐이다. 되도록 심플하게 했다.

 아래는 관리 서적 어플의 클래스도를 나타낸다.

 처리의 흐름을 간단히 설명하자면 다음과 같다.

1. 유저로부터 리퀘스트를 받아 BookController 클래스가 호출된다.

2. BookController 클래스는 리퀘스트에 대해서 UseCase 클래스를 호출한다.

3. Usecase 클래스는 model인 Book을 리퀘스트에 맞게 조작한다.

 3 후에는 메일을 보내거나 DB에 저장하는 등의 처리가 있지만 설명을 위해 생략했다.

 여기에서는 클래스 그림의 화살표 의미에 대해서 설명하도록 하겠다(이 화살표는 굉장히 중요하다).

 

실선의 화살표

 BookController에서 BorrowUsecase를 연결하는 실선의 화살표는 의존을 나타낸다. BookController이 BorrowUsecase를 호출하고 있다. 멤버 변수로써 가져지거나, 메소드내에서 인스턴스화하여 사용하거나 하는 경우에 이 화살표를 사용하게 된다.

 

실선 + 삼각형이 흰색인 화살표

 BookController에서 BaseController를 연결하는 실선의 흰 삼각형은 범화를 나타낸다. BaseController과 부모가 되는 BookController를 작성한다. 코드에는 아래와 같은 extends를 사용하는 예이다.

class BookController extends BaseController

 

점선 + 삼각형이 흰색인 화살표

 MailSender에서 ISender을 연결하는 점선의 흰 삼각형의 화살표는 실현을 나타낸다. ISender에 정의되어 있는 함수를 MailSender가 구체화하여 구현한다. 코드로 본다면 아래와 같은 implements이다.

class MailSender implements ISender

 

 

계승(상속)


 계승(상속)이란 이미 정의된 클래스를 상속받아 새로운 클래스를 정의하는 것이다. 계승(상속)은 프로그램의 재이용을 목적으로 사용하는 경우가 많다.

 이번에는 그림의 빨간색 박스 부분을 사용하고 있다.

 MVC나 클린 아키텍처에서 등록하는 Controller클래스는 대규모 시스템에서는 많이 만들어진다. 많이 만들어진 Controller의 안에는 공통화할 수 있는 것을 BaseController로 만들어 두고, 상속처에서 사용하게 된다.

 예를 들어, Controller의 응답은 JSON형식등과 같이 어떠한 형태로 결정된 데이터로 정형화할 생각인 경우 그 응답 형태 처리를 BaseController에 정의하면 된다.

 아래가 그 예이다.

// BaseController.php

class BaseController{
    private function sendResponse(array $responseData){
        // 응답 송신 처리
        ・・・
    }

    private function sendError(int $statusCode, string $errorMessage){
        // 에러 응답 송신 처리
        ・・・
    }
}
// BookController.php

class BookController extends BaseController{ // BaseController를 계승(상속)
    public function bollow(array $requestData){
        // 어떠한 처리
        ・・・
        try {
            $result = $borrowUsecase->handle($inputData);
        } catch(Exception e) {
            return $this->sendError(500, $e->getMessage()); //상속받고 있으므로 부모의 sendError메소드가 사용된다.
        }
        // 대출 실행 결과를 sendResponse로 보낸다.
        return $this->sendResponse($result); // 상속받고 있으므로 부모의 sendResponse메소드가 사용된다.
    }
}

 이렇게 하는 것으로 보수가 쉬워지고 일관성이 향상된다. BaseController가 없는 경우, 여러 개의 Controller에서 데이터의 형태 처리를 작성하게 되어, 만약 요건 변경이 된 경우에 모든 Controller를 확인하여 수정할 필요가 있다.

 또한, 개발 멤버가 많은 경우, 각각 좋아하는 데이터로 정형화하여, 응답 형식이 Controller에 따라 달라지게 되어버릴 수 있다.

 Controller에 한정하지 않고 동일한 역할의 클래스를 여러 개 작성하는 경우, 상속을 사용할 수 있지 않은지를 고려해보자.

 

 

캡슐화


 캡슐화는 오브젝트 멤버를 보호하는 것으로 오브젝트의 안전성을 높인다. 캡슐화는 데이터의 정합성을 담보를 위한 꽤 중요한 요소이다.

 이번에는 아래 그림의 빨간 박스 부분으로 설명하고자 한다.

 캡슐호의 기본은 클래스의 필드(멤버 변수)는 private, 외부에서 메소드를 사용하여 멤버 변수를 조작하는 것이다. 이 내용을 지키면 꽤 안전한 코드가 된다.

 서적 관리 어플리케이션의 데이터 정합성의 핵심은 Book 클래스이다.

// Book.php

class Book
{
    // 필드(멤버 변수)는 private으로 한다.
    private int $id;
    private string $title;
    private boolean $isAvailable;
    private DateTime $dueDate;
    public function __construct(int $id, string $title, boolean $isAvailable, DateTime $dueDate)
    {
        // 인스턴스를 만들 때에는 반드시 올바른 데이터가 들어있는지를 확인하게 되므로 안전성을 향상된다.
        if($id <= 0){
            throw new Exception('id는 1문자이상 입력해주세요.');
        }
        $this->id = $id;
        if(mb_strlen( $title, "UTF-8" ) > 255){
            throw new Exception('title는 255문자 이하로 입력해주세요.');
        }
        $this->title = $title;
        $this->isAvailable = $isAvailable;
        $this->dueDate = $dueDate;
    }
    // 외부에서 사용하는 메소드는 public으로 한다.
    // 내부 메소드에서 멤버 변수의 값을 조작한다.
    public function borrow(DateTime $dueDate)
    {
        if(!$this->isAvailable) {
            throw new Exception('대출중입니다.');
        }
        if($dueDate < new DateTime()) {
            throw new Exception('반환 기한은 오늘 이후의 날짜로 해주세요.');
        }
        $this->isAvailable = false;
        $this->dudate = $dudate;
    }
    public function return() // return은 예약후로 되어 있을 가능성이 높지만 읽기 쉬운 점을 중시해서 명명하고 있다.
    {
        if($this->isAvailable) {
            throw new Exception('반환됐습니다.');
        }
        $this->isAvailable = true;
        $this->dudate = null;
    }
}

 책을 빌리는 것은 먼저 정확한 책의 데이터로 인스터화해야한다. construct로 인스턴스화하는 값의 검증을 하므로, 버그로 연결될 것 같은 값이 혼재되지 않게 된다. 

 인스턴스화해도 멤버 변수가 private이므로, 외부에서 무리하게 변경을 할 수 없다. 정해진 메소드로만 데이터의 조작이 가능하므로 데이터의 정합성을 유지할 수 있다.

 이 Book 클래스의 사용방법은 다음과 같다.

// BorrowUsecase.php

class BorrowUsecase
{
    public function handle(array $inputData)
    {
        // DB에서 책 데이터를 획득
        ・・・
        // 획득한 데이터에서 book인스턴스를 생성
        $book = new Book($id, $title, $isAvailable, $dueDate);
        // 대출 처리
        $book->borrow($inputData['dueDate']);
        // DB에 저장 처리
        ・・・
    }
}

 인스턴스화하여 메소드를 호출하여 데이터를 조작하는 흐림이 된다.

 개발을 하는 도중에 데이터를 조작하게 된다고 생각한다. 캡슐화되어 있지 않으면 여러 곳에서 데이터의 정합성을 유지하기 위한 코드를 작성하게 되므로 데이터에 대한 지식이 점차 점차 다른 클래스로 유출된다.

 혹시 요건이 변경된 경우 모든 지식을 검증하여 변경할 필요가 있다. 그러므로 리스크가 높아진다. 맨 처음에 직접 데이터를 변경하고 싶어질 것 같지만, 조금 참고 클래스의 멤버 변수는 private 외부에서 메소드를 사용하여 멤버 변수를 조작하는 것을 의식하여 구현하길 바란다.

 

 

다형성 (폴리모피즘)


 다형성은 오버라이드나 오버로드에 의해 하나의 메소드 상황에 따라 동작하는 것이다. 공부할 때, 다형성을 가장 이해하기 힘들었다.

 다양한 용도가 있지만, 맨 처음은 외부 서비스를 연결하는 부분에 사용하는 이미지를 가지고 있으면 좋다. 이번 설명은 아래의 그림 빨간 박스에 대한 것이다.

 여기에서는 도서를 빌린 후에 완료 메일을 송신하는 것을 고려하고 있다. 다형성을 생각하지 않고 구현한다면 다음과 같이 된다.

// MailSender.php

class MailSender
{
    public function send(string $message)
    {
        // Mail의 송신처리
        ・・・
    }
}
// BorrowUsecase.php

class BorrowUsecase
{
    private MailSender $mailSender;
    public function __construct(MailSender $mailSender)
    {
        $this->mailSender = $mailSender
    }
    public function handle(array $inputData)
    {
        // 대출 처리
        ・・・
        // mail로 완료 통지
        $mailSender->send($message);
    }
}
// BookController.php

class BookController
{
    private MailSender $mailSender;
    public function bollow(array $requestData){
        // 어떠한 처리
        ・・・
        $bollowUsecase = new BorrowUsecase($mailSender);
        $bollowUsecase.handle($inputData);
    }
}

 Usecase에서 MailSender을 가지고, 이 send 메소드를 호출하여 메일을 송신한다. 다음은 위 코드를 다형성을 고려한 형태로 수정한 것이다.

// ISender.php

// interface를 준비한다.
interface ISender
{
    public function send(string $message);
}
// MailSender.php

// ISender의 구현한다.
- class MailSender
+ class MailSender implements ISender
{
    public function send(string $message)
    {
        // Mail의 송신처리
        ・・・
    }
}
// BorrowUsecase.php

class BorrowUsecase
{
    // UseCase에서는 Interface를 가진다.
-   private MailSender $mailSender;
-   public function __construct(MailSender $mailSender)
+   private ISender $sender;
+   public function __construct(ISender $sender)
    {
-       $this->mailSender = $mailSender
+       $this->sender = $sender
    }
    public function handle(array $inputData)
    {
        // 대출처리
        ・・・
        // 어떠한 송신으로 완료 통지(이 Usecase는 어디에서 송신하는지는 모른다.)
-       $mailSender->send($message);
+       $sender->send($message);
    }
}
// BookController.php

class BookController
{
    private MailSender $mailSender;
    public function bollow(array $requestData){
        // 어떠한 처리
        ・・・
        // Controller쪽에서 ISender의 구현인 MailSender를 전달해준다.
        $bollowUsecase = new BorrowUsecase($mailSender);
        $bollowUsecase.handle($inputData);
    }
}

ISender의 구현으로써 MailSender를 작성한다. MailSender에 메일 송신의 구체적인 처리를 기재한다. Usecase에서는 어떠한 송신처리를 하는 것은 알지만, 구체적으로 어떤 것을 사용해서 송신하는가에 대한 상세한 내용은 모른다. Controller에서 MailSender을 전달해야 알 수 있다.

 이렇게 바꿔쓰는 것에 대한 이점이 확 와닿지 않을지도 모르겠지만, 요건이 변경되었을 때를 생각해보자.메일로 보낸 것을 Slack으로 보내도록 요건이 변경된 경우는 아래와 같은 SlackSender을 생성한다.

// SlackSender.php

class SlackSender implements ISender
{
    public function send(string $message)
    {
        // Slack으로 송신처리
    }
}

ISender의 구현으로 SlackSender에 구체적인 처리를 기재한다. 기존 코드의 수정은 아래와 같은 BookController만이 된다. Usecase의 수정은 불필요하게 된다.

// BookController.php

class BookController
{
-   private MailSender $mailSender;
+   private SlackSender $slackSender;
    public function bollow(array $requestData){
        // 어떠한 처리
        ・・・
-       $bollowUsecase = new BorrowUsecase($mailSender);
+       $bollowUsecase = new BorrowUsecase($slackSender);
        $bollowUsecase.handle($inputData);
    }
}

 다형성을 사용하는 것으로 외부 서비스의 요건 변경이 있어도 변경하는 부분은 적어진다. 또한 외부 서비스의 바꾸는 것도 간단하므로, 유닛 테스트 구현도 쉬워진다.

 예를 들어, 다형성을 고려하지 않고 BorrowUsecase의 handle을 테스트하는 경우는 MailSender를 사용하고 있으므로, 테스트에서도 메일 송신을 할 수 있게 되어버린다.

 그러나, 다형성을 고려한 유닛 테스트의 경우 MockSender을 사용하여 송신 처리를 다른 형태로 바꾸는 것이 간단하게 될 수 있다.

// MockSender.php

class MockSender implements ISender
{
    public function send(string $message)
    {
        // 로컬 파일에 쓰기등의 테스트를 하기 쉬운 처리
    }
}
// Unit테스트시에 혹은 HogeUsecase의 인스턴스 작성시에MockSender를 전달한다.
$bollowUsecase = new BorrowUsecase($mockSender);

참고자료

https://qiita.com/takuch/items/4ac6b8cc59de774335ff

728x90