본문 바로가기

[OOP] 절차 지향과 객체 지향 (객체 지향과 디자인 패턴)

 

 

들어가며

스타트업의 개발자로서 좋은 설계가 우선인가, 아니면 빠른 기능 개발이 우선인가 항상 고민하곤 했습니다. 제가 다녔던 스타트업은 빠르게 기능 개발을 해서, 시장에서 인정받아야 했기에, 빠른 시간 안에 많은 기능을 개발해야 했습니다. 그러다 문득 추가 기능을 개발하거나, 기존 기능을 수정해야 할 때, 많은 어려움을 겪곤 했습니다. 그렇게 일을 마치고, 더 나은 개발자가 되기 위한 공부를 하면서 제가 작성한 코드는 배려가 부족한 코드였다는 것을 깨달았습니다.

 

제가 개발한 코드는 당장의 기능 개발은 빠르게 할 수 있더라도, 재사용성이 대단히 떨어졌고, 가독성 또한 대단히 떨어졌습니다. 또한 기능을 수정하거나 추가할 때도 광범위한 코드를 건드려야만 했기에 효율성도 떨어졌습니다. 빠르게 기능을 개발하기 위해 적었던 코드가 언젠가는 발목을 잡을 수 있는 코드가 될 수 있다는 것을 깨달았습니다. 그렇다면 비즈니스에 도움이 되고, 다른 팀원들에게도 도움이 될 수 있는, 배려가 담긴 코드를 작성하기 위해서는 어떻게 해야 할까 고민하기 시작했습니다. 배려가 담긴 코드를 작성하기 위해 공부한 내용에 대해 작성해보고자 합니다.

 

 

 

 

 


 

 

 

 

 

 

 

출처: 인프런 '객체 지향 프로그래밍 입문'

 

절차 지향

이전 글에서 if, else if가 상당히 많았던 코드가 있었는데, 이렇게 작성된 것을 절차 지향적 코드라고 부릅니다. 그렇다면 절차 지향은 무엇이고, 절차 지향을 했을 때 어떤 문제점이 있는지 살펴보겠습니다.

 

소프트웨어를 구현한다는 것은 결국 소프트웨어를 구성하는 데이터와 데이터를 조작하는 코드를 작성하는 것입니다. 데이터를 조작하는 코드를 별도로 분리해서 함수나 프로시저와 같은 형태로 만들고, 각 프로시저들이 데이터를 조작하는 방식으로 코드를 작성할 수 있을 것입니다. 이때 프로시저는 다른 프로시저를 활용하고, 프로시저는 여러 개의 데이터를 조작할 수 있습니다. 이렇게 프로시저로 프로그램을 구성하는 기법을 절차 지향 프로그래밍이라고 합니다.  

 

 

 

 

 

 

이전 글에서 본 것처럼, 절차 지향적으로 코드를 작성했을 때, 처음에는 코드를 쉽게 작성할 수 있지만 시간이 흐를수록 구조가 더욱 복잡해지고, 코드를 유연하게 수정하기 힘든 문제가 있습니다. 한 곳을 수정하면 다른 곳에서 문제가 발생하고, 다시 그곳을 수정하면 또 다른 부분에서 문제가 발생하는 문제가 발생할 수 있습니다. 그럼 이 문제를 해결하기 위해서는 객체 지향적으로 코드를 작성하는 것이 필요합니다. 

 

 

 

 

 

 

 

 


 

 

 

 

 

출처: 인프런 '객체 지향 프로그래밍 입문'

 

 

객체 지향

절차 지향은 프로시저로 프로그램을 구성하는 기법이었다면, 객체 지향은 데이터와 프로시저를 객체로 묶어서, 객체들을 모아 프로그램을 구성하는 기법을 말합니다. 

 

객체는 자신만의 기능을 제공하며, 각 객체들은 서로 연결되어 다른 객체가 제공하는 기능을 사용할 수 있게 됩니다. 객체는 다른 객체에 기능을 제공하기 위해 프로시저를 이용하는데, 이때 프로시저는 자신이 속한 객체의 데이터에만 접근할 수 있으며, 다른 객체에 속한 데이터에는 접근할 수 없습니다. 

 

