객체지향 체조
내가 부트캠프를 하던 시절
과제로 코드를 작성하는데 몇 가지 조건을 제한해둔 과제를 받은 적이 있다.
제한해둔 조건은 대략적으로 아래와 같았다. (기억이라 틀릴 수 있음)
1. 절대로 인덴트 depth 가 3 이상이 되지 않도록 하시오.
2. 중복코드를 최대한 없도록 하시오.
3. 하나의 함수가 80줄을 넘지 않도록 하시오.
그 당시에는 "이것이 가능한가?" 라는 물음이 있었다.
근데 또 하다보니 어찌어찌 되긴됬다.
근데 이 중 for 문 부분에서 3뎁스에 도달해서 떨어질꺼란 생각을 했던 기억이 난다.
여튼 그 당시에는 잘 몰랐지만, 이 내용이 객체지향 체조에 들어있던 내용이였다.
그래서 오늘은 객체지향 체조를 정리해보려고 한다.
객체지향 체조
1. 메소드 당 indent depth 는 하나로 제한한다.
LeetCode 의 대표문제인 TwoSum 을 O(n^2) 복잡도로 푼 해결방법이다. (일부로 이런거니.. 댓글은 노노)
이제 저 이중포문을 한번 살펴보자.
일단 중첩 포문을 사용한 이유는 처음 값을 고정시키고 두번째 값을 더하기 위해 사용했다.
그럼 이 코드를 한번 인덴트를 1로 줄이는 리팩토링을 해보자.
이제 메소드당 인덴트를 1개로 줄였다.
Arguments 가 너무 많은건 일단은 신경쓰지마라.
우리가 원칙에 의해 코딩하다보면 이쁜 코드로 변경될 것이다.
일단 1개로 줄이고 나니 이중포문 보다는 좀더 명확해졌다.
첫번째로 첫번째 인덱스와 두번째 인덱스의 합이 타겟이 되는 값을 알아낸다는 의미를 파악할 수 있다.
두번째 인덱스와 첫번째 인덱스를 고정하고 합이 Target 인 인덱스를 저장한다.
일단 이 단계에서는 인덴트를 1로 제한한다는 리팩토링만 진행하겠다.
다른 것들도 보이긴 하지만, 그건 다른 단계에서 진행하겠다.
2. ELSE 키워드를 사용하지마라
이건 코드리뷰를 받아봤다면 꽤 많이 등장하는 말 중 하나이다.
코드를 보는게 더 빠르므로 일단 아래 코드를 한번 보자.
일단 else 가 위험한 이유는 여러가지 인데 그 중 하나는
정확한 condition 파악에 어려움이다.
실무 코드를 작성하다가 else 에 마주하게 되면 도대체 어떤 컨디션일때 뱉는거지? 라는 의문을 가지게 된다.
실무에서는 위처럼 간단한 조건을 가진 코드 보다 더 복잡한 코드가 훨씬 많다
일단, 위와 같은 경우에는 사용할때는 else 를 쓸 이유가 전혀 없다.
일단 else 를 제거해주자.
쓸모 없는 else 를 제거했다.
전통적으로 프로그래밍을 안전하게 하기 위해서는 예외를 Throw 하는 쪽을 조건문 안에 넣는 것이 좋다.
그 이유는 현재 코드는 컨디션에 걸리지 않는다면 무조건 예외를 Throw 하는데
이는 정말 위험한 프로그래밍 방법이다.
누군가의 실수가 곧바로 예외로 이어질 수 있다는 것이다.
일단 If 문안에 그렇다면 예외를 넣어보자.
이제 좀 안정적으로 변했다.
이제는 Password 가 validator 가 되지않는다면 예외를 호출한다.
즉, 예외를 호출하는 시점이 조금 더 명확해 졌다.
위와 같은 방식을 Defensive Programming 이라고 한다.
3. 모든 원시값을 객체형태로 랩핑해라
음, 이건 자신이 맞게 판단하는게 좋다고 생각한다.
나는 무조건 모든 원시값을 객체형태로 랩핑해라 라는 말에는 동의할 수 없다..
분명 좋은 상황이 있고, 나쁜 상황이 있다.
본인이 잘판단해서 랩핑여부를 파악하자.
일단 원시값을 랩핑하게 되면 어떤 점이 좋을지 파악해보자.
그래야 어느상황에 좋을지를 판단할 수 있을 것 이다.
위 처럼 계좌 클래스를 구성했다.
위의 코드를 봤을때 일단 money 는 양수여야만 한다.
즉 int 라는 타입을 쓰게 되면 계속해서 음수가 되지 않는 다는걸 확인해야만 한다.
이걸 Money 라는 객체로 Wrapping 해보자.
일단 Money 객체를 Wrapping 해서 생성자를 사용할때 money 에 무조건 양수값을 넣어야만 작동한다.
이제 이걸 Account 에 int type 대신 money type 으로 변경해보자.
위와 같이 변경되었다.
이제 Account 의 대부분 로직에서 음수값에 대한 걱정을 하지 않아도 된다.
Account 메소드들은 조금 더 자신이 책임져야 하는 부분에 신경쓸 수 있게 되었다.
이처럼 원시타입은 우리 현실세상의 돈을 표현하기는 힘들다.
왜냐 음수가 될 수 있으므로.
4. 일급 콜렉션 클래스를 사용해라
이건 우리가 콜렉션 리스트를 이용해서 안의 Element 를 다루고, 특정 동작을 취하려면
동작과 관련된 클래스를 만들라는 것이다.
일단 일급이라는건 진짜 쉽게 말해서 이 자체로 값으로 평가받을 수 있느냐이다.
쉽게 얘기하면 함수형 프로그래밍은 함수를 인자로 넘길 수 있다.
그래서 함수는 "일급 객체" 처럼 취급받는다.
다소 이상하게 생각할수도 있는데 쉽게 설명하기 위해 이렇게 적을 수 밖에 없었다.
더 자세하게 공부하려면 직접 찾아보기 바란다.
일단 백문이 불여일견 코드로 한번 작성해보자.
위와 같이 1급 콜렉션의 정의 대로 필드를 콜렉션 원소밖에 가지고 있지 않는다.
외부에서는 Lego List 를 받는 것이 아니라, LegoBox 라는 1급 콜렉션 객체를 주입 받는다.
LegoBox 는 Lego 를 List 형태의 자료구조에 넣으며 관리하기 위해 생성된 1급 콜렉션 객체이다.
따라서 외부에서는 Lego 가 어떤 자료구조에서 어떻게 관리되고 있는지를 알 필요가 없다.
5. 한줄당 하나의 메소드를 호출해라
본문의 영어는 "One Dot Per Line" 인데, 내가 저렇게 의역한 것이다.
그러니 틀린거일 수도 있으니 잘 판단하길 바란다.
"Method Chaining Pattern" 을 적용한 QueryBuilder 와 같은 모델에는 적용되지 않으니 유의해서 읽길 바란다.
일단 아래코드를 보자.
append 라인을 보면 method chaining 으로 인해 끔찍한 사태가 발생했다.
이는 개발자에게 몇가지 문제를 선사한다.
예를 들면, 저렇게 Board 에서 location 에 대한 정보를 너무 많이 알게 되면
Location 이 변경되면 Board 의 코드도 상당히 높은 확률로 수정될 확률이 크다.
즉, Open Close Principle 을 위반하고 있다는 뜻이다.
이러한 사태가 일어나게 된 계기는 여러가지 이유가 있는데 한번 같이 살펴보자.
일단 Location 클래스는 "디미터의 법칙" 을 위반하고 있다.
그래서 외부에서 Location 에 Piece 가 있다는 정보를 불가피하게 알아야 한다.
이 점이 매우 중요한데, 이건 나중에 따로 아티클로 설명하려고 한다.
그리고 Piece 또한 아래와 같이 디미터의 법칙을 위반하고 있다.
이로 인해 Board 는 너무 많은 정보를 알게 된다.
이렇게 코드를 작성하게 되면 변경에 너무 취약해진다.
일단 Board 가 많은 정보를 알지 못하게 정보 차단부터 한번 실행해보자.
Board 가 representation 의 0,1 이 진짜 값이라는 것을 이제는 알 수 없게 되었다.
즉, Board 가 Piece 클래스를 신뢰하고 협력을 요청하는 구조로 바뀐 것이다.
한번 Board 의 코드를 다시보자.
전보다 훨씬 나아진 느낌이다.
이제 Location 의 정보도 감추어 보자.
이제 Location 에서는 Piece 의 위치를 전달해준다.
Location 또한 Piece 를 믿고 협력하는 구조이다.
이제 Board 는 Location 에 위치한 Piece 의 정보를 추가하게 된다.
위와 같이 이제 Board 는 정확히 어떻게 위치를 계산해서 알려주는지 알 수 없다.
"다른 객체의 내부를 알 수 없다" 라는 건 좋은 코드라는 뜻일 확률이 높다.
우린 내부를 알 수는 없지만 제공된 Interface 로 협력을 요청해서 코드를 작성했다는 뜻이다.
즉, "디미터의 법칙" 을 준수하게 된 것이다.
6. 모든 엔티티는 최소한으로 설계해라
Entity File 의 코드라인을 최소 150줄을 넘지 않도록 하고, 하나의 패키지에는 10개 미만의 파일을 위치하도록 하자.
라고 적혀있는데, 사실 이건 저렇게 까지 강제하자 라는 느낌보다는
Domain 에 알맞은 책임을 분배해서
Domain 의 역할에 맞는 것만 존재한다면 저절로 코드라인이 적어질 것이라고 생각한다.
즉, Single Responsibility Principle 과 일맥상통하는 내용이 아닐까 싶다.
7. Getter/Setter 를 남발하지 마라.
이건 정말 동감하는데, 내가 코틀린의 자동 getter / setter 를 싫어하는 이유 중 하나이다.
물론, 막을 수 있는 방법이 있지만 그걸로 코드를 더 쳐야한다는 사실이 안타깝다.
코틀린의 자동 getter / setter 에 대해서는 할 말이 많지만 넘어가도록 하겠다.
일단 getter / setter 를 남발했을때 유발되는 것은 아래와 같다.
모든 property 에 getter / setter 가 달려있다면 외부에서 이 객체의 속사정을 더 잘알게 될 것이다.
또한 별다른 의미 없이 여는 것이 협력을 요구하는 것인가? 라는 생각을 해야 한다.
예를 들면 아래와 같은 코드를 보자.
이렇게 무의미하게 getter 와 setter 를 열어 둔다면 아래와 같이 외부에서 사용할 수 있다.
저 코드가 외부에 존재해야 할까?
내 생각엔 BaseBallScore 에 있어야 하는 존재이다.
한번 코드를 아래와 같이 바꿔보자.
똑같이, 스코어를 1 점 추가하는 것이지만, 아래와 같이 더 명확해졌다.
즉, 이제야 비로소 BaseBallScore 에 명확한 동작에 대한 협력을 요청하는 것이다.
다른 문제점으로 무지성 getter / setter 는 응집도를 떨어뜨릴 수 있으므로 주의해야 한다.
예를 들면, 우리가 스코어를 추가할때 "특정 조건" 을 무조건 확인해야 한다고 해보자.
그렇게 되면 제공된 "addScore" 인터페이스에 "특정조건" 을 확인하는 메소드를 private 으로 추가하면 된다.
하지만 아까와 같이 getter / setter 로 이루어진 코드라면 위와 같은 상황에서
외부에서 특정 조건을 검사해야 하는 일들이 발생한다.
즉, 엄청난 중복코드를 양산해낼 것이다.
분명하게 응집도의 큰 영향을 끼칠 뿐더러 확장에 불리한 구조가 되고 말것이다.
이것이 잘 생각하고 인터페이스를 설계해야 하는 이유이다.
Lombok 과 같은 라이브러리를 이용하다보면
@Getter / @Setter 를 아무생각없이 달아놓는 경우가 있는데,
이는 위와 같은 문제를 초래할 수 있다.
느낀점
객체 지향 체조를 오랜만에 정리하면서
지금까지 공부한 객체지향에 대해 좀 더 정리하는 계기를 가지게 되었다.
1년 정도 더 객체지향 패러다임을 연구하고 공부해서 인프런 강의를 찍어보고 싶다.
대부분의 코드는 아래 제 깃허브에 존재합니다.
https://github.com/tmdgusya/object-calisthenics
이글의 대부분은 아래 내용을 참조해서 정리한글임을 적습니다.
https://williamdurand.fr/2013/06/03/object-calisthenics/