프로그래밍 공부
작성일
2024. 1. 30. 15:49
작성자
WDmil
728x90

5.5 추상화

추상화는 해당 클래스의 이용 방법, 즉, 인터페이스를 그 구현과 분리하는 것이다. 추상화는 앞서 살펴본 여러 가지 이유에서 바람직하다. 그리고 객체 지향 디자인의 기본적인 요소다.


5.5.1 인터페이스와 구현

추상화의 핵심은 구현에서 인터페이스를 분리해애는 데 있다. 구현은 목적하는 작업을 수행하도록 작성한 코드고, 인터페이스는 다른 사람이 그 코드를 이용할 떄 사용해야 하는 규약이다. C에서는 헤더 파일이 라이브러리에 포함된 함수 목록을 담고 있어서 인터페이스 역할을 했다. 

 

객체지향 프로그래밍에서는 클래스의 접근 가능한 프로퍼티와 행동이 인터페이스 역할을 한다. 좋은 인터페이스는 공개된(public) 행동만으로 이루어진다. 프로퍼티와 값은 그 자체로는 공개되지 않고 게터와 세터에 의해 공개된다.


5.5.2 오픈할 인터페이스 선택

내가 작성한 클래스 객체를 다른 프로그래머가 어떻게 이용하고 연계할 것인지 클래스를 디자인할 때 결정한다.

C++ 에서는 클래스의 프로퍼티와 행동이 public, protected, private 세 항목중 하나에 속하게 된다.

 

  • pulbic
    항목에 프로퍼티나 행동을 담으면 외부에서 접근할 수 있도록 공개된다.
  • protected
    항목에 담으면 외부에서는 접근할 수 없지만, 파생 클래스에서는 접근 가능해지고,
  • private
    항목에 담으면 외부는 물론 파생 클래스에서조차도 해당 프로퍼티와 행동에 접근할 수 없고 클래스 내부적으로만 이용할 수 있다.

인터페이스 디자인은 결국 public 항목에 어떤 것을 담을것 인가로 귀결된다. 여러 프로그래머가 협업하는 큰 프로젝트라면 인터페이스 디자인 단계를 정규적인 절차로 취급해야 한다.


5.5.2.1 사용자 관점 고려

외부 인터페이스를 디자인 할 때 첫 번째 단계는 그 인터페이스를 누가 이용할 것인지 생각해보는 것이다.

 

나 혼자 이용할 것인가?

같은 팀의 다른 동료가 이용할 것인가?

회사 밖의 다른 프로그래머인가?

 

어쩌면 계약 관계의 해외 고객일 수도 있다. 여기에 더하여 인터페이스의 사용법을 물어보기 위해 나를 찾아올 사람이 누가 될지 생각해보면 디자인이 무엇을 지향해야 하는지 힌트를 얻을 수 있다.

 

만약 나 혼자 이용하기 위한 인터페이스라면, 반복해서 수정해도 별 문제가 없을 것이다. 나 스스로가 고객이기 때문에 내가 원하는 대로 얼마든지 바꿀 수 있다. 하지만 조직에 속해있다면 언제든지 역할이 바뀔 수 있기 떄문에 내가 아닌 다른 사람이 이용할 가능성도 항상 염두에 두는 것이 바람직하다.

 

나 혼자 이용하는 경우가 아닌, 팀 내 다른 동료가 이용해야 한다면 상황이 달라진다. 인터페이스는 그 동료에게 약속한 일종의 계약서가 된다. 예를 들어 데이터 저장 컴포넌트를 구현했다면, 다른 개발자들은 그 컴포넌트에 의존해서 어떤 작업을 수행한다.  내가 개발할 컴포넌트를 팀 내 동료가 어떻게 이용할지 모두 알아낼 필요가 있다. 인터페이스에 버전을 매겨야 할까? 저장하는 데이터 타입은 어떤 것들이 있나? 인터페이스는 계약서의 성격을 가지기 때문에 내 마음대로 마구 변경할 수 없다. 만약 사전에 이야기 되었던 인터페이스와 다르게 중간에 인터페이스를 바꾸어버리면 동료 개발자가 짜증을 낼 수도 있다.

 

사용자가 외부 고객이라면 디자인 요건이 크게 달라진다. 이상적으로는 인터페이스의 각 기능을 결정할 떄 고객이 함께 참여하는 것이 바람직하다. 당장 고객이 요청하는 기능 뿐만 아니라 미래에 필요하게 될 기능까지 같이 고려해서 인터페이스를 디자인해야 한다. 그리고 인터페이스 디자인에 이용되는 용어들도 내가 익숙한 용어가 아닌 고객이 익숙한 용어를 사용해서 문서화해야 한다.

 

조직 내부에서 사용되는 약어나 은어를 사용해서는 안된다.


5.5.2.2 사용 목적에 대한 고려

