이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 27~8장 써머리입니다.
27장은 객체지향 설계 5가지 원칙인 SOLID을 설명하며, 필수는 아니지만 프로젝트를 만들 때 어떤 원칙들을 기반으로 설계하면 좋을지에 대한 청사진을 제시한다. SOLID는 원칙들의 첫 단어들로 구성한 축약어이다. Golang에서만 활용할 수 있는 원칙들이 아닌 OOP 패러다임을 지원하는 여러 프로그래밍 언어들에서 적용할 수 있다.
28장은 Golang에서 지원하는 단위 테스트 도구와 단위 테스트를 코딩할 때 유용한 라이브러리를 소개한다. 또한 TDD(Test Driven Domain)에 대한 내용도 소개한다.
- 27 객체지향 설계 원칙 SOLID
- 27.1 객체지향 설계 5가지 원칙 SOLID
- 27.2 단일 책임 원칙
- 27.3 개방-폐쇄 원칙
- 27.4 리스코프 치환 원칙
- 27.5 인터페이스 분리 원칙
- 27.6 의존 관계 역전 원칙
- 27.7 학습 마무리
- 28 테스트와 벤치마크
- 28.1 테스트 코드
- 28.2 테스트 주도 개발
- 28.3 벤치마크
27. 객체지향 설계 원칙 SOLID
시작하기 선행지식이 필요하다. 책에서는 바로 설명하지 않고 나쁜 설계 방법을 소개하면서 선행지식을 소개하는 식으로 진행한다. 사실 소개할 선행지식을 이해하려면 기반이 되는 또 다른 선행지식인 모듈에 관련된 내용이 필요하지만 그건 필요할 때 찾아보면 좋을 것 같다. 선행지식은 응집도와 결합도이고 모듈의 독립성을 판단하는 지표라 하며, 응집도는 높아야 하고 결합도는 낮아야 한다. 본인은 보통 예제 코드들을 같이 만들어 리뷰하는데 SOLID는 이론 위주로만 리뷰할 생각이다. 이유는 이러한 이론들을 이해하고 각자 자신들의 코드나 남의 코드를 보면서 이론들을 대입해야 판단할 수 있는 판단력이 생기고 애매한 부분들은 동료들과 토론하면서 계속 발전해 나가야 한다. 당연한 말이지만 예제 코드만 보고 아~ 그렇구나 하고 넘어가면 절대 자신게 될 수 없다. 간단한 예시는 작성하겠지만 응집도와 결합도는 본인 스스로도 이번에 공부하면서 알게 된 내용이라 모르는 내용이 많고 잘못 이해할 수 있으니 참고했던 사이트를 공유하겠다. 잘못된 부분이 있다면 댓글로 피드백 부탁드립니다.
응집도 (cohesion)
응집도를 쉽게 설명하면 하나의 목적을 달성하기 위한 기능들만 모여있는 것이라 생각하면 된다. 또한 응집도를 높이면 높일수록 좋은 모듈을 만들 수 있다 한다. 이해를 돕기 위해 간단한 예를 들면 로그인 기능이 있을 때 아이디와 비밀번호를 검증해 사용자를 확인하는 로직과 애플리케이션을 이용할 때 필요한 접근토큰(access token) 발행 로직과 세션(session) 등록 로직이 같이 있는 경우 응집도가 높을까? 정답은 아니다. 로그인 로직을 수행하기 위한 기능들의 집합과 접근토큰을 발행하는 로직들의 집합, 세션을 등록 로직들의 집합을 나눠야 응집도를 올릴 수 있다. 응집도가 높을 때 장점으론 로그인 기능이 확장돼 소셜 로그인 기능을 추가해야 할 때 나타날 수 있다. 두 로직이 같이 있는 경우에는 소셜 로그인을 만들 때 소셜 로그인 기능들과 접근토근이나 세션을 발행하는 코드가 계속 같이 있을 것이고 다른 형태의 로그인 기능이 추가될 때마다 접근토큰과 세션을 발행하는 코드들은 혹처럼 계속 따라다닐 것이다. 이런 상황에서 접근토큰과 세션 로직을 수정해야 할 상황이 오면 기존에 작업했던 로그인 기능들을 전부 수정해야 하고 이런 상황에 실수는 너무 쉽게 발생할 수 있다. 반면 두 로직을 나눈다면 로그인 기능들을 추가될 때마다 접근토큰과 세션 로직을 호출하기만 하면 되기 때문에 추가되는 로그인 기능들에 집중할 수 있게 되고 접근토큰과 세션 로직을 수정할 때에도 로그인 기능과 별도의 로직이기 때문에 기존 로그인 기능의 수정을 최소화할 수 있다. 이 부분은 다음으로 설명할 결합도와 밀접한 이야기다.
- 우연적 응집도(Coincidental Cohesion)
로직끼리 연관 없이 모여있는 응집도로 매우 낮은 응집도라 할 수 있다. - 논리적 응집도(Logical Cohesion)
앞서 설명한 로그인 기능과 접근토큰 및 세션 기능들과 같이 논리적으론 하나의 로직처럼 사용하기 때문에 논리적 응집도라 할 수 있다. - 시간적 응집도(Temporal Cohesion)
예를 들자면 특정 시간마다 이전에 계속 쌓은 로그 데이터들을 분석하기 위해 데이터 레이크를 저장하는 곳에 전송하는 로직이 한 모듈(한 파일)에 있다고 가정 하자. 이때 특정 시간마다 실행시키는 로직과 로그 파일들을 한 곳에 모으는 로직, 데이터 레이크에 전송하는 로직이 같이 있는 경우 시간적 응집도라 할 수 있다. 이때도 로그 파일들을 한 곳에 모으는 로직과 데이터 레이크에 전송하는 로직과 특정 시간마다 작업을 실행시키는 로직을 분리하여 하나의 목적을 수행할 수 있는 로직들끼리 나눠 응집도를 올리는 게 좋다. - 절차적 응집도(Procedural Cohesion)
한 모듈에 서로 다른 기능들이 모여있지만 다른 기능들끼리 순서대로 실행될 경우 절차적 응집도라 하는 것 같다. 이 부분은 적당한 예시가 떠오르지 않아 넘어가겠다. 좋은 예시가 있다면 댓글로 공유 부탁드립니다. - 통신적 응집도(Communication Cohesion)
서로 다른 기능들이 기능을 수행하기 위해 필요한 입력과 수행 결과의 출력 형태가 같은 기능들이 모여있을 경우 통신적 응집도라 한다. 예를 들어 문자열을 입력받아 특정 포맷에 맞게 파일을 생성하는 로직이 있을 때 json 포맷을 만드는 로직과 csv 포맷을 만드는 로직이 한 모듈에 같이 있는 경우를 뜻하는 것 같다. 두 로직의 입력은 문자열로 같고 출력도 파일로 같으니 적당한 예인 것 같다. 틀리면 댓글로 알려주세요. 이 문제를 해결하기 위해선 인터페이스 분리 원칙인 ISP를 적용하면 될 것 같다는 생각이 든다. - 순차적 응집도(Sequential Cohesion)
절차적 응집도와 헷갈릴 수 있지만 차이점은 모듈 내의 여러 로직들이 있을 때 한 로직의 수행 결과인 출력값을 같은 모듈 내의 다른 로직들이 사용할 경우를 의미하는 것 같다. 예를 들면 애플리케이션에서 DB를 사용해야 할 때 여러 방법이 있지만 Connection pool을 만들어 사용할 경우 애플리케이션의 main 함수가 실행될 때 Connection pool을 초기화(initialized)할 것이다. Connection pool을 초기화한 결과를 같은 모듈에 있는 다른 로직들이 사용할 경우 순차적 응집도라 생각하면 될 것 같다. 틀리면 댓글로 알려주세요. - 기능적 응집도(Functional Cohesion)
높은 응집도를 만들기 위해 우리가 지향해야 하는 응집도이며, 한 모듈에는 오직 하나의 목적을 수행하는 로직들만 있는 경우를 뜻한다. 매우 높은 응집도를 뜻한다.
결합도 (coupling)
응집도와 반대되는 개념이라 생각해도 좋지만 결합도를 쉽게 설명하면 여러 모듈들 간의 연관관계(통신, 호출 등등)를 느슨하게 유지하여 확장 및 변경 즉 유지보수성을 올리자는 개념이다. 예를 들어 응집도에서 설명했던 로그인 기능과 접근토큰 발행 및 세션 등록 로직을 수행하는 각각의 모듈들 간에 통신하기 위해 어떤 데이터들을 어떻게 받아 처리하는지에 대한 관점과 A 모듈을 수정하더라도 A가 아닌 다른 모듈들에게 영향을 가장 적게 줄 수 있는 관점이 결합도를 이해할 수 있도록 도와줄 것이다.
- 내용 결합도(Content Coupling)
A 모듈에 있는 변수나 기능을 A가 아닌 다른 모듈에서 사용하는 경우 내용 결합도라 한다. 결합도가 제일 높은 결합도이며, 우리가 지양해야 한다. 이유는 A 모듈이 수정될 경우 A 모듈에 있는 변수나 기능을 사용한 다른 모듈들에게 영향을 주기 때문이다. - 공통 결합도(Common Coupling)
A 모듈에 있는 전역 변수를 B 모듈에 있는 로직에서 A 모듈에 있는 전역 변수와 상호작용(수정하거나 읽거나 쓰거나 등)하는 경우 공통 결합도라 한다. 코딩을 하다 보면 전역 변수로 사용해 다른 모듈에서도 공통적으로 사용해야 할 데이터들이 있는데 이때는 전역 변수를 상수로 만들어서 다른 모듈들이 읽기만 하거나 전역 변수의 접근제한자를 둬서 다른 모듈에서 직접 접근하지 못하고 함수를 통해 접근할 수 있게 만들어야 한다. 함수를 통해 접근할 수 있게 하더라도 가능하면 다른 값으로 대입을 못 하도록 해야 한다. - 외부 결합도(External Coupling)
예를 들면 로그인 로직을 담당하는 모듈에서 로그인 로직을 수행한 결과인 사용자 정보를 구성하는 구조체를 접근토큰을 발행하는 모듈의 로직을 수행하기 위해 매개변수(parameter)로 넘기고 접근토큰을 발행하기 위해 매개변수로 받은 사용자 정보가 있는 구조체를 사용하는 경우 외부 결합도라 한다. 이럴 경우 사용자 구조체와 접근토큰 발행하는 함수의 결합도가 높아져 접근토큰을 발행하기 위해선 사용자 구조체가 꼭 필요하다는 의미다. 이런 상황엔 사용자를 식별할 수 있는 고유한 값을 접근토큰을 발행하는 함수의 매개변수로 사용하게 수정함으로 접근토큰을 사용자 구조체로부터 자유로울 수 있다, - 제어 결합도(Control Coupling)
A 모듈과 B 모듈이 있을 때 A 모듈의 로직이 B 모듈의 로직을 호출하고 있는 상황에서 A 모듈이 B 모듈의 로직을 제어할 수 있는 매개변수를 전달하여 다른 동작을 하도록 하는 것이 제어 결합도라 한다. 이럴 경우 A 모듈은 B 모듈의 로직을 알아야 하는 점과 B 모듈의 제어와 관련된 상수들을 A 모듈에서 사용함에 따라 권리 전도현상이 발생할 수도 있다. 그러므로 A 모듈과 B 모듈의 결합도는 올라가서 B 모듈을 수정하는 경우 A 모듈도 수정해야 하는 상황이 생길 수 있다. - 스탬프 결합도(Stamp Coupling)
A 모듈과 B 모듈의 통신할 때 필요한 매개변수의 형태가 배열이나 구조체와 같은 원시값이 아닌 경우 스탬프 결합도라고 한다. 여기서 원시값은 더 이상 쪼갤 수 없는 값들을 뜻하며 char, int, string, boolean, float과 같은 가장 기본이 되는 형들이다. - 자료 결합도(Data Coupling)
모듈 간 통신할 때 매개변수로만 전달되고 그 매개변수의 형태가 원시값인 경우 자료 결합도라 하고 모듈 간 제일 낮은 결합도를 유지할 수 있기 때문에 우리가 지향해야 한다. 즉 모듈 간 상호 작용할 때 원시값을 통해서 일어난다는 뜻이다.
이렇게 SOLID를 이해하기 위한 선행 지식인 응집도와 결합도를 알아봤다. 응집도와 결합도는 서로 얽혀있기 때문에 응집도를 높이기 위해 관련 로직들만 모듈화 했다 하더라도 모듈 내부에 결합도가 높을 수도 있고 반면 결합도를 낮추기 위해 수정한 내용으로 응집도를 낮출 수 있다. 코딩할 때 위에 설명한 이론들을 전부 이해하고 응용할 순 없겠지만 이런 이론들을 토대로 만들어진 Best Practice(모범사례) 들을 찾아서 공부해서 습관으로 만드는 것도 방법이다. 여러 모범사례 중 하나인 SOLID는 높은 응집도와 낮은 결합도를 유지하기 위한 다섯 가지 원칙들을 뜻하는 것이니 잘 공부하는 것이 좋을 듯하다.
27.1 객체지향 설계 5가지 원칙 SOLID
이제 본론이다. 앞서 SOLID는 다섯 가지 원칙들의 첫 글자를 이어 붙여 만든 축약어라 설명했는데 자세히 알아보자. 책에선 무엇이든지 항상 맨 처음 나오는 게 가장 중요한 법이라 말하고 있다.
S - SRP (Single Responsibility Principle) 단일 책임 원칙
말 그대로 한 모듈(객체)은 단 하나의 책임만 가져야 한다. SRP를 잘 이해하고 코딩하면 각각의 모듈은 응집도가 높아질 수 있을 것이다. 쉽게 말하면 로그인 로직을 담당하는 모듈은 오직 로그인이라는 동작을 수행하기 위한 로직만 포함하고 로그인과 관련 있는 다른 로직이라도 나눠서 만들라는 의미이며, 기존 코드를 SRP를 잘 따를 수 있도록 리팩터링 할 때 여러 기법들이 있으니 찾아보면 좋을 것 같다.
O - OCP (Open-Closed Principle) 개방-폐쇄 원칙
확장은 열려(개방) 있고 변경(수정)은 닫혀(폐쇄) 있다는 뜻으로 A 모듈에 기능 확장함에 있어 A 모듈을 사용하고 있는 다른 모듈들에게 영향이 없어야 한다. 쉽게 말해 A 모듈에 기능을 추가한다 해서 다른 모듈들의 코드를 수정하는 상황이 생기면 안 된다는 뜻이다.
L - LSP (Liskov Substitution Principle) 리스코프 치환 원칙
책에서는 리스코프 치환 원칙이 제일 이해하기 어렵다 말하고 있다. 책에 있는 내용 중 정의 부분만 그대로 공유해 보겠다.
q(x)를 타입 T의 객체 x에 대해 증명할 수 있는 속성이라 하자.
그렇다면 S가 T의 하위 타입이라면 q(y)는 타입 S의 객체 y에 대해 증명할 수 있어야 한다.
쉽게 말하면 T 형태의 객체 x(변수)는 S 형태의 객체 y(변수)와 같아야 하고 생각하면 된다.
즉 T 형태로 S 형태의 instance을 생성할 수 있어야 한다는 뜻이고 의사코드로 표현하면
T x = new S();
위와 같다. 이로 인해 T 형태에서 호출할 수 있는 함수(동작)들은 S 형태에 재정의(override)돼야 한다. 더 쉽게 말하면 다형성(polymorphism)이다.
Golang에서는 상속이나 구현이 없지만 덕 타이핑이 있기 때문에 LSP를 적용할 수 있다. 책에서 상속이 없어도 상속이 있는 다른 언어들보다 더 발전된 oop를 지원한다 주장하니 궁금하면 책을 사서 읽어봐도 좋을 것 같다.
I - ISP (Interface Segregation Principle) 인터페이스 분리 원칙
이건 어떻게 보면 인터페이스의 SRP인가?? 라 생각할 수 있지만 조금 개념이 다른 것 같다. 인터페이스 분리 원칙은 하나의 인터페이스에 목적을 수행할 필요한 모든 동작들을 추상화하여 정의하지 말고 동작들 간의 공통적인 부분들을 추려 여러 인터페이스로 분리한 후 필요한 동작들만 구현체에서 재정의할 수 있도록 하는 원칙이다. 하나의 인터페이스 모든 동작들을 정의하면 구현체들은 불필요한 기능들까지 전부 재정의해야 하는 상황과 동작을 추가할 때 많은 구현체들이 받는 영향을 줄이고자 나온 원칙인 것 같다.
D - DIP (Dependency Inversion Principle) 의존 관계 역전 원칙
책에서 SRP의 다음으로 가장 중요하다고 말하는 DIP는 상위 계층이 하위 계층을 의존하지 않도록 상위 계층과 하위 계층은 구현체가 아닌 추상체에 의존하도록 설계한다는 뜻이다. 쉽게 말하면 입력장치와 출력장치가 있을 때 입력장치는 키보드, 마우스 등 구현체가 아닌 입력장치라는 추상체를 만들어 키보드나 마우스를 구현하는 구현체가 의존하도록 만들고 출력장치는 모니터, 스피커, 프린터 등 입력장치와 마찬가지로 구현체가 아닌 추상체인 출력장치를 만들어 구현체가 의존하도록 만들어 상위 계층과 하위 계층 간의 결합도를 낮춰 확장에 용이하게 만든다는 의미다. 이렇게 구현체가 아닌 추상체를 의존하게 만들면 키보드와 프린터, 키보드와 스피커, 키보드와 모니터, 마우스와 프린터, 마우스와 스키퍼, 마우스와 모니터와 같이 여러 조합을 만들 수 있음으로 더 유연하게 만들 수 있다. 여기서 추상체는 인터페이스고 구현체는 인터페이스의 동작들을 재정의한 것들이다.
책에 더 많은 내용들과 예제들 그리고 나쁜 설계들을 알려주고 있다.
27장을 한 줄로 요약하면 기능은 한 가지 책임만 수행하도록 만들며, 구현체보단 추상체에 의존하여 응집도는 높게 결합도는 낮게 만들도록 노력하자.
28.1 테스트 코드
Golang은 세 가지 표현 규약을 잘 따라 테스트 코드를 작성하면 다른 라이브러리들을 사용하지 않아도 테스트 코드를 실행할 수 있도록 내부적으로 지원한다.
- 파일명이 *_test.go 형식이어야 한다.
- testing package를 import 해야 한다.
- 테스트할 코드가 있는 함수의 명은 항상 Test라는 접두어가 꼭 들어가야 한다.
또한 함수의 매개변수로 *testing.T 형태를 명시해야 한다.
심지어 go test 명령을 지원하니 IDE(Integrated Development environment)가 없어도 손쉽게 테스트할 수 있어 배포하기 전 미리 테스트를 수행한 후 테스트의 결과를 확인하여 배포를 진행하도록 만들 수 있다.
그럼에도 불편한 점은 분명 있는데 기본으로 지원하는 함수들의 기능이 미약하다. 하지만 테스트를 도와주는 외부 라이브러리들을 추가로 사용하면 다양한 기능들로 테스트할 수 있는데 책에서 소개해주는 외부 라이브러리는 "stretchr/testify"다.
28.2 테스트 주도 개발
책에서 과거에 비해 테스트의 중요성이 커지는 이유로 두 가지를 말하고 있는데 첫 번째론 과거에는 소수의 프로그래머들이 프로그램을 만든 반면 현대에는 사용자들의 요구조건을 만족시키기 위해 더 복잡해진 프로그램을 만들어야 하는 상황이 돼서 많은 프로그래머들이 협업하는 환경에서 예기치 못한 버그들을 예방하기 위함이다. 두 번째론 과거에 비해 고가용성(high availability)에 대한 요구사항이 높아졌다 한다. 가용성은 오랫동안 장예 없이 서비스를 지원할 수 있는가? 라 생각하면 된다.
테스트 방법으로 두 가지를 소개한다.
- 블랙박스 테스트
프로그램의 코드들이 대상이 아닌 오로지 사용자의 입장에서 프로그램을 테스트하는 방법이다. 사용성 테스트(usability test)라고도 한다. - 화이트박스 테스트
프로그램의 코드를 테스트하는 방법으로 단위 테스트(unit test)라고도 한다.
본론인 TDD(Test Driven Development)는 화이트박스 테스트의 허점을 해결할 수 있는 대안으로 테스트의 작성 방법이 아닌 테스트 진행 방식 즉 테스트 사이클이다.
본인은 TDD는 약팔이라 생각하긴 하는데 대규모 프로젝트를 경험하지 못했기 때문에 약팔이라 주장은 못 하겠다.
기본 개념은 테스트하기 위한 코드를 작성하고 테스트를 진행했을 때 실패하거나 코드의 개선이 필요한 경우 SOLID 원칙을 기반으로 리팩터링(refactoring) 한 후 테스트를 진행한다. 이때 리팩터링의 결과로 테스트하기 위한 코드를 수정해도 괜찮다. 이 과정을 테스트가 성공하거나 코드를 개선할 필요가 없어질 때까지 계속 반복한다.
테스트 작성 방법으로 AAA라는 것도 있으니 찾아보면 좋을 것 같다.
28.3 벤치마크
Golang은 테스트 외 코드 성능을 검사하는 벤치마크도 지원한다. 테스트처럼 세 가지 표현 규약이 있다. 벤치마크는 A 기능과 B 기능의 성능을 비교한다는 뜻으로 생각하면 된다.
- 파일명이 *_test.go 형식이어야 한다.
- testing package를 import 해야 한다.
- 벤치마크할 코드가 있는 함수의 명은 항상 Benchmark라는 접두어가 꼭 들어가야 한다.
또한 함수의 매개변수로 *testing.B 형태를 명시해야 한다.
테스트와 마찬가지로 go test -bench 명령을 지원한다.
마무의리!!!
'Golang > Tucker' 카테고리의 다른 글
[묘공단] 7주차 (1) (0) | 2023.11.17 |
---|---|
[묘공단] 6주차 (2) (0) | 2023.11.12 |
[묘공단] 5주차 (3) (0) | 2023.10.29 |
[묘공단] 5주차 (2) (0) | 2023.10.29 |
[묘공단] 5주차 (1) (1) | 2023.10.29 |