안녕하세요?
오늘은 객체지향의 5가지 원칙인 SOLID 5원칙에 대해 공부해보았답니다.
사실 며칠 걸림 (ㅋ)
(오랜만에 이런 공부해보니까 대학생 된 거 같고 좋았어요)
(그 땐 왜 하기 싫었을까요?)
제 애증의 단짝 Gemini가 그려준 엉망진창 이미지,,, 하
아무튼 여러 문서들을 보면서 아래와 같이 간단하게 정리해 보았어요
그럼 시작~
SOLID 원칙 정리
1. SRP (Single Responsibility Principle) - 단일 책임 원칙
- 개념
: 하나의 클래스(객체)는 단 하나의 책임(기능)만 가져야 함.
→ 여기서 책임(Responsibility)은 "변경해야 하는 이유"와 연결됨.
→ 즉, 하나의 클래스는 단 하나의 이유로만 변경되어야 함. - 오해하기 쉬운 점:
- "하나의 클래스에는 하나의 함수만 있어야 한다"는 것이 아님! -> 처음에 이렇게 오해함 ㅎ
- 여러 개의 메서드가 있어도 괜찮음. 단, 모두 같은 책임(기능)을 수행하는 데 필요한 것들이어야 함.
- 예시
- ❌ SRP 위반 코드 (여러 책임이 한 클래스에 섞여 있음)
class UserManager {
func saveUser() { /* 데이터베이스 저장 로직 */ }
func validateUser() { /* 유효성 검사 로직 */ }
func sendEmail() { /* 이메일 전송 로직 */ }
}
UserManager에게 너무 과도한 임무가 몰렸어요
- ✅ SRP 준수 코드 (책임을 분리함)
class UserRepository {
func saveUser() { /* 데이터베이스 저장 로직 */ }
}
class UserValidator {
func validateUser() { /* 유효성 검사 로직 */ }
}
class EmailService {
func sendEmail() { /* 이메일 전송 로직 */ }
}
각 클래스는 하나의 책임만 하도록 나눠줍니다!
2. OCP (Open Closed Principle) - 개방 폐쇄 원칙
- 개념:
- 확장에는 열려 있고, 수정에는 닫혀 있어야 함
- 즉, 새로운 기능을 추가할 때 기존 코드를 직접 수정하지 않고, 확장을 통해 추가해야 함.
- 왜 필요한가?
- 기존 코드 수정 없이 새로운 기능을 추가할 수 있어야 유지보수가 쉬움.
- 예시
- ❌ OCP 위반 코드 (기능 추가 시 기존 코드 수정 필요)
class PaymentProcessor {
func process(paymentType: String) {
if paymentType == "CreditCard" {
print("신용카드 결제 처리")
} else if paymentType == "PayPal" {
print("PayPal 결제 처리")
}
}
}
이렇게 구성하면 나중에 새로운 결제 수단 추가 할 때 아주 골치 아파지겠죠
- ✅ OCP 준수 코드 (새로운 결제 방식 추가 시 기존 코드 수정 불필요)
protocol Payment {
func process()
}
class CreditCardPayment: Payment {
func process() {
print("신용카드 결제 처리")
}
}
class PayPalPayment: Payment {
func process() {
print("PayPal 결제 처리")
}
}
class PaymentProcessor {
func process(payment: Payment) {
payment.process()
}
}
결제 수단을 프로토콜로 만들어서 각 클래스에서 프로토콜 채택하는 방식으로~
3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
- 개념:
- 자식 클래스가 부모 클래스로 대체되어도 정상 작동해야 함.
- 부모 클래스를 사용하는 코드가 자식 클래스로 변경되어도 오류가 발생하면 안 됨.
- 위반 예시 (LSP를 지키지 않는 코드)
class Bird {
func fly() {
print("날아간다!")
}
}
class Penguin: Bird {
override func fly() {
fatalError("펭귄은 날 수 없음!")
}
}
func makeBirdFly(bird: Bird) {
bird.fly()
}
let penguin = Penguin()
makeBirdFly(bird: penguin) // 💥 런타임 에러 발생!
아무래도 펭귄은 날지 못하니깐요...
- 해결 방법 (LSP 준수 코드)
protocol Flyable {
func fly()
}
class Bird {}
class Sparrow: Bird, Flyable {
func fly() {
print("참새가 난다!")
}
}
class Penguin: Bird {
// 날지 못하는 새이므로 fly() 미구현
}
func makeBirdFly(bird: Flyable) {
bird.fly()
}
let sparrow = Sparrow()
makeBirdFly(bird: sparrow) // ✅ 정상 작동
4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
- 개념:
- 인터페이스(프로토콜)를 세분화하여, 필요한 기능만 구현하도록 해야 함.
- 하나의 커다란 인터페이스(프로토콜)보다는 여러 개의 작은 인터페이스로 나누는 것이 좋음.
- 위반 예시 (ISP를 지키지 않는 코드 - 불필요한 메서드 구현 강제됨)
protocol Worker {
func work()
func eat()
}
class Robot: Worker {
func work() {
print("로봇이 일함")
}
func eat() {
fatalError("로봇은 먹지 않음!")
}
}
프로토콜에서도 분리 원칙을 지켜줘야해요
- 해결 방법 (ISP 준수 코드 - 필요한 기능만 구현하도록 인터페이스 분리)
protocol Workable {
func work()
}
protocol Eatable {
func eat()
}
class Robot: Workable {
func work() {
print("로봇이 일함")
}
}
class Human: Workable, Eatable {
func work() {
print("사람이 일함")
}
func eat() {
print("사람이 밥을 먹음")
}
}
5. DIP (Dependency Inversion Principle) - 의존 역전 원칙
- 개념:
- 구체적인 클래스에 의존하지 말고, 프로토콜과 같은 추상화된 인터페이스에 의존해야 함.
- 상위 모듈(고수준)이 하위 모듈(저수준)에 의존하면 안 됨.
- 위반 예시 (DIP를 지키지 않는 코드 - 구체적인 클래스에 의존)
class FileLogger {
func log(message: String) {
print("파일에 로그 저장: \(message)")
}
}
class UserService {
let logger = FileLogger() // ❌ 직접 FileLogger에 의존
func registerUser(name: String) {
logger.log(message: "\(name) 유저 등록됨")
}
}
전 다 이렇게 했었는뎁,,,ㅎ
- 해결 방법 (DIP 준수 코드 - 추상화된 프로토콜에 의존)
protocol Logger {
func log(message: String)
}
class FileLogger: Logger {
func log(message: String) {
print("파일에 로그 저장: \(message)")
}
}
class ConsoleLogger: Logger {
func log(message: String) {
print("콘솔에 로그 출력: \(message)")
}
}
class UserService {
let logger: Logger // ✅ 프로토콜(추상화)에 의존
init(logger: Logger) {
self.logger = logger
}
func registerUser(name: String) {
logger.log(message: "\(name) 유저 등록됨")
}
}
앞으로 이렇게 할게요
📌 한 줄 정리
SRP: 하나의 클래스(객체)는 하나의 책임(기능)만 가져야 함
OCP: 확장에는 열려 있고, 수정에는 닫혀 있어야 함 (기능 추가는 확장으로)
LSP: 자식 클래스가 부모 클래스로 대체되어도 정상 작동해야 함
ISP: 인터페이스(프로토콜)를 작은 단위로 분리해야 함
DIP: 구체적인 클래스가 아니라 프로토콜(추상화된 인터페이스)에 의존해야 함
그러니까~ 하나의 클래스는 하나의 책임만 가지고 있어야하고
구체적인 클래스에 의존하는게 아니라 프로토콜 같은 추상화 인터페이스에 의존해야함
그래야 자식 클래스가 부모 케이스로 변경되어도 정상 작동 원칙이 가능하고
곧 확장에는 열려있고, 수정에는 닫혀있는 코드가 됨~~~
'STUDY > CS' 카테고리의 다른 글
코드 작성 시 표기법에 대해 알아보자ㅏㅏㅏ (0) | 2025.04.18 |
---|---|
순수함수에 대해 R아보자 (0) | 2025.04.18 |
객체지향 프로그래밍에 대해 아라보자! (0) | 2025.03.28 |