인터페이스를 작성하는데는 여러 가지 이유가 있다. 외부에 노출할 기능을 결정하거나 코드를 문서화하기 전에 인터페이스의 목적부터 이해해야 한다.


애플리케이션 프로그래밍 인터페이스(Application Programming Interface{API])

API는 어떤 제품을 확장하거나 그 기능을 다른 맥락에서 이용하기 위한 목적으로 외부에 공개된 장치다. 내부 인터페이스가 계약 이라면 API는 변할 수 없는 헌법과도 같다. API사용자는 조직 내부든 외부든 기능 추가와 같은 특별한 이유 없이 API가 바뀌는 것을 원하지 않는다. 이때문에 API는 애초부터 신중하게 설계되어야 하고, 고객과 충분한 커뮤니케이션을 거친 후에 결과물이 전달되어야 한다.

 

API를 디자인할 때 가장 큰 딜레마는 사용성과 유연성의 절충점을 찾는것이다. 고객은 전달된 제품의 내부가 어떤식으로 작동하는지 잘 모르기 때문에 인터페이스를 배우는 데 시간이 걸릴 수 밖에 없다. 당연하지만 고객은 API를 이용하려고 제품을 구매한 것이기 때문에 사용법을 이기히가 너무 어렵다면, 즉 사용성이 떨어진다면 그 API디자인은 실패한 것이 된다/ 유연성은 종종 사용성과 배치되는 성격이 있다. 고객이 어떤 상황에서도 요긴하게 쓸 수 있도록 다양한 사용 방식을 지원하고 싶을 수 있다. 하지만 제품의 모든 긴으을 활용할 수 있게 하는데 몰입한 나머지 API가 너무 복잡해지는 것도 문제다.

 

좋은 API는 '쉬운 일은 쉽게, 복잡한 일도 가능하게' 라는 말로 요약된다. 학습 곡선이 길어지지 않도록 자주 활용되는 케이스 들은 최대한 쉽고 간단하게 처리할 수 있게 API를 디자인한다. 그렇다고 어렵고 복잡한 활용 케이슬르 배제해서는 안되고 방법은 어렵더라도 적용은 가능하도록 욜어둔다. 다시 말해, 드물게 발생하는 복잡한 상황은 사용성이 떨어지더라도 그 활용 루트를 열어두어 유연성을 확보하고, 자주 발생하는 일반적인 상황에 대해서는 매우 쉽고 간단하게 API를 구성한다.


유틸리티 클래스 또는 라이브러리

애플리케이션 안에서 일반적으로 사용될 어떤 기능을 만들어야 할 떄가 있다. 랜덤 넘버 생성 라이브러리나 로그 저장 클래스가 그러한 경우다. 이 때는 인터페이스 항목을 결정하기 쉽다. 거의 모든 기능을 외부에서 사용하기 떄문에 내부 구현을 너무 드러나지 않는 선에서 모두 노출하면 된다. 단, 일반적인 용도로 사용되는 코드는 사용 시나리오가 매우 다양해질 수 있다는 점을 고려해야 한다.


서브시스템 인터페이스

애플리케이션 안에서 주요 서브시스템 간에 인터페이스를 디자인해야 할 수도 있다. 데이터 베이스 접근 메커니즘이 그러한 예다. 이 때는 인터페이스를 구현으로부터 분리하는 것이 무엇보다 중요하다. 다른 프로그래머가 당신보다 먼저 모듈을 완성해보려서 다른 프로그래머가 구현해놓은 모듈에 맞추는 수고를 해야할 수도 있기 떄문이다. 개발할 서브시스템의 주요 기능을 확인했다면 상세한 구현 내용을 민하기 전에 그 서브시스템이 외부 모듈 입장에서 어떻게 보이고, 어떻게 사용될 것인지 먼저 생각해야 한다.


컴포넌트 인터페이스

보통 API나 서브시스템 보다는 훨씬 작은 규모의 인터페이스를 디자인할 일이 많다. 보통 자신이 작성한 다른 코드에서 사용할 객체들이 그러한 경우다. 이러한 내부 인터페이스는 점직적으로 수정되고 관리도 엄격하게 되지 못해서 제멋대로 되는 경향이 있다. 비록 나 혼자 사용할 인터페이스지만 다른 사람도 사용할 수 있다는 생각으로 디자인하는 것이 바람직하다. 서브시스템 인터페이스의 경우와 마찬가지로 각 클래스의 주목적이 무엇인지 고려하고 그 목적에 합치되지 않는 기능은 외부로 노출시키지 않는다.


5.5.2.3 미래에 대한 고려