이런 객체 지향은 절차 지향과 달리 프로그램의 규모가 작을 때에는 절차 지향 방식보다 복잡한 구조를 갖게 됩니다. 하지만 객체 지향은 객체의 데이터를 변경하더라도 데이터가 포함된 객체만 변화되고, 다른 객체는 영향을 주지 않는다는 특징이 있어서, 코드를 보다 유연하게 변경할 수 있습니다. 

 

그럼 데이터와 프로시저를 객체로 묶는다고 표현했는데, 여기서 객체란 무엇일까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 

 


 

 

 

 

 

 

 

출처 : https://m.blog.naver.com/songintae92/221374000102

 

 

객체(Object)

객체를 정의할 때 사용되는 것은 객체가 제공해야 할 기능이며, 객체가 내부적으로 어떤 데이터를 갖고 있는 지로는 정의되지 않습니다. 예를 들어 소리 크기 제어 객체가 있다고 할 때, 이 객체는 소리 크기를 제어하는 기능을 제공하며, 객체가 제공하는 기능은 다음과 같을 것입니다. 

 

  • 소리 크기 증가
  • 소리 크기 감소
  • 음 소거

 

이 객체가 내부적으로 소리 크기를 어떤 데이터 타입 값을 보관하는지는 중요하지 않습니다. 또한, 실제로 객체가 어떻게 소리 크기를 증가시키거나 감소시키는지는 알 수 없습니다. 단지, 소리 크기 제어 객체는 '소리 크기 증가', '소리 크기 감소', '음 소거'라는 세 개의 기능을 제공한다는 것이 중요합니다. 이때 세 개의 기능을 어떻게 사용하는지를 알아야 기능을 사용할 수 있습니다. 예를 들어 소리 크기 제어 객체의 소리 크기 증가 기능을 사용하려면, 사용법을 알아야 하는데, 이때 사용법은 일반적으로 다음과 같이 세 개를 알아야 합니다. 

 

 

  • 기능 식별 이름
  • 파라미터 및 파라미터 타입
  • 기능 실행 결과 값

 

객체가 제공하는 모든 기능의 집합을 객체의 '인터페이스(interface)'라고 부르며, 서로 다른 인터페이스를 구분할 때 사용되는 명칭이 바로 타입(type)입니다. 여기서 말하는 인터페이스는 언어 안에 포함되어 있는 인터페이스가 아니라, 객체 지향에서 기능 집합을 표현할 때 사용되는 용어입니다. 인터페이스는 객체가 제공하는 기능에 대한 명세서입니다. 명세서는 실제 객체가 무엇이 있는지 알 순 있지만 어떻게 구현됐는지의 내용은 포함하지 않는데, 실제 객체의 구현을 정의하는 것은 클래스에서 담당합니다. 그럼 여러 객체가 모여있을 때, 객체 간의 소통은 어떻게 할 수 있을까요? 이에 대해 알아보겠습니다. 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

출처 : https://m.blog.naver.com/songintae92/221374000102

 

메시지

객체 간의 소통을 해야 한다면, 객체는 다른 객체에게 요청(메시지)을 보냄으로써 소통을 할 수 있습니다. 예를 들어 파일에서 데이터를 읽는 객체가 있고, 데이터를 암호화해주는 객체가 있으며, 파일에 데이터를 쓰는 객체가 있다고 하겠습니다. 이 경우 위의 그림처럼 객체 간의 관계를 표현할 수 있습니다.

 

파일 읽기 객체가 제공하는 인터페이스가 다음과 같은 기능을 제공한다고 하겠습니다. 

 

  • 기능 이름 : read
  • 파라미터 : 없음
  • 리턴 타입 : Array 배열

 

