소프트웨어의 가치
- 가독성, 커뮤니케이션
- 개발자는 코드를 통해 의사소통을 함. 읽고 이해할 수 없는 코드는 가치가 없음
- 단순성
- 코드는 단순해야 함.
- → 커뮤니케이션에 도움이 되고, 버그 방지, 미래의 확장 용이
- 유연성
- 기존의 코드를 수정하는 데 최대한 적은 시간을 해야 함.
- → 처음부터 유연성있는 코드를 작성하긴 어렵기 때문에, 이를 위해 많은 경험이 바탕이 되어야 함
- 처음엔 단순하게 → 기획 또는 정책의 변경이 발생하면 수정
- 고민해야할 부분 : 이 기획/정책의 변경의 원인은 무엇일지, 이러한 변경이 계속 발생 가능한 일 일지 고려해보기
- 확장성을 고려한 리팩토링을 진행
왜 가독성, 단순성, 유연성이 중요한 가치인 이유는 무엇일까? 가치는 어떤 기준으로 매기는 걸까?
- 비용과 이익을 기준으로!
- 비용이란 시간과 돈 → 주된 비용은 개발자들의 시간 = 연봉이기 때문에 고려해야 함
- 상황에 따라 SOLID를 지키지 않을 수도 있음
- No Silver Bullet : 모든 문제를 해결하는 최고의 선택(답)은 존재하지 않기 때문에 엔지니어는 현재의 상황에서 우선순위를 계산하고, 그에 맞는 최선의 선택을 해야 함. 다만 이때 최선의 선택을 위해 기준을 세워야 함
SOLID
- 객체 지향 프로그래밍을 위한 5가지 원칙
- 객체 지향이란 :프로그램들의 동작을 객체끼리의 상호작용으로 풀어나가는 프로그램
- 객체란 : 기능과 특성을 가지는 실체
- 원칙 : 지켜지면 좋은 규칙 → 좋은 코드, 높은 품질의 코드를 쓰기 위해 사용하자
- 좋은 코드의 기준을 생각하기 위해선 가치와 원칙을 이해야 함
- → 가치는 원칙보다 높은 수준의 개념. 원칙은 가치를 지키기 위해서 존재해야 함
Single Responsibility Principle 단일 책임 원칙 |
모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다. |
Open/Closed Principle 개방 폐쇄 원칙 |
확장 가능하도록 열려있고, 변경에는 닫혀있어야한다. |
Liskov Substitution Principle 리스코프 치환 원칙 |
서브타입은(상속받은) 기본 타입으로 대체가능해야 한다. 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다. |
Interface Segregation Principle 인터페이스 분리 원칙 |
클라이언트 객체는 사용하지 않는 메소드에 의존하면 안된다. 각각의 필요한 인터페이스만 남도록 인터페이스를 분리해라. |
Dependency Inversion Principle 의존관계 역전 원칙 |
상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다. 추상화된 인터페이스에 의존해야한다. |
SRP 단일 책임 원칙(Single Responsibility Principle)
- 소프트웨어의 요소(타입, 모듈, 함수 등)는 응집도 있는 하나의 책임을 갖는다. 타입를 변경해야하는 이유는 단지 응집도여야 한다.
높은 응집도, 낮은 결합도를 추구
결합도 : 서로 다른 타입이 얼마나 연관되어있는지 정도 → 결합도는 낮을 수록 좋다
응집도 : 같은 맥락을 공유하는 책임이 하나의 타입에 몰아져있는지 정도
- 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함
- 클래스의 수정이유는 단 하나여야함
- 하나의 클래스는 하나의 책임을 가져야함
- 하나의 책임이 여러개의 클래스에 나뉘어 있어서도 안됨
- ex) 디자이너에게 개발 시키지 마라..!!
역할 vs 책임
- 역할: 자기가 마땅히 하여야 할 맡은 바 직책이나 임무
- 책임: 맡아서 해야할 임무나 의무 → 변경의 이유! 책임을 잘 이수하냐에 따라 역할이 바뀔 수 있음
- ex) 엘사가 여왕으로 왕국을 잘 다스리면 여왕 / 잘 다스리지 못하면 폭군
OCP 개방-폐쇄 원칙(Open/Closed Principle)
- 소프트웨어 요소(클래스, 모듈, 함수 등)는 확장 가능하도록 열려있고, 변경에는 닫혀있어야 함. 새 기능을 추가할 때 변경하지 말고 새 클래스나 함수를 만들어야 함
- 확장에 열려있다
- → 프로토콜를 준수하는 클래스면 기존의 코드를 수정X → 클래스나 인스턴스가 늘어나도 확장가능
- → 프로토콜이 프로토콜을 채택하면 수직확장도 가능!
- 변경에 닫혀있다
- → 연쇄수정이 일어나는 상황을 방지
- → 기존 코드의 변경은 버그가 일어날 가능성이 높기 때문, 이에 연달아 테스트 코드의 변경도 필요한 상황이 올 수 도 있음
- 객체가 변경될 때는 해당 객체만 바꿔도 동작이 잘되면 OCP를 잘 지킨 것
- 이러한 특성들은 "추상화" 를 통해 이루어질 수 있음
- ex) ErrorExplainable 프로토콜을 사용함으로 여러종류의 에러를 하나로 묶어서 사용하는 방식도 가능
// OCP 준수하지 않은 경우
class Dog {
func makeSound() {
print("멍멍")
}
}
class Cat {
func makeSound() {
print("야옹")
}
}
class Zoo {
var dogs: [Dog] = [Dog(), Dog(), Dog()]
var cats: [Cat] = [Cat(), Cat(), Cat()]
// var birds: ~~ 수정해야함
func makeAllSounds() {
dogs.forEach {
$0.makeSound()
}
cats.forEach {
$0.makeSound()
}
// birds.forEach~~~~ 수정해야함
}
}
// OCP 준수하는 경우
protocol Animal {
func makeSound()
}
class Dog: Animal {
func makeSound() {
print("멍멍")
}
}
class Cat: Animal {
func makeSound() {
print("야옹")
}
}
//class Birds: Animal {~~
//Birdsclass를 추가해도 Zoo에서는 변화가 생기지 않음!
class Zoo {
var animals: [Animal] = []
func makeAllSounds() {
animals.forEach {
$0.makeSound()
}
}
}
디미터의 법칙(Law of Demeter) : 객체의 속성을 가져오지 말고, 객체가 일하도록 시켜라
기존의 프로토콜에 추가하는 식으로 수정할 경우가 필요하다는 생각이 들 때, 정말 이 프로토콜을 수정해야하는 것이 맞는 건지 다시 생각해보기! 꼭 이 프로토콜 안에 들어가야돼? 새로운 타입을 만들어야하는 거 아냐? 아니면 기능을 호출하는 것을 만들어서.. 책임을 갖고 있는 역할을 만들어서 그 역할이 일을 할 수 있도록 값을 주입하는 데 이때 SRP를 지키고자 각 메소드를 나눠서 파라미터에서 값을 받아서 실제 기능을 동작하는 메소드에 넘기는 역할만 하게끔 한다.. 의존성의 주입을 통해서 OCP를 지킨다
LSP 리스코프 치환 원칙(Liskov Substitution Principle)
- 서브타입은(상속받은) 기본 타입으로 대체가능해야 한다. 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다.
- 직사각형-정사각형처럼 자식 클래스는 부모 클래스 동작(의미)를 바꾸지 않는다.
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
- → 부모 클래스의 타입에 자식 클래스의 인스턴스를 넣어도 똑같이 동작(의미)하여야 합니다.
- → ex) 숨쉬다라는 메소드에 밥을 먹는다는 의미를 넣으면 안된다
- → 현실에서는 이미 대상이 존재하고 공통점을 찾아서 일반화해서 범주를 만듦. 코드에서 범주를 만드는 것은 범주를 만들고 세부 범주를 만들어가는 식으로 진행된다.
ISP 인터페이스 분리 원칙(Interface Segregation Principle)
- 클라이언트 객체는 사용하지 않는 메소드에 의존하면 안된다. 각각의 필요한 인터페이스만 남도록 인터페이스를 분리해라.
- → 프로토콜은 기능구현의 명세이며 약속이다(구현을 강제한다)
- → 프로토콜이 커지면 그만큼 한번에 요구되는 구현량이 많아진다. 이런 경우 일부 기능만 필요하다면, 나머진 퇴화(텅 빈 구현, 깡통)시키게 된다.
- 클래스 내에서 사용하지 않는 인터페이스는 구현하지 말아야한다.
- 클라이언트 객체는 사용하지 않는 메소드에 의존하면 안된다.
- 인터페이스가 거대해지는 경우 SRP를 어기는 경우가 생길 수 있고, 해당 인터페이스를 채택해서 사용하는 경우 쓰지 않는 메소드가 있어도 넣어야 하는 경우가 발생할 수 있으니 최대한 인터페이스를 분리하는 것을 권장
- → 인터페이스가 사용되지 않을 기능을 포함한 여러 기능을 가지면 안된다(단일책임을 충족했다는 가정 하)
DIP 의존관계 역전 원칙(Dependency Inversion Principle)
- 상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다. (둘 다 추상화된 인터페이스에 의존해야한다. 추상화는 구체화에 의존하면 안되고 구체화는 추상화에 의존하면 안된다) → 구체적인 것은 쉽게 변하지만, 추상적인 것은 잘 변하지 않기 때문이다. → 규약, 명세(프로토콜/인터페이스)를 지키면 동작이 가능하게끔
- UI : 어떤 사용자가 와도 사용할 수 있게끔 인터페이스를 만든 것
- API : 앱을 어떠한 기기에서도, 어떠한 사용자도 사용가능하게하는 인터페이스
- 상위 계층이 하위 계층에 의존하는 전통적인 의존관계를 역전시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
- 인터페이스 : 프로토콜, 깡통 클래스, 추상 클래스
- → 상위계층 : 인스턴스로 갖고 있거나(연관관계), 의존관계
- 첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야한다.
- 둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
- → 클래스에서 프로토콜을 채택하는 것
- DIP는 ‘상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다’는 객체 지향적 설계의 대원칙을 따른다.
- 상위레벨 모듈은 하위레벨 모듈에 의존하면 안된다.
- 도 모듈은 추상화된 인터페이스(프로토콜)에 의존해야한다.
- 추상화 된 것은 구체적인 것에 의존하면 안되고, 구체적인 것이 추상화된 것에 의존해야한다.
- 하위레벨 모듈이 상위레벨 모듈을 참조하는 것은 되지만 상위레벨 모듈이 하위레벨 모듈을 참조하는 것은 안한는게 좋다. 그런 경우는 제너릭이나 Associate를 사용
- DIP를 만족하면 의존성 주입이라는 기술로 변화를 쉽게 수용할 수 있다.
class APIHandler {
func request() -> Data {
return Data(base64Encoded: "This Data")!
}
}
// LoginService가 상위계층
class LoginService {
let apiHandler: APIHandler = APIHandler()
func login() {
let loginData = apiHandler.request()
print(loginData)
}
}
// 추상화 -> 프로토콜, 내부구현을 하지 않은 것, 두루뭉실
protocol APIHandlerProtocol {
func requestAPI() -> Data
}
// 세부상황, 구체화, 실체화 -> 선언, 정의, 구체적으로 설명가능한 것
class LoginService {
let apiHandler: APIHandlerProtocol
init(apiHandler: APIHandlerProtocol) {
self.apiHandler = apiHandler
}
func login() {
let loginData = apiHandler.requestAPI()
print(loginData)
}
}
class LoginAPI: APIHandlerProtocol {
func requestAPI() -> Data {
return Data(base64Encoded: "User")!
}
}
let loginAPI = LoginAPI()
let loginService = LoginService(apiHandler: loginAPI)
loginService.login()
→ 이렇게 작성하게 되면 LoginService는 기존에 APIHandler에 의존하지 않고 추상화 시킨 객체인 APIHandlerProtocol에 의존하게 됩니다. 그렇기 때문에 APIHandlerProtocol의 구현부는 외부에서 변화에 따라 지정해서 지정해주면 되기 때문에 LoginService는 구현부에 상관없이 좀 더 변화에 민감하지 않은 DIP의 원칙을 지킨 프로그램을 설계할 수 있게 됩니다.
SOLID 원칙을 지키지 않았을 때 발생하는 문제점
- 경직성 : 하나를 바꿀 때 다른것들도 바꿔야해서 시스템을 변경하기 어려움
- 부서지기 쉬움 : 한 부분이 변경되었을 때 다른 한 부분이 영향을 받아서 새로운 오류가 발생 가능
- 부동성 : 다른 시스템이 재사용하기 어려움
- 점착성 : 제대로 작동하기 어려움
- 불필요한 반복 및 복잡성 : 과도한 설계
- 불투명성 : 의도를 파악하기 어려운 혼란스러운 표현
- 변경과 수정에 용이하게 대응할 수 없음
- 수정이 발생할 때마다 어디까지 영향을 미치는지 파악이 어려움
728x90
'TIL' 카테고리의 다른 글
UICollectionView (0) | 2023.12.15 |
---|---|
MarkDown 접기 (0) | 2023.12.11 |
URL Session (0) | 2023.11.30 |
estimatedItemSize / itemSize (0) | 2023.11.30 |
UIApplicationDelegate / UISceneDelegate (0) | 2023.11.24 |