인터페이스를 디자인할 때는 미래에 대한 부분도 고려해야 한다. 지근 만든 디자인을 수년동안 변경할 수 없는 상황이라면 어떻게 될까? 그런 경우라면 확장성을 고려하여 플러그인 아키텍처를 인터페이스에 적용하는 것이 좋을 수 있다. 인터페이스가 원래 의도와 다른 목적으로 오용될 가능성이 있다면 어떻게 해야 할까? 사용자들과 이야기를 나눠보고 사용 케이스들을 더 잘 이해할 수 있도록 한다. 불가키하다면 나중에 사용 케이스가 명확해졌을 때 다시 작성해야 할 수 도 있다.

 

최악은 그때그때 되는대로 기능을 추가하다 보니 인터페이스가 엉망이 되어 버리는 상황이다. 하지만 그렇다고 확장성을 크게 고려한 나머지 과하게 일반적인 인터페이스를 디자인해서는 안된다. 예를 들어 어떤 경우든 처음부터 끝까지 적용 가능한 로그 클래스를 만들려 해서는 안 된다. 향후 어떤 사용케이스가 있을지 불분명한 상황이라면, 당장 확실한 사용 케이스 부터 정리하는 것이 좋다. 그렇지 않으면 인터페이스가 불필요하게 복잡해지고 구현하기 어려워질 수 있다.


5.5.3 바람직한 추상화 디자인

좋은 추상화 디자인을 할 수 있으려면 경험을 통해 반복해보는 것이 최선이다.

 

실제로 잘 디자인된 인터페이스는 오랫동안 다른 추상화 코드를 이용해보고 또 직접 작성해보면서 나오게 된다. 다른 사람이 수년동안 작성한 것을 활용하는 방법도 있다. 아주 잘 디자인된 추상화는 이미 표준 디자인 패턴으로 존재하고 있다. 다른 추상화 코드를 이용할 떄마다 어떤 부분이 제대로 적용되고 어떤 부분이 쓸모 없었는지 기억하도록 노력하는 것이 좋다.

 

지난 주에 이용했던 윈도우 파일 시스템 API는 어떤 부분이 부족했었나? 동료가 작성한 네트워크 래퍼 코드를 내가 다시 작성한다면 어떻게 바꾸겠는가? 첫 버전의 인터페이스가 정답인 경우는 매우 드물다. 만약 회사에서 코드리뷰 절차를 수행하고 있다면 구현 부분에 앞서서 인터페이스 디자인 명세부터 리뷰받도록 한다. 비록 다른 프로그래머를 귀찮게 하더라도 추상화 디자인을 수정하길 주저해서는 안된다.

 

더 좋은 추상화가 장기적으로는 더 큰 이익이 될 수 있다.

 

어떤 때는 내가 만든 디자인을 다른 프로프래머에게 적극 전파하고 수용하도록 설득해야 할 수 도 있다. 팀 내에서는 기존 디자인에 전혀 문제를 못 느끼는 사람도 있을 수 있고 새로운 디자인 때문에 수정해야 할 부분이 부담이 될 수도 있다. 이러한 사오항에서는 당신의 디자인을 방어할 논리를 세우는것과 동시에 다른 사람의 의견을 들어보고 합당한 것은 수용하는 자세를 취한다.

 

잘 추상화된 인터페이스는 외부로 공개된 행동들만 가진다. 모든 코드는 구현 파일에 위치하고 클래스 정의 헤더 파일에는 위치하지 않는다. 이것은 구현 내용이 수정되더라도 인터페이스 정의 외에는 변경이 없음을 의미한다.

 

클래스 하나로 추상화를 할 떄는 주의가 필요하다. 만약 코드가 너무 많아지고 구조가 깊어진다면 클래스를 쪼개어 주 인터페이스 부분에 부가적인 인터페이스를 배치하는 것이 좋다. 예를 들어 어떤 데이터 처리 인터페이스를 만들었다면, 그 결과 데이터에 대한 부분을 별도의 인터페이스로 만들어서 결괏값을 조회하고 해석하기 편하게 할 수 있다.

 

그리고 가능하다면 프로퍼티를 행동으로 바꾸는 것이 좋다. 즉, 외부에서 직접 클래스 데이터를 조작하게 하는 것 보다 메서드를 경유하게 하는 것이다. 

 

예를 들어 물건의 크기를 나타내는 프로퍼티에 어떤 엉뚱한 프로그래머가 마이너스 값을 집어넣는 오류를 피하고 싶다면, 프로퍼티 변수를 직접 노출시키는 대신 setHeight()라는 행동을 정의해서 값을 설정하기 전에 올바른지 검사할 수 있다.

 

반복은 너무 중요하기 때문에 몇 번 강조해도 지나치지 않는다. 좋은 디자인을 찾고 피드백을 구하고, 수정하고, 실수로부터 배우는 것을 반복하라.

 

정리하자면, 좋은 추상화 디자인을 할 때 사용자와 목적 이라는 두 요소를 중요하게 고려해야 한다는 것 이다.

728x90