이 경우, 암호화 처리 객체는 파일 읽기 객체에게 read 기능을 실행해달라는 요청을 전달하며, 요청을 받은 파일 읽기 객체는 해당 요청에 해당하는 기능을 실행한 뒤에 응답을 전달하게 됩니다. 이때 기능의 실행을 요청하는 것을 '메시지를 보낸다.'라고 표현합니다.

 

이런 특성이 있는 객체는 자신만의 책임이 있습니다. 예를 들어 파일 읽기 객체는 파일에서 데이터를 읽어와 제공하는 책임이 있고, 암호화 처리 객체는 제공받은 데이터를 암호화해서 다른 파일에 보내는 책임이 있으며, 파일 쓰기 객체는 파일에 데이터를 쓰는 책임이 있습니다. 즉, 한 객체가 갖는 책임을 정의한 것이 바로 타입/인터페이스라고 생각하면 됩니다. 그럼 객체가 갖는 책임은 어떻게 결정될까요? 이 결정을 하는 것이 바로 객체 지향 설계의 출발점입니다. 그럼 객체의 책임에 대해 알아보겠습니다.

 

 

 

 

 

 

 

 


 

 

 

 

 

 

출처 : https://m.blog.naver.com/songintae92/221374000102

 

 

 

객체의 책임과 크기

다음은 소프트웨어를 구성하기 위해 필요한 기능 목록을 정리한 예입니다.

 

  • 파일의 byte 데이터를 제공한다.
  • 파일에 byte 데이터를 쓴다.
  • byte 데이터를 암호화해서 새로운 byte 데이터를 생성한다.
  • 전체 흐름을 제어한다.

 

이 기능을 어떻게 객체에게 분배하느냐에 따라 객체의 구성이 달라집니다. 상황에 따라 객체가 가져야 할 기능의 종류와 개수가 달라지기 때문에, 모든 상황에 들어맞는 객체-책임 구성 규칙이 존재하는 것은 아닙니다. 하지만, 객체가 얼마나 많은 기능을 제공할 것인가에 대한 확실한 규칙이 하나 존재하는데, 그 규칙은 바로 객체가 갖는 책임의 크기는 작을수록 좋다는 것입니다. 객체가 갖는 책임이 작아야 한다는 것은 객체가 제공하는 기능의 개수가 적다는 걸 의미합니다. 따라서 객체가 갖는 책임의 크기는 작아질수록 객체 지향의 장점인 변경의 유연함을 얻을 수 있게 됩니다. 이 과정에서 단일 책임 원칙(SRP)에 대해 생각해볼 수 있습니다. 단일 책임 원칙은 말 그대로 객체는 단 한 개의 책임만을 가져야 한다는 원칙입니다. 이 원칙에 따르면 위의 그림처럼 네 개의 객체가 각각 한 개의 책임만을 갖도록 구성할 수 있습니다.  

 

 

단일 책임 원칙을 따르다 보면 자연스럽게 기능의 세부 내용이 변경될 때, 변경해야 할 부분이 한 곳으로 집중됩니다. 지금까지 객체의 책임과 크기에 대해 알아봤다면, 이번에는 객체 지향에서 중요한 '의존'에 대해 알아보겠습니다. 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

출처 : https://m.blog.naver.com/songintae92/221374000102

 

의존

객체 지향적으로 프로그램을 구현하다 보면, 다른 객체가 제공하는 기능을 이용해서 자신의 기능을 완성하는 객체가 출현하게 됩니다. 위 그림처럼, '흐름 제어' 객체는 'Byte 암호화' 객체와 '파일 읽기' 객체, '파일 쓰기' 객체를 이용해서 파일 데이터 암호화 프로그램의 실행 흐름 기능을 완성했습니다.

 

한 객체가 다른 객체를 이용한다는 것은, 실제 구현에서는 한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출한다는 것을 뜻합니다. '흐름 제어' 객체의 실제 코드는 다음과 같이 다른 객체의 메서드를 호출해서 기능을 완성해 나갈 것입니다.  

 

 

