객체지향생활체조 원칙
Last updated
Last updated
NEXTSTEP에서 진행하는 클린코드 수업을 들은 것이 NEXTSTEP을 알게 된 계기였다. 지금도 많이 부족하지만 그 수업을 들으면서 많이 성장할 수 있었고, 이걸 듣지 않았으면 계속해서 퀄리티가 떨어지는 코드를 작성했겠다는 생각을 수업을 듣는 그 당시에도 많이 했었다.
그만큼 수업, 피드백 자체가 양질이었는데 강사진이나 리뷰어분들이 잘해서도 있겠지만 그 근간에 에서 제시하고 있는 객체지향생활체조원칙 이 자리하고 있기 때문이라는 생각이 든다.
이 단어 자체는 리뷰어분들이 여기서 나오는 원칙에 근거해서 리뷰를 해주시는 과정에서 알게 되었는데, 체조? 지향생활? 과 같은 단어 자체가 너무 이상했고 메소드 하나 만드는 것도 원칙을 지키려니 너무 시간이 오래걸리는 것 같았고 왜 그런지 원칙을 이해하지 못한체 마냥 이걸 따라야하니 너무 답답했다.
하지만 그렇게 불평(?)을 품은채로 미션을 진행해보니 어느새 완성된 코드 자체가 그 원칙을 지키면서 작성을 하다보니 단위 테스트를 하기에도 무척 효율적이게 되었고, 가독성도 좋아 보였다.
결국 포비님 말씀대로 이건 각각의 원칙에 대한 당위성을 머리로 이해하는 것 보다 마치 운동을 하듯이 아래 원칙들을 생활화 하다보면 클린코드의 의미를 이해하게 되고 아래 원칙들의 당위성을 알게 되는 것 같다.
아래는 각 원칙별로 정리한 내용이다.
한 메서드에 오직 한 단계의 들여쓰기만 한다.
else 예약어를 쓰지 않는다.
모든 원시 값과 문자열을 포장한다.
한 줄에 점을 하나만 찍는다.
줄여 쓰지 않는다(축약 금지).
모든 엔티티를 작게 유지한다.
3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
일급 컬렉션을 쓴다.
getter/setter/프로퍼티를 쓰지 않는다.
위 원칙들은 클린코드를 위한 정량적인 지침들이며 각 원칙은 곧 원칙이 지켜지는 전제로 코드가 작성될 경우 최대한 클린코드에 가까워진다는 전제를 갖고 있다. 하지만 그저 원칙만으로 달달 외워서 이를 따르면서 코드를 작성하기 보다는 근본적으로 왜 이 원칙대로 코드를 작성하면 클린코드에 가까워지는지 세부적으로 이해하는 것이 학습 가성비 측면에서 더 좋다고 판단되어서 어렴풋했던 이유들을 세세하게 구체화해보는 시간을 가져보았다.
기본적으로 모든 원칙들이 지향하는 바는 클린코드이며 클린코드가 지향하는 바는 근복적인 '좋은 품질'이라는 덕목을 추구한다. 여기서 말하는 '좋은 품질'이란 응집력, 느슨한 결합, 무중복, 캡슐화, 테스트가능성, 가독성을 의미한다.
이 지침의 대전제는 '하나의 메서드는 하나의 기능을 수행해야한다'는 것이다. 왜냐햐면 하나의 메서드가 둘 이상의 기능을 할 경우 재사용성도 떨어지고 모듈화가 그만큼 떨어진다는 의미이므로 테스트도 힘들어지는 등 여러가지 안좋은 경우가 발생한다.
다르게 말하면 하나의 메서드가 둘 이상의 기능을 수행한다는 것은 곧 메서드의 응집력이 떨어진다고 표현할 수 있다. 그러므로 하나의 메서드는 하나의 기능을 수행해야하는데, 한 메서드에 둘 이상의 들여쓰기가 있다는 것은 중첩된 제어구조가 존재할 가능성이 매우 크다는 뜻이며 이는 곧 하나의 메서드에 둘 이상의 기능이 추상화되어 코드로 작성되었을 가능성이 크다는 것을 의미한다.
for문 안에 for문이 있거나 for문 내에 if 문이 있는 등 특정 연산 내에 또 다른 연산이 들어가게되면 이는 두 단계 이상의 들여쓰기이다.
객체지향언어의 장점중 하나인 다형성을 사용하기 위한 Strategy 패턴에서 else를 쓸 경우 코드의 의도를 읽기 쉽지 않고 다형성 활용성을 저해시킨다는 의미에서 else 사용을 원천적으로 금지하는 것이다.
다르게 말해서 다형성을 활용하는 코드에서 if-else를 사용할 경우 실제로 재사용성과 가독성을 떨어트리기 때문에 OOP의 정수를 제대로 활용하지 못한다는 논리인 것이다.
사실 나도 else를 피하려다보니 보호절과 조기종결을 너무 자주 쓰게 되는 감이 있었고 항상 '이게 과연 더 최선인가'하는 의문이 있긴 했지만 습관화가 되니 오히려 보호절과 조기종결에 의한 처리가 편해지긴 했다.
정리하자면 OOP의 장점인 다형성을 활용하기 위해서는 원천적으로 else를 사용하면 안되고, 이를 위해서 미리 다른 경우에서도 else를 쓰지 않도록 습관화하기 위한 원칙이 바로 'else 예약어를 쓰지 않는다.' 인 것이다.
모든 원시 값과 문자열을 Constant 로 빼서 쓰라는 의미이다. 이 원칙의 효용은 실무에서 매우 크게 느꼈다. 이 원칙의 대 전제는 '원시형 변수 자체로는 프로그래머에게 그 값이 어떤 값이며 왜 쓰이고 있는지에 대한 정보를 전달할 방법이 없다' 이다. 부가적으로 만약 이 특정한 값이 다른 곳에서도 재사용 된다면, 이것이 만약 변경 되었을 때 상수로 빼서 쓰지 않을 경우 변경 지점이 너무 많아진다. 즉, 유지보수가 더 어려워지는 것이다.
예를 들어 사용자가 하루에 먹은 모든 끼니를 가져와서 각 끼니의 칼로리가 500을 넘을 경우 뭔가 처리를 해야 한다고 해보자.(요즘 다이어트중이라 예제가 다이어트에 관해서만 떠오른다) 이 때 원시값을 그대로 사용할 경우 아래와 같은 코드가 나올 수 있다.
이걸 만약 만든사람이 아닌 다른 사람이 본다면(혹은 만든 사람이 오랜 시간이 지나서 기억이 휘발된 채로 다시 본다면) 여기서 사용된 500이 권장칼로리인지 하루 제한 칼로리인지 정책적으로(기획상) 의미가 무엇인지 알길이 하나도 없다. 아래와 같이 코드를 바꿔보자.
만약 위와 같이 작성이 되어 있다면? 바로 이 하루 제한 칼로리가 500인지는 알 수 없게 되지만, 코드상에서 이미 의도가 전달되고 있다. 그리고 이 값이 여러군데에서 쓰인다면 똑같은 곳에서 끌어다 쓸 수 있고 그만큼 유지보수도 쉬워진다.
반대로 원소값 자체가 의미 전달이 가능하다면 굳이 포장해서 쓰지 않아도 된다. 가령, 원소의 첫번째를 가리키는 0을 쓰는 상황에서 누구든 프로그래머라면 배열 내 인덱스를 가리키는 지점에 0이 있는 것을 보면 이것의 의미가 첫번째 원소를 가리키는 것을 아는데 여기서 이걸 포장한다면 되려 혼란을 가중시키는 것이다.
한 줄에 둘 이상의 점이 있다는 것이 곧 둘 이상의 오브젝트를 동시에 다루고 있다는 의미이며 이것 자체가 코드의 복잡도를 올리고, 잘못이 발생 했을 때 책임 소지를 파악하기 힘들어지기 때문에 한 줄에 점을 하나만 찍으라는 원칙이다.
스트림과 같은 주어진, 내가 바꿀 수 없는 것에서는 체이닝을 쓰되 라인만 구분해주고 그 외에 내가 만드는 class에서는 이 원칙을 지켜주는 것이 타협안이다.
축약을 지양하게 하는 이유는 축약된 것을 보고 누구나 같은 해석을 하지 않기 때문에 혼란이 커지기 때문이다. 이 원칙 역시 뚜렷한 논리가 있긴 하지만 실무에서 동료에게 피드백을 하기에는 다소 '단순한 개인 스타일 차이'에 가타부타 이야기를 얹는 느낌이 들어서 실무에서도 좀 애매한 것 같긴 하다.
말하기 편한 환경이거나 컨벤션을 처음부터 같이 잡을 수 있는 환경이라면 꼭 축약을 하지 않는 방향으로 협의를 하는게 좋은 것 같다.
다만 이 부분에 대해서 '프로그래머의 뇌' 책에 명확하게 실험기반으로 제시된 자료가 있는데, 핵심은 축약 하지 않은 코드에서 버그가 덜 나오고 생산성이 더 좋았다.
책에서는 50줄 이상 되는 클래스와 10개 파일 이상 되는 패키지를 만들지 말라고 말하고 있다. 당연히 코드의 절대량이 작으면 한 번에 파악하기가 쉽고 간결한 것은 맞는데 문제는 하나의 클래스에 꼭 같이 있어야 할 기능들이 붙어 있기 위해서는 5줄을 넘기기 쉬운데 이런 경우 패키지를 적극 활용하라고 책에서는 권하고 있다.
클래스가 점점 작아지면서 하나의 패키지 내에 기능들을 최대한 분산하고, 그러면서 동시에 패키지 자체의 응집력을 올리라는 것이다. 일단 내가 인식하고 있던 패키지는 '동일 부류의 묶음' 기능으로만 생각했는데, 패키지의 응집력을 키운다는 생각을 못해봤어서 새로운 view라서 좋았다. 그리고 패키지 역시 응집력있고 작게 유지함으로써 패키지 자체로서의 정체성을 갖게 해야한다고 한다.
클래스 내에서 3개 이상의 지역 변수를 갖지 않도록 하는 원칙이다. 이유는 지역 변수가 많을 수록 응집력이 떨어지기 때문이다. 이러한 원칙하에서 클래스를 작성하게 되면 결국 클래스의 종류는 두 가지만 나오게 된다.
지역변수 1개만을 가지며 이 하나의 상태를 유지하는 것
두 개의 독립된 변수를 조율하는 것
1번은 말 그대로 어떠한 목적으로 만든 클래스의 상태값 하나를 의미하는 것이고 2번은 상태값의 변화보다는 정보성 클래스를 의미하는 것으로 보인다. 책에 나와있는 예시가 괜찮아서 옮겨보자면 아래와 같다.
이걸 7번 원칙에 맞게 쪼개본다면 아래와 같아진다.
훨씬 더 의미가 분명해지고 책임과 역할이 분배되어서 모듈화가 더 잘 되었다.
콜렉션을 포함한 클래스는 반드시 다른 멤버변수가 없어야 한다는 규칙이다. 위 7번 규칙에서 GivenNames가 바로 일급콜렉션 이라고 볼 수 있다. 일급콜렉션이란 Collection을 Wrapping 하면서 해당 Collection 외에 다른 지역변수가 없는 클래스를 일급콜렉션 이라고 한다.
일급콜렉션을 쓰면 코드가 간결해지고 역할과 책임이 더 모듈화 될 수 있는 장점이 있다. 지역변수로 있는 콜렉션에 대해서 어떠한 처리를 해줘야할 때 이 지역변수를 가진 클래스가 그걸 처리하기 위해서 기능을 하나 갖기 보다는 애초에 그 지역변수가 차라리 클래스화(Wrapping)되어서 온전히 그 클래스의 책임으로 분산해주면 더 객체지향적인 코드가 되기 때문이다.
스택오버플로우에 실제로 이 있었다. 이 글에서 다형성에 if-else가 얼마나 해가 되는지를 알 수 있다.
하지만 이건 좀 이상하다. 왜냐면 체이닝 자체를 부정하는 것인데 체이닝이 주는 이점들이 많기 때문이다. 그래서 외국의 포스팅을 찾아봤더니 아니나 다를까 조금 더 을 발견했다.
그리고 또 일급콜렉션의 유일한 그 지역변수 하나를 final로 선언해주어서 재할당을 막는다면 데이터의 불변성까지 보장해줄 수 있다. 더 자세한건 을 참고하자.
getter, setter는 성격상 캡슐화를 방해하는 동작의 성격을 지니고 있기 때문에 쓰지 않도록 하는 원칙이다. 여기에 관해서는 예전에 내가 피드백을 받을때 리뷰어님께 직접 질문을하고 을 받았던 내용이 있다.