Roen의 iOS 개발로그

초심으로 돌아가기) 객체지향 프로그래밍 2: SOLID 원칙과 객체지향 생활체조

by Steady On

객체지향 프로그래밍 시리즈 이전글

[Apple Platform Develop/Study Log] - 초심으로 돌아가기) 객체지향 프로그래밍1: 특징

 

초심으로 돌아가기) 객체지향 프로그래밍1: 특징

영어로는 Object Oriented Programming 줄여서 OOP... 개발하면서 진짜 많이 듣는 단어다. 약간 뜬구름 잡는 듯한 느낌이 있는데 요즘 하고 있는 스터디와 참여하고 있는 원티드 프리온보딩에서 배운 내용

steady-on.tistory.com

 

객체지향 설계의 원칙

앞에서 객체지향 프로그래밍이 뭔지, 왜 하는지 알아봤으니까 이제 이걸 어떻게 하는지 알아보려고 한다. 그 유명한 솔리드 원칙!!

일단 대충 위키를 참고하면, 로버트 마틴이라는 사람이 2000년대 초반에 만든거라고 한다. 이 원칙은 소프트웨어 작업물이 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할때 적용할 수 있다고 한다.

유지 보수와 확장이 용이한 코드란 결국 가독성이 높고, 구조가 단순해야 한다는 뜻이다. SOLID원칙은 가독성과 단순성, 유연성을 추구한다는 점에 초점을 맞춰서 하나씩 뜯어보자.

 

SOLID 원칙

1. 단일 책임 원칙(Single-Responsibility Principle: SRP)

하나의 객체는 하나의 책임만 가져야 한다.

앞글에서 객체지향은 프로그램을 여러개의 모듈이 모여서 만들어지는 것으로 본다고 했다. 여기서 말하는 모듈이 곧 객체다. 따라서 바꿔말하면 하나의 모듈은 하나의 일만 하도록 해야한다고 할 수 있다. 우리가 사용하는 프로그램은 한가지 기능만 가지고 있지 않다. 로그인도 할 수 있고, 회원정보도 저장할 수 있고, 글을 쓸 수도 있고, 글을 저장할 수도 있다. 이런 기능들이 만약 여러개의 다른 모듈로 쪼개져 있다면, 우리는 로그인 기능을 하나 수정하기 위해서 모든 모듈을 다 살펴보고 수정해야 할지도 모른다. 그리고 무사히 수정을 했다고 하더라도 다른 글쓰기 기능 등에 영향을 미쳤을지도 모른다(에러가 발생할지도?!). 이런걸 결합도가 높다(응집도가 낮다)고 한다. 어떤 기능에 대해 수행되어야 할 명령(메서드)들이 여기저기 퍼져 있으니(응집이 되어있지 않음) 응집도가 낮고, 또 이 모듈, 저 모듈에 메서드가 걸쳐 있으니까 결합도가 높은 것이다.

따라서 하나의 객체(모듈)은 하나의 책임(일)만 하게 만들자.

 

2. 개방-폐쇄의 원칙(Open-Close-Principle: OCP)

확장에 열려있고 변경에는 닫혀있어야 한다.

확장(어떤 기능을 수정하는게 아니라) 뭘 더 추가시키기는 쉬워야 하지만, 그게 기존의 코드를 최대한 건드려서는 안된다는 의미이다. 예를 들어 "애플 계정 로그인"에 관련한 모듈을 만들고, 로그인 메서드를 만들었다. 근데 탈퇴 기능이 없어서 심사에 리젝을 당했다고 하자. 그래서 탈퇴 메서드를 추가할 때 기존에 구현된 로그인 메서드는 최대한 건드리면 안된다는 말이다.

 

3. 리스코프 치환 원칙(Liskov Substitution Principle: LSP)

서브 클래스는 슈퍼 클래스의 역할을 완벽히 수행할 수 있어야한다.

이건 앞글에서 이야기 한 객체지향의 특징 중 일반화와 관련이 있는 것 같다. 앞글의 일반화 예시로 다음과 같은 언급을 했는데,

(구체적) 하루 → 푸들 → 개 → 동물 (일반적)

여기서 오른쪽으로 갈 수록 상위(슈퍼) 클래스이고, 왼쪽으로 갈 수록 하위(서브) 클래스이다. 

동물은 움직일 수 있다.

마찬가지로 동물인 개도 움직일 수 있고, 다리는 4개이고, 털이 있고, 짖는다.

푸들도 움직일 수 있고, 다리가 4개이고, 털을 가졌지만, 곱슬곱슬하고, 짖는다.

하루는 움직일 수 있고, 다리가 4개이고, 곱슬곱슬한 털을 가졌고, 짖고, 앉아/기다려/빵을 할 수 있다.

이렇게 서브 클래스가 슈퍼클래스의 특징을 모두 가져야 한다는게 리스코프 치환 원칙이다.

하지만, 만약에 우리가 클래스 설계에서 동물은 뛸 수 있고, 날 수 있다고 정의했다면 어떨까? 그렇다면 개는 날 수 없다. 이렇게 되면 서브클래스인 개가 슈퍼클래스의 날 수 있다를 할 수 없으므로 이 원칙에 위배된다.

 