class FlowController {
  /* 
    생략
  */
  public process(): void {
    const reader = new FileDataReader(fileName); // 객체 생성
    const plainBytes = reader.read(); // 메서드 호출

    const encryptor = new ByteEncryptor(); // 객체 생성
    const encryptedBytes = encryptor.encrypt(plainBytes); // 메서드 호출

    const writer = new FileDataWriter(); // 객체 생성
    writer.write(encryptedBytes); // 메서드 호출
  }
}

 

 

이렇게 한 객체가 다른 객체를 생성하거나 다른 객체의 메서드를 호출할 때, 이를 그 객체에 의존(dependency)한다고 표현합니다. 위 코드에서는 FlowController가 FileDataReader에 의존한다고 표현할 수 있습니다. 객체를 생성하거나 메서드를 호출하는 것뿐만 아니라 파라미터로 전달받는 경우에도 의존한다고 볼 수 있습니다. 

 

예를 들어, 아래 코드와 같이 ByteEncryptor를 파라미터로 받으면, 메서드 구현 과정에서 파라미터로 전달받은 ByteEncryptor 객체를 사용할 가능성이 높기 때문에, 이는 process() 메서드가 ByteEncryptor에 의존한다고 볼 수 있습니다. 

 

 

public process(encryptor: ByteEncryptor) {
   ... // 내부에서 encryptor를 사용할 가능성이 높다.
}

 

 

객체를 생성하든, 메서드를 호출하든, 또는 파라미터로 전달 받든 다른 타입에 의존을 한다는 것은 의존하는 타입에 변경이 발생할 때 나도 함께 변경될 가능성이 높다는 것을 뜻합니다. 예를 들어 FileDataWriter의 생성자가 String 타입으로 파일 경로를 받도록 수정된다면, FlowController 클래스의 코드에서 FileDataWriter를 생성하는 코드도 알맞게 수정해 줘야 합니다. 

 

 

 

 

 

 

 

 

의존의 영향은 꼬리에 꼬리를 문 것처럼 전파되는 특징을 갖습니다. 예를 들어 위와 같이 C 클래스가 B 클래스에 의존하고, B 클래스가 A 클래스에 의존한다고 하면, 이 경우, A 클래스의 변경은 B 클래스에 영향을 줄 가능성이 높고, 이는 다시 C 클래스에 영향을 주게 됩니다. 심지어 C 클래스의 변화가 A 클래스에 영향을 줄 수 있는, 순환 의존이 발생할 경우에는 적극적으로 이를 해소하는 방법을 찾아야 합니다. 순환 의존이 발생하지 않도록 하는 원칙 중의 하나로 의존 역전 원칙(DIP)이 있는데, 이는 추후에 알아보겠습니다.

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

마치며

객체지향 프로그래밍의 필요성을 점점 느끼고 있습니다. 언젠가, 코드에 대한 역할과 책임을 명확하게 보여줄 수 있는 코드를 작성할 수 있다면 동료들이, 일을 더 수월하게 할 수 있지 않을까 생각합니다. 배려가 담긴 코드를 작성하기 위해 꾸준하게 노력하고 싶습니다. 

 

 

 

 

 

 

 


 

 

 

 

 

 

출처

 

객체 지향과 디자인 패턴 - YES24

객체 지향 안내서. 객체, 책임, 의존, 캡슐화 등 객체 지향의 주요 개념들을 쉬운 예제와 그림을 통해 이해하기 쉽게 설명한다.

www.yes24.com

 

객체 지향 프로그래밍 입문 - 인프런 | 강의

잘 하는 개발자가 되기 위해서는 유연한 코드를 작성할 줄 알아야합니다. 객체 지향을 이용해서 변경하기 좋은 유연한 코드를 만드는 방법을 알아보세요., - 강의 소개 | 인프런...

www.inflearn.com

 

객체 지향

이번 포스트에서는 객체지향에 대해 포스트를 진행하겠다.절차지향과 객체 지향객체 지향을 잘 하려면 이와...

blog.naver.com