본문 바로가기

1편. 객체지향 설계 5원칙[SOLID] Single Responsibility Principle (단일 책임 원칙)

by Recstasy 2020. 3. 26.

 문법공부를 끝낸 수준에서 '함수', '클래스' , '객체' , '배열' 등...을 사용해서 프로그램을 만들다보면 어느새 스파게티 코드가 탄생한다. 그 이유는 간단하다. 프로그래밍이 아닌 코딩을 하고 있기 때문이다. 


코딩 VS 프로그래밍



프로그래머는 코더일 수 있지만 코더는 프로그래머가 아니다.


"라면 끓이기"를 생각해보자. 라면이란 요리는 누구나 할 수 있다. 주변의 마트에서 구입한 라면 한 봉지와 물만 있으면 된다. 그런데 누군가가 '인스턴트 라면'을 끓여놓고, 자신을 요리사라고 불러달라고 한다. 이는 당연히 말도 안 되는 이야기다. 그러나 프로그래밍 세계에서는 이러한 일이 발생한다. 


라면 요리사는 밀가루 반죽부터 국물까지 모두 자신만의 방법으로 요리를 만든다. 라면 스프같은 프레임워크를 사용하지 않는다. 그렇다보니 오히려 인스턴트 라면보다 맛이 떨어지는 일도 종종 발생한다. 게다가 사람들 중에는 빠르고 평균 수준의 맛을 낼 수 있는 인스턴트 라면을 선호하는 경우도 있다. 그래서 라면 요리사가 되기로 작정한 사람들은 빠른 길을 선택한다. 어설프게 만들 바엔 그냥 인스턴트 라면을 사용하는 편이 낫기 때문이다. (프레임워크가 폭발적으로 증가하는 이유)


하지만 라면 전문점을 창업해야 한다면 상황은 변한다. 


프로그래밍 설계는 프랜차이즈 본사 창업(가맹점 아님)과 비슷하다. 프랜차이즈의 시작은 일단 효율적인 무언가를 설계하는 것에 있다. 본사는 가맹점과 달리 인스턴트 방식으로 시작할 수 없다. 이를 프로그래밍 세계로 비유하자면 코더 역시 "설계" 혹은 "추종" 중에서 선택을 내려야 한다. 어느 순간 프랜차이즈 창업주로 나아갈 지 혹은 가맹점 업주(프레임워크 의존) 머물러야 할지를 선택해야하는 것이다. 



SOLID

'SOLID'원칙은 추종자의 끝이자 설계자의 시작점이다.


"SOLID"

S : Single Responsibility Principle (단일 책임 원칙)

O : Open-Closed Principle  (개방 폐쇄 원칙)      

     L : Liskov Substitution Principle  (리스코프 치환 원칙)

          I : Interface Segregation Principle  (인터페이스 분리 원칙)

     D : Dependency Inversion Principle (의존성 역전 원칙)


특정 컴퓨터 프로그래밍 언어의 문법을 마스터했다면, 프로그래밍의 걸음마를 뗀 것이다. 개인적인 견해를 말하자면 이 정도 수준에서는 아직 SOLID를 접목하기보다 초급 코드를 많이 분석하고, 소규모 개별 모듈을 많이 제작해보는 편이 좋다고 생각한다. 아마도 이 글도 별 도움이 되지 않을 수 있다. 


SOLID원칙이 필요한 상황은 최소한 코더 이상의 수준이어야 한다. 필자 역시 초급 프로그래머의 수준을 벗어나지 못했기에 아래 기준은 지극히 주관적이며, 또 중,고급의 세계를 전부 알지 못한다. 다만 프레임워크의 달달한 맛에 취하여 못 벗어나는 코더분들을 위해 정리했다 


1. 입문자(10%) : 문법 이해 * 응용가능

1.5. 코더(15%) : 프레임워크 적용 가능

2. 아마추어(30%) : 함수형 or 객체지향 코딩


------------- 개별 프로젝트 개발 多 ----------

3. 초급 프로그래머(50%) : "책임-협력-역할" 주도 소프트웨어 설계(요구분석)


------------- 협업 개발 多 ----------