4. 인터페이스 분리 법칙(Interface Segregation Principle: ISP)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 즉 바꿔말하면, 광범위 항생제 하나 쓰는 것보다 특정 균을 표적한 항생제 여러개가 낫다는 거다(도메인 유머ㅎㅎ). 예를 들면, 위에서 언급한 예로 우리가 동물의 특성을뛸 수 있고, 날 수 있는 것이라고 정의했다고 하자. 그럼 우리가 이 동물 클래스를 상속 받아 개를 만들었을때 "날 수 있다"는 메서드를 실행할 수 있는 것은 오류이기 때문에 못쓰게 막아야 한다. 그러므로 차라리 범용적으로 동물을 정의하지 말고, 포유류; 뛸 수 있는, 조류; 날 수 있는 이렇게 분리해서 필요한 부분만 선택해서 사용할 수 있도록 만들어야 한다는 것이다.

 

5. 의존성 역전 원칙(Dependency Inversion Principle: DIP)

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."....고 하는데, 다르게는 상위 모듈이 하위 모듈에 의존해서는 안된다고 하는 모양이다. 추상화는 앞글에서 말했다시피 불필요한것을 쳐내는 작업이고, 구체화는 반대로 디테일을 살리는 것이다. 객체가 자세하면 자세해질수록 하위 객체가 준수해야 하는게 많아진다. 그렇게 되면 하위 모듈을 만들었을 때 해당 구체화에 맞지 않는 일이 생기면서 상위 모듈을 오히려 수정해야 하는 일이 발생할 수 있다. 하위 모듈을 만들기 위해서 오히려 상위 모듈을 수정하는 것, 즉 상위 모듈이 앞으로 만들 하위 모듈에 따라 달라져야 한다. 다시 바꿔 말하면 상위 모듈이 하위 모듈에 의존한다.... 게다가 그렇게 해버리면 2법칙인 개방-폐쇄 원칙도 위반하는게 된다. 그러니까 처음부터 추상화를 잘 해서 피라미드 형태로 상하위 클래스가 잘 정렬되도록 설계를 해야한다.

 

요약

결국은

1. 설계를 잘 하자! 객체가 피라미드 형태로 상하위 구조를 쌓을 수 있게! 

2. 잘 쪼개자! 하나가 하나의 일만 수행할 수 있게!

3. 추상화를 잘 하자! 불필요한 것을 잘 처내지 못해서 수정에 수정을 거듭하지 않게!

인 것 같다.

 

객체지향 생활체조

이것도 사실은 원티드 프리온보딩에서 배운건데, 찾아보니까 원래 출처는 마틴 파울러라는 사람이 쓴 소트웍스 앤솔러지라는 책에 나오는 9가지 규칙이라고 한다. SOLID 원칙이 대충 이렇게 볼때는 이해되고 알겠는데 막상 코드로 쓸려면 그래서 뭘 어쩌라는 거야? 싶은 나같은 사람들에게 제시하는 가이드라고 한다.

 

규칙 1. 메서드당 들여쓰기 한 번

규칙 2. else 예약어 사용 금지

두 규칙은 묶어서 얘기하자.

코드의 들여쓰기가 깊어지면 깊어질수록 가독성이 폭망한다. 조건문 안에 조건문 안에 조건문 안에...... 갈래도 많아질 뿐더러 알아보기도 힘들다. 또 갈래가 많아진다는 건 그만큼 예외 상황 처리를 많이 해야 한다는 것이고, 함수가 여러가지 일을 하게 될 가능성이 높아진다는 의미이다. else 예약어도 마찬가지로 갈래의 갈래를 낳을 뿐이고, 이게 많아지면 원래 if문이 뭐였는지도 알 수 없어진다.

결론은 가독성적인 측면에서, 1함수 1기능을 위해서 메서드당 들여쓰기 한 번! else 예약어는 사용금지!

 

규칙 3. 원시값과 문자열의 포장

이건 나도 생각없이 쓰고 있었던 건데, 얼마전에 멘토님과 기초에 대해 공부하면서 숫자 야구 게임을 만든 적이 있다. 숫자 야구 게임은 1~9까지의 숫자 중에서 랜덤하게 뽑아 정렬하고, 숫자와 자리를 맞추는 게임이다. 나는 아무 생각없이 숫자를 3개 맞추는거니까 이런 저런 조건문에 3이라는 숫자를 썼다. 근데 생각해보면, 게임의 난이도를 올리기 위해서 3보다 더 많은 숫자를 맞출 수도 있다. 또, 숫자 야구가 아니라 다른 훨씬 더 복잡한 프로그램이었다면, 나는 나중에 이 3이 뭘 의미하는지 알 수 있을까? (아니요!) 그러므로 이런 원시값이나 직접적으로 쓰는 문자열들은 상수에 담아 포장을 하고 이름을 붙여서 이게 뭔지 알 수 있게 해야 한다. 가독성도 올라간다! 그냥 3이라고 하는거 보다 맞춰야할 숫자의 개수 CountOfGuessNumber 이런거면 나중에도 알아보기 쉽다!

 

