이번 게시글은 Effective Kotlin — Chapter3. Reusability에 대한 내용입니다.

1. Knowledge를 반복하여 사용하지 말라

필자가 생각하는 프로그래밍의 가장 큰 규칙은 다음과 같습니다.

프로젝트에서 이미 있던 코드를 복사해서 붙여넣고 있다면, 무언가가 잘못된 것이다.

굉장히 단순하지만 정말로 잘 들어 맞습니다. 실용주의 프로그래머 라는 책에서는 DRY(Don’t Repeat Yourself) 규칙이라고 표현합니다.

Knowledge

프로그래밍에서 knowledge는 넓은 의미로 의도적인 정보를 뜻합니다. 이 의도적인 정보란 결국엔 프로그램 자체를 뜻합니다.
우리가 비즈니스 로직을 구현하기 위한 로직, 알고리즘, ui 형태 등이 모두Knowledge입니다.

모든 것은 변화한다.

프로그래밍에서 유일하게 유지되는 것은 ‘변화한다는 속성’이라는 말이 있습니다. 어떤 프로그램이던, 유지보수를 진행한다면 변화를 할 수 밖에 없습니다.

모든 것은 변화하고, 우리는 이에 대비해야 합니다. 변화할 때 가장 큰 적은 knowledge가 반복되어 있는 부분입니다.

간단하게 말해서 동일한 로직이 하나의 함수 혹은 클래스에서 관리되지 않고, 각기 별도로 구현하여 동작하고 있을 때, 우리는 변화에 대응하기 어렵습니다.

언제 코드를 반복해도 될까?

반대로 추출을 통해 knowledge 반복을 줄이면 안되는 상황을 살펴봅시다. 결론 부터 말하면 얼핏 보면 knowledge 반복처럼 보이지만, 실질적으로 다른 knowledge를 나타내므로 추출하면 안 되는 부분입니다.

이것을 분리해야할지, 합쳐야할지는 결정하기 모호한 부분입니다. 그 분리의 기준은 “함께 변경될 가능성이 높은가? 아니면 따로 변경될 가능성이 높은가?”에 대한 질문으로 어느정도 결정할 수 있습니다.

단일 책임 원칙

코드를 추출해도 되는지를 확인 할 수 있는 원칙으로, SOLID 원칙 중 하나인 단일 책임 원칙이 있습니다. 단일 책임 원칙이란 클래스를 변경하는 이유는 단 한가지여야 한다. 라는 의미 입니다.

비유를 들어 설명하자면 두 액터가 같은 클래스를 변경하는 일은 없어야 한다라고 표현할 수 있습니다. 여기서 액터는 변화를 만들어 내는 존재를 의미합니다.
액터는 서로의 업무와 분야에 대해서 잘 모르는 개발자들로 비유될 수 있고, 이러한 개발자들이 같은 코드를 변경하는 것은 굉장히 위험한 일입니다.

사실 이렇게 표현하면 잘 와닿지 않으므로, 간단한 예를 들어보도록 하겠습니다.

단일 책임 원칙 예시

어떤 대학에서 Student라는 클래스를 갖고 있다고 할 때, 이 클래스는 장학금과 관련된 부서와, 인증과 관련된 부서에서 모두 사용합니다.

단일 책임 원칙 예시 그림
  • qualifiesForScolarship()은 장학금 관련 부서에서 만든 함수, 학생이 장학금을 받을 수 있는 포인트를 갖고 있는지 나타냅니다.
  • isPassing()은 인증 관련 부서에서 만든 함수로, 학생이 인증을 통과했는지를 나타냅니다.

이 두 함수는 모두 학생의 이전 학기 성적을 기반으로 계산됩니다. 그래서 개발자는 두 프로퍼티를 한꺼번에 계산하는calculatePointsFromPassedCourses()라는 함수를 만들었습니다.

그런데 어느 날 학부장이 “덜 중요한 과목은 장학금 포인트를 줄여달라”라고 요청해서 규칙을 바꿔야 하는 상황이 생겼습니다. 이것을 변경하기 위해 파견된 다른 개발자는 qualifiesForScolarship() 을 확인하고 calulatePointsFromPassedCoureses() 를 요구사항에 맞게 변경했습니다.

그럼 어떻게 되었을까요? 변경된 calulatePointsFromPassedCoureses() 로 인해 isPassing() 도 영향받게 되어 학생의 인증이 통과할 수도, 못할수도 있게 변경되어 버렸습니다.