4. 중급 프로그래머(70%) : 알고리즘(디자인패턴 자유자재 구사), 자료구조, API설계


------------- 실무 개발 多 --------------

5. 고급 프로그래머(90%) : 프로그래밍 외적부분(조직관리), 아키텍처, 테스트주도 설계


흔히, '코더'를 나쁘게 생각하는 경향이 있는데, 이는 좋지 못한 선입견인 듯 하다. 코더 단계는 대부분 프로그래머가 거쳐가야 할 필수코스처럼 되고 있다. 문제는 코더의 위치에 계속 머물러 있는 자세다. 코더는 입문자와 아마추어 사이의 어중간한 위치에 있기 때문에 자신의 위치를 "프로그래머" 수준으로 착각할 가능성이 높다. 


과감하게 프로그래머의 세계로 나아가기 위해서는 초~중급 프로그래머의 첫 관문이라 할 수 있는 SOLID원칙부터 숙지해야 한다. 최근 각종 프레임워크의 폭발적인 성장 덕분에 코더들이 넘쳐나고 있는데 SOLID원칙만 잘 따르더라도 자신의 수준을 크게 향상시킬 수 있을 것이다.



Single Responsibility Principle || 

단일 책임 원칙 || 

식당카운터에서 요리하지 마라 ||


단일책임 원칙? 이에 관한 사전적인 설명은 다음과 같다.


클래스는 단일 책임을 맡는다


언뜻 쉽게 와닿지 않는 문장이다.

빠른 이해를 위해서 '맥도널드'를 떠올려보자.

 

계산대 앞. 햄버거 주문을 받은 담당자는 메뉴를 주방에 전달하고, 계산을 하고 있다. 이제 당신이 할일은 햄버거가 나올 때까지 기다리는 일이다.  잠시 뒤, 아니 이게 웬일인가? 카드로 계산하던 담당자가 주문대 아래에서 불고기 패티를 꺼내더니 소스를 뿌려댔다.  주문 담당자는 놀란 당신을 신경쓰지 않고 신나게 햄버거를 만들기 시작했다. 


맥도널드 같은 프랜차이즈는 철저하게 분업화되어 있기 때문에 각자 책임에 따라 움직인다. 주문 담당자는 고객의 주문을 받고, 계산하며, 요리는 주방담당자들의 책임이다. 그런데 위의 사례처럼 주문 담당자가 카운터에서 요리하고, 홀 관리 및 회계업무까지 한다면 어떻게 될까? 


- 테이블이 더럽다. 누구의 책임인가?

- 패티가 덜 익혀진 상태로 나왔다면 누구의 책임인가?

- 계산에 오류가 발생했다면 누구의 책임인가?

- 식자재가 제시간에 들어오지 않았다면 누구의 책임인가?


책임소재가 불분명하면 작은 오류에도 시스템 전체가 움직인다. 이는 비단 프랜차이즈 시스템 뿐만 아니라 프로그래밍에도 해당된다. 만일 하나의 클래스에 모든 동작을 집어넣다보면 시스템은 점차 비대해지고, 한 부분이 고장나면 전체가 무너진다. 특히, 하나의 클래스 내에서 메서드끼리 거미줄처럼 연결되어 있다면 더욱 끔찍한 상황이 발생한다. 가령, 아래와 같이 Worker라는 클래스에 각종 기능(메서드)을 집어넣는 상황을 생각해보자


class Worker{

   constructor(){

        this.food = null;

    }


    counter(){ }

    cook(){}

    design(){}

    cleaning(){}

    serve(){}

      ...

}


Worker클래스가 많은 기능을 담당함으로써 사실상 매장 전체의 책임을 맡고 있다. this.food에 문제가 발생했다고 생각해보자. this.food와 관련된 메서드는 cook(), serve()이다. 만일 어디를 고쳐야 할지 모르겠다면 2개의 메서드를 모두 뜯어봐야 한다. 그나마 위의 예시는 메서드가 2개이므로 충분히 수정할 수 있지만 수백 개의 메서드들이 서너 개의 클래스 내부에 엉켜있다면 개발자는 야근해야 할 수밖에 없다. 