규칙 4. 한 줄에 한 점만 사용

여기서 말하는 점은 객체의 프로퍼티에 접근하기 위한 점을 의미한다. (고차함수 체이닝 그런거는 아님!)

예를 들어 로그인 된 유저의 이메일 주소를 불러오기 위해서

class Player {
	var email: String?
    
    func getEmail() {
		self.email = Authentication().AppleAuth().getUser(ID).getInfo(email)
    }
}

이런식으로 객체의 인스턴스의 인스턴스의 메서드의 메서드...를 파고파고 들어가면 이메일 주소 하나를 얻기 위해서 Player 객체는 Authentication 객체도 알고, AppleAuth 객체도 알고 getUser, getInfo까지 너무 많은 변화에 영향을 받게 되므로 의존도가 높아지니 하지 말라는 것이다.

 

이 규칙은 디미터 법칙이라는 것과 함께 많이 설명되는데,

디미터 법칙(Law of Demeter)이란 "모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다"는 것이며, 다른 말로는 최소한의 지식 원칙이라고 한다. 이 법칙에서는 "노출 범위를 제한하기 위해 객체의 모든 메서드는 다음에 해당하는 메서드만을 호출해야 한다"고 말한다.

  1. 객체 자신의 메서드
  2. 메서드의 파라미터로 넘어온 객체들의 메서드
  3. 메서드 내부에서 생성, 초기화된 객체의 메서드
  4. 인스턴스 변수로 가지고 있는 객체가 소유한 메서드

그래서 메서드를 부르기 위한 점은 한줄에 하나만 써서 객체간의 의존도를 낮추고자 하는 것이 이 규칙의 요지이다.

 

규칙 5. 축약 금지

이건 Swift API Design Guidelines에서도 나왔던 건데 나만 아는 단축어를 사용하지 말라는 것이다. 축약해서 단순하게 짓기보다는 명료하게 짓자! 코드의 가독성을 높이기 위해서!! 그리고 혹시나 이름이 너무 길면 다시한번 생각해보자. 이 메서드는 여러가지 일을 하고 있지는 않는가?!

 

규칙 6. 모든 엔티티를 작게 유지

50줄이 넘는 객체와 파일이 10개 이상인 패키지를 지양하자는 원칙이라고 한다. SOLID의 단일 책임 원칙에 따르면 하나의 객체는 하나의 책임만 가진다. 그러므로 추상화를 잘해서 최소한의 필요한 만큼의 코드를 가져가야 한다는 규칙이다.

 

규칙 7. 2개 이상의 인스턴스 변수를 가진 클래스 사용 금지

인스턴스의 변수가 많아질수록 객체의 응집도가 낮아지기 때문에, 마틴 파울러는 대부분의 클래스가 인스턴스 변수 하나만으로 일을 하는 것이 마땅하지만, 가끔 두개의 변수가 필요한 경우가 있다고 했다. 클래스는 하나의 프로퍼티를 유지하고 관리 하는 것, 두개의 독립된 변수를 조율하는 것의 두가지 경우로 나뉜다고....

프로퍼티를 2개만 쓰는건 굉장히 엄격하지만, 최대한 객체를 쪼개는게 목적이라는 규칙이다.

근데 swift의 경우에는 사실 struct와 enum이라는 다른 종류의 객체도 있기 때문에 프리온보딩에서는 3개까지만으로 제한하는게 좋다고 한다.

 

규칙 8. 일급 컬렉션 사용

일급 컬렉션이란 단하나의 컬렉션만 프로퍼티로 가지는 타입을 말한다. 예를 들어 우리가 어떤 객체에 배열 타입의 프로퍼티를 만들었다고 하자. 하지만 그 배열에는 선입선출을 적용해야 한다. 하지만, 그 프로퍼티를 가만히 놔두면 사용하는 사람마다 insert(_:at:), remove(at:)와 같은 의도하지 않은 메서드를 사용할것이 뻔하다! 그러므로 프로퍼티를 은닉화하고 캡슐화하여 이 프로퍼티를 접근하고 제어할 수 있는 메서드를 구현하자. 그러면 이 프로퍼티는 일반적인 배열이 아니라 선입선출만 가능한 배열이 될 수 있다!

 

규칙 9. getter/setter사용 금지

객체지향 프로그래밍 1에서 말한 것처럼 "객체가 일을 하도록 시켜라!" 즉, 캡슐화를 잘 해서 프로퍼티에 직접 값을 넣거나 수정하지말고, 객체가 직접 접근하고 제어할 수 있도록 해야 한다!

 

마무리

처음부터 SOLID의 5원칙과 생활체조 9가지를 적용하기는 쉽지 않다. 그러므로 한번에 하나씩! 차근차근 내코드에 적용하면서 리팩토링해보고 앞으로 쓰는 코드들에서 규칙을 되새기는게 중요할 것 같다.

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기