아마도 이걸 만든 개발자는 얼핏 봤을땐 knowledge가 동일했기 때문에 calulatePointsFromPassedCoureses() 라는 하나의 함수로 처리했을 것입니다. 하지만 좀 더 생각해본다면 이 둘은 knowledge가 다르므로, 별도로 분리하는 것이 맞습니다.

이를 해결하기 위해 최초 설계부터 StudentPassingValidator와 StudentQualifiesForScholarshipValidator로 분리해서 만들어야 했었습니다.

아니면 클래스로 분리하기엔 너무 과하다고 판단될 경우엔 확장함수를 이용해서 해결할 수 있습니다.

이 두 예제를 보고 그런 생각이 들 수도 있습니다. “아니 Student 클래스에 private함수로 2개를 만들면 되잖아!”라고 생각할 수 있지만 그건 좋지 않습니다. 이유는 다음과 같습니다.

  • 헬퍼 함수는 public으로 사용하는 것이 관례이며
  • 단일 책임에도 어긋나게 됩니다. 상식적으로 생각해보았을 때, 학생 자체가 자신의 장학금을 자체 계산하거나, 인증 통과 여부를 계산하지 않습니다. 학생은 어떤 무언가에게 내가 통과 했는지를 확인하는 것이 상식적으로 맞으며, 그것에 대한 분리로 위의 2개의 예제처럼 분리해야합니다.

2. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

코틀린은 코드 재사용과 관련해서 프로퍼티 위임(by)라는 새로운 기능을 제공합니다. 아주 대표적으로 lazy , Delegates.observable, Delegates.vetoable, Delegates.notNull이 있습니다.

Delegates.observableDelegates.vetoable의 차이점

그럼 어떻게 이런 코드가 가능하고, 프로퍼티 위임을 어떻게 활용할 수 있는지 살펴볼 수 있게 간단한 프로퍼티 델리게이트를 만들어 보겠습니다.

프로퍼티 위임 예시

간단한 예로, 간단한 로그를 출력하는 프로퍼티를 만들어 보도록 하겠습니다.

가장 기본적인 형태
프로퍼티의 setter/getter에서 로그를 출력하는 방법일 것입니다.

만약 이 로그를 다른 곳에서도 재 사용한다면 Property클래스를 상속하여 by 키워드를 통해 위임시킬 수 있습니다.

프로퍼티 위임이 어떻게 동작하는지 이해하려면, by가 어떻게 컴파일 되는지 확인 해야 합니다.

by 키워드를 통해 우리가 정의한 LoggingProperty가 생성되고 property들이 함수로 생성이 됩니다. 그리고 그 setter/getter들이 LoggingProperty를 호출하는 식으로 구현이 됩니다.

3. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라

Argument로 함수에 값을 전달할 수 있는 것처럼, 타입 Argument를 사용하면 함수에 타입을 전달할 수 있습니다. 이를 제네릭 함수라고 부릅니다. 대표적으로 Iterable의 확장함수인 filter()가 있습니다.

제네릭은 기본적으로 List<String> 또는 Set<User> 처럼 구체적인 타입으로 컬렉션을 만들 수 있게 클래스와 인터페이스에 도입된 기능입니다. 물론 컴파일 과정에서 최종적으로 이러한 타입 정보는 사라지지만, 개발 중에는 특정 타입을 사용하게 강제할 수 있습니다.

제네릭 제한

타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용하게 타입을 제한하는 것입니다.

다음과 같이 정의하여 T 타입을 Comparable<T>로 제한할 수 있습니다.

또한 드물지만 where 키워드를 이용하여 2개 이상의 타입을 제한할 수 있습니다

4. 타입 파라미터의 섀도잉을 피하라

다음 코드처럼 글로벌 프로퍼티와 함수 파라미터가 같은 이름을 가질 수 있습니다. 이렇게 되면 함수 파라미터가 글로벌 프로퍼티를 가리게 됩니다. 이걸 섀도잉이라고 부릅니다.

그리고 이러한 섀도잉 현상은 클래스 타입 파라미터와 함수 타입 파라미터 사이에서도 발생합니다.

이러한 상황을 의도하는 경우는 거의 없을 것 입니다. 또한 코드만 봐서는 둘이 독립적으로 동작한다는 것을 빠르게 알아내기 힘듭니다. 따라서 addTree() 가 클래스 타입 파라미터인 T를 사용하게 하는 것이 좋습니다.

Effective Kotlin Chapter 3. Reusability에 대한 요약은 여기까지 입니다.

다음 게시글에서 Chapter4. 추상화 설계에 대한 요약으로 작성하도록 하겠습니다.

--

--