주문담당, 조리담당, 매장관리, 고객관리 등... 각 클래스는 자신의 역할에 관한 것에만 책임을 져야한다. 가령, 위와같이 Worker클래스를 사용해야겠다면 매너저와 같은 상위클래스를 생성하고, 그 아래에 다음과 같은 클래스를 만들어 책임을 맡기는 식이다.  


class Counter{

    constructor(name=''){

   this.name = name;

   this.payment = [card, check, coupon]

    }

    orderCheck(){ }


class Cooker{

    constructor(name){

        this.name = name;

        this.orderMenu = [] 

    }

    cook(){ }

}


class Cleaner{

    constructor(name){

        this.name = name;

        this.checkList = [table, chair, ...]

    }

    clean(){  }

}



클래스의 책임범위

깔끔한 설계를 위해서는 어떠한 경우에도 클래스는 단일 책임을 져야 한다. 마치 철저한 분업으로 운영되는 프랜차이즈 매장처럼 하나의 클래스는 복수의 책임을 지지 않는다.


하지만 막상 객체 설계를 시작하면 클래스의 책임범위가 어디까지 둬야하는지 고민되는 경우가 많다. 가령, 주문관리 담당자와 고객관리 담당자의 책임 범위가 '고객'이란 접점에서 겹치는 것과 같은 경우다. 만일 책임의 범위가 애매하면 덩달아 기능도 모호해진다. 위의 코드에서 Counter클래스에 '테이블 청소'라는 cleanCounterTable() 기능을 추가해보자. 


카운터 테이블을 청소하는 일은 청소담당자일 수도 있지만 카운터를 관리하는 담당자의 역할이 될 수도 있다. 그렇다면 어떤 클래스에 책임을 씌워야 할까? 경험상, 이런 경우에는 메서드(기능)의 '실패상황'을 가정해보면 클래스의 책임 범위를 비교적 쉽게 파악할 수 있다. 기능이 실패했을 때, 특정 프로퍼티(속성)의 값에 영향이 미친다면 해당 클래스가 바로 책임자일 가능성이 높기 때문이다. 


왜 그럴까?


"책임"이란 단어가 빈번하게 사용되는 계약서를 생각해보자. 


우리는 중요한 일에 앞서 계약서를 작성한다. 일이 잘 풀리지 않을 때를 대비하기 위해서다. 만일 일이 술술 잘 풀린다면 아무도 책임질 필요가 없고, 계약서도 필요없다. 문제는 실패했을 경우다. 계약서는 실패한 상황에서 빛을 발한다. 바로 위약벌과 같은 조항이 있기 때문이다. 


위약벌은 계약 당사자가 주어진 책임을 지키지 않았을 때, 계약서에 명시된 사항을 이행해야만 하는 조약이다. 프로그램의 클래스들 역시 일종의 계약관계로 생각할 수 있다. 따라서 클래스의 기능이 실패했다면 계약이 어긋난 상황이며, 해당 클래스의 프로퍼티의 값이 비정상적으로 될 가능성이 높다.(일종의 위약벌)


가령, 위의 Cleaner클래스의 clean()메서드가 제대로 동작하지 않았을 경우, this.payment나 this.orderMenu는 별 타격이 없다. 가장 큰 피해(변화)를 입는 속성(프로퍼티)은 table, chair과 같은 속성이다. 따라서 테이블(카운터테이블 포함)을 청소하는 책임은 Counter클래스가 아니라 'table', 'chair'속성을 갖고 있는 Cleaner클래스가 갖는다. 즉, 카운터 테이블을 청소하는 메서드는 Cleaner클래스에 속해야 한다.


이와 같이 책임-범위를 설정할 때는, 실패 상황을 가정해보는 방법 유용하다. 언제까지나 '책임'이라는 단어는 '실패'에 관한 개념을 내포하고 있기 때문이다.


결론

'책임'범위에 관한 내용으로 잠시 설명이 옆길로 빠진 감이 있지만 결론은 '클래스당 단일 책임'이다. 이를 한 장면으로 정리하면 "식당 카운터에서 요리를 하지 말자"이다.

댓글

최신글 전체

이미지
제목
글쓴이
등록일