[iOS 2주차] Swift: class와 class의 관계 / class의 단일책임원칙 / 객체지향 프로그래밍

DoyleHWorks·2024년 10월 28일
4

과제를 진행하는데 클래스 간의 관계? 클래스의 단일원칙책임? 의존성역전원칙?? 모르는 개념들이 너무 많아서..

궁금한 내용들을 ChatGPT에게 구와악 물어보았다.

// ChatGPT 4o Prompts:
- swift에서 class와 class의 관계를 맺는다는 게 무슨 뜻이지
- 의존성에 대해서는 설명을 안한 것 같은데
- 연관관계에서 말하는 Dog은 상속 예시코드에서 나오는 Dog을 말하는거야?
- 지금 말한 점에 착안해서, 처음부터 끝까지 혼동의 여지 없이 잘 설명해봐

클래스 관계 설명

Swift에서 클래스 간의 관계를 설정하는 것은 소프트웨어 구조를 설계하고, 객체 간의 상호작용을 규정하는 중요한 요소이다.
Swift에서의 클래스 관계는 상속, 의존성, 연관 관계, 집합 관계, 복합 관계의 형태로 나타날 수 있다.

1. 상속 (Inheritance)

상속은 한 클래스가 다른 클래스의 속성과 동작을 물려받아 사용하는 관계이다.
이를 통해 클래스 간의 계층 구조를 설정할 수 있으며, 코드의 재사용성과 유연성을 높인다.

예시

class Animal {
    func makeSound() {
        print("Animal makes a sound")
    }
}

class Cat: Animal {
    override func makeSound() {
        print("Meow")
    }
}

let cat = Cat()
cat.makeSound()  // 출력: Meow
  • Animal: 부모 클래스(슈퍼클래스)로 기본 동작인 makeSound() 메소드를 정의함
  • Cat: 자식 클래스(서브클래스)로 Animal을 상속받아, makeSound 메소드를 재정의(override)함

이처럼 상속을 통해 클래스는 다른 클래스를 기반으로 확장할 수 있으며, 재사용이 용이해진다.

2. 의존성 (Dependency)

의존성은 한 클래스가 다른 클래스의 인스턴스를 필요로 하는 일시적인 관계이다.
보통 메소드의 매개변수로 다른 클래스의 인스턴스를 전달받아 사용한다.
이 관계는 두 클래스가 강하게 결합되지 않고, 호출하는 클래스가 특정 작업을 위해 일시적으로 다른 클래스를 사용하는 경우에 해당한다.

예시

class Printer {
    func printDocument(_ document: String) {
        print("Printing: \(document)")
    }
}

class OfficeWorker {
    func performTask(printer: Printer) {
        printer.printDocument("Report")
    }
}

let printer = Printer()
let worker = OfficeWorker()
worker.performTask(printer: printer)
  • Printer: 문서를 출력하는 기능을 가진 클래스
  • OfficeWorker: performTask 메소드에서 Printer 클래스의 인스턴스를 전달받아 사용함
    하지만 OfficeWorkerPrinter의 인스턴스를 소유하지 않으며, 메소드 내에서만 일시적으로 사용함

이처럼 의존성 관계는 두 클래스 간의 결합도를 낮춰 유연한 설계를 가능하게 한다.

3. 연관 관계 (Association)

연관 관계는 한 클래스가 다른 클래스의 인스턴스를 프로퍼티로 가지는 관계이다.
이 경우 두 클래스는 독립적으로 존재하지만, 한 클래스가 다른 클래스의 인스턴스를 참조하여 협력한다.

예시

class Pet {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Person {
    var name: String
    var pet: Pet? // Person 클래스가 Pet 클래스를 참조하는 연관 관계
    
    init(name: String, pet: Pet?) {
        self.name = name
        self.pet = pet
    }
}

let myPet = Pet(name: "Buddy")
let person = Person(name: "John", pet: myPet)
  • Person: pet이라는 프로퍼티를 통해 Pet 클래스의 인스턴스를 참조함
    그러나 PersonPet은 서로 독립적으로 존재할 수 있음
  • Pet: 독립적으로 존재하는 클래스이며, Person과의 관계는 참조에 불과함

연관 관계는 두 객체가 서로의 생명 주기에 영향을 주지 않는 독립적인 관계를 유지하면서 협력할 수 있도록 한다.

4. 집합 관계 (Aggregation)

집합 관계는 한 클래스가 다른 클래스의 인스턴스를 포함하는 관계이다.
이 경우 포함된 클래스와 포함하는 클래스는 독립적인 생명 주기를 가지며, 서로의 존재 여부에 영향을 주지 않는다.

예시

class TeamMember {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Team {
    var members: [TeamMember] // 여러 TeamMember 인스턴스를 포함하는 집합 관계
    
    init(members: [TeamMember]) {
        self.members = members
    }
}

let member1 = TeamMember(name: "Alice")
let member2 = TeamMember(name: "Bob")
let team = Team(members: [member1, member2])
  • Team: 여러 TeamMember 인스턴스를 프로퍼티로 가지며, 팀을 구성함
  • TeamMember: 독립적으로 존재할 수 있으며, 팀이 없어져도 팀 멤버는 여전히 존재할 수 있음

집합 관계는 클래스 간의 결합이 느슨하며, 포함된 클래스의 독립성을 보장한다.

5. 복합 관계 (Composition)

복합 관계는 한 클래스가 다른 클래스의 인스턴스를 소유하는 관계로, 두 클래스의 생명 주기가 밀접하게 연결되어 있다.
포함하는 클래스가 소멸되면 포함된 클래스도 함께 소멸된다.

예시

class Engine {
    func start() {
        print("Engine starts")
    }
}

class Car {
    let engine = Engine() // Car 클래스가 Engine 인스턴스를 소유
    
    func start() {
        engine.start()
    }
}
  • Car: engine 프로퍼티를 통해 Engine 인스턴스를 소유함
    만약 Car 인스턴스가 소멸되면, 그 안의 Engine 인스턴스도 함께 소멸됨
  • Engine: 독립적으로 생성되지 않으며, Car의 구성 요소로서만 존재함

복합 관계는 클래스 간의 생명 주기를 강하게 결합하여, 포함된 클래스가 포함하는 클래스에 의존하도록 만든다.


이처럼 Swift에서의 클래스 간 관계는 각기 다른 결합의 강도와 생명 주기를 기반으로 정의된다.
이들을 명확히 이해하고 활용하면, 더 유연하고 확장 가능한 소프트웨어 구조를 설계할 수 있다.


// ChatGPT 4o Prompt:
클래스의 책임: 단일책임원칙 에 대해서 설명해줘. 다른 책임도 있다면 그것도 포함해서.

클래스의 SOLID 원칙

클래스의 책임과 관련하여 가장 중요한 원칙은 단일 책임 원칙(Single Responsibility Principle, SRP)이다.
이 원칙은 객체 지향 설계에서 필수적인 개념이며, SOLID 원칙의 첫 번째 요소이기도 하다.

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

SRP는 클래스가 하나의 책임만 가져야 하며, 그 책임은 변경의 이유가 하나여야 한다는 원칙이다.
이는 클래스가 오직 하나의 기능이나 역할에 집중해야 한다는 뜻이다.
만약 하나의 클래스에 여러 책임이 혼재되어 있다면, 이를 분리하여 각 책임을 별도의 클래스로 분리해야 한다.

예시

class User {
    var name: String
    var email: String
    
    init(name: String, email: String) {
        self.name = name
        self.email = email
    }
}

class UserDatabase {
    func saveUser(user: User) {
        print("\(user.name) has been saved to the database.")
    }
}

class UserNotifier {
    func sendWelcomeEmail(user: User) {
        print("Sending welcome email to \(user.email).")
    }
}

위 예시에서는 User 클래스를 정의하고, 그에 따라 데이터베이스 저장알림 전송이라는 두 가지 책임을 분리했다.
만약 단일 책임 원칙을 따르지 않고 모든 기능을 User 클래스에 넣었다면, 수정 사항이 발생할 때마다 여러 부분에 영향을 미쳤을 것이다.

SRP의 이점

  • 유지보수성: 변경이 필요한 부분만 집중해서 수정할 수 있으므로, 오류를 줄이고 유지보수를 용이하게 한다.
  • 재사용성: 단일 책임을 가진 클래스는 더 쉽게 다른 코드에서도 재사용될 수 있다.
  • 테스트 용이성: 한 클래스에 집중된 단일 책임 덕분에 테스트가 더 단순해지고 명확해진다.

2. 개방-폐쇄 원칙 (Open-Closed Principle, OCP)

클래스는 확장에 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙이다.
즉, 새로운 기능이 필요할 때 기존 코드를 변경하지 않고 확장하여 구현할 수 있어야 한다.
이를 통해 코드의 안정성을 높이고, 유지보수 시 오류가 발생할 가능성을 줄인다.

예시

protocol Payment {
    func processPayment(amount: Double)
}

class CreditCardPayment: Payment {
    func processPayment(amount: Double) {
        print("Processing credit card payment of \(amount)")
    }
}

class PayPalPayment: Payment {
    func processPayment(amount: Double) {
        print("Processing PayPal payment of \(amount)")
    }
}

class PaymentProcessor {
    func execute(paymentMethod: Payment, amount: Double) {
        paymentMethod.processPayment(amount: amount)
    }
}

위 예시에서 PaymentProcessor 클래스는 Payment 프로토콜을 준수하는 모든 클래스(CreditCardPayment, PayPalPayment 등)를 사용할 수 있다.
새로운 결제 방식을 추가하려면 Payment 프로토콜을 구현하는 새로운 클래스를 만들면 되며, 기존 코드인 PaymentProcessor는 수정하지 않아도 된다.
이를 통해 개방-폐쇄 원칙을 구현할 수 있다.

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

LSP는 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙이다.
즉, 서브클래스가 부모 클래스의 기능을 확장하거나 재정의할 때, 그 행동이 부모 클래스의 기대에 부합해야 한다.
이 원칙을 지키지 않으면 다형성의 이점을 제대로 활용할 수 없게 된다.

예시

class Bird {
    func fly() {
        print("Flying")
    }
}

class Penguin: Bird {
    override func fly() {
        print("Penguins cannot fly")
    }
}

let bird: Bird = Penguin()
bird.fly()  // 출력: "Penguins cannot fly"

위의 예시는 LSP를 위반한 사례이다.
Bird 클래스는 fly 메소드를 가지고 있으며, 이를 상속받는 Penguin 클래스는 "펭귄은 날 수 없다"는 메시지를 출력한다.
PenguinBird를 대체할 수 없으므로, LSP를 따르지 않는다.
이 경우, Bird의 서브클래스가 아닌, 비행이 불가능한 조류를 위한 별도의 클래스를 정의하는 것이 좋다.

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

ISP는 클라이언트가 자신이 사용하지 않는 메소드에 의존하지 않아야 한다는 원칙이다.
즉, 하나의 거대한 인터페이스보다는 클라이언트에 맞게 작은 인터페이스 여러 개로 나누는 것이 좋다.

예시

protocol Printer {
    func printDocument()
}

protocol Scanner {
    func scanDocument()
}

class MultiFunctionPrinter: Printer, Scanner {
    func printDocument() {
        print("Printing document...")
    }

    func scanDocument() {
        print("Scanning document...")
    }
}

class SimplePrinter: Printer {
    func printDocument() {
        print("Printing document...")
    }
}

MultiFunctionPrinter는 프린터와 스캐너 기능을 모두 제공하지만, SimplePrinter는 프린트 기능만 제공한다.
이를 통해 ISP를 준수하며, 각 클래스가 필요한 기능만을 제공하도록 설계할 수 있다.

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

DIP는 고수준 모듈(상위 수준의 정책을 정의하는 클래스)이 저수준 모듈(세부 사항을 구현하는 클래스)에 의존하지 않고, 둘 다 추상화에 의존해야 한다는 원칙이다.
이를 통해 모듈 간 결합을 줄이고, 유연한 구조를 유지할 수 있다.

예시

protocol Notification {
    func send(message: String)
}

class EmailNotification: Notification {
    func send(message: String) {
        print("Sending email: \(message)")
    }
}

class SMSNotification: Notification {
    func send(message: String) {
        print("Sending SMS: \(message)")
    }
}

class NotificationSender {
    var notificationMethod: Notification
    
    init(notificationMethod: Notification) {
        self.notificationMethod = notificationMethod
    }
    
    func notify(message: String) {
        notificationMethod.send(message: message)
    }
}

let emailNotification = EmailNotification()
let sender = NotificationSender(notificationMethod: emailNotification)
sender.notify(message: "Hello, User!")

NotificationSenderNotification 프로토콜에 의존하며, 구체적인 구현체(예: EmailNotification 또는 SMSNotification)에는 의존하지 않는다.
이를 통해 새로운 알림 방식이 추가되어도 NotificationSender를 수정하지 않아도 됩니다.


이처럼 클래스의 책임과 관련된 다양한 원칙들은 객체 지향 설계에서 중요한 역할을 하며, 클래스 간의 결합도를 낮추고, 유연성과 확장성을 높이기 위해 필수적인 개념들이다. 이를 준수하면 더 유지보수하기 쉬운 코드를 작성할 수 있다.


// ChatGPT 4o Prompt:
객체지향 프로그래밍에 대해서 간략하게 설명해봐

객체지향 프로그래밍

객체지향 프로그래밍(Object-Oriented Programming, OOP)객체(Object)라는 개념을 중심으로 프로그램을 설계하고 구현하는 프로그래밍 패러다임이다.
OOP는 코드의 재사용성, 유지보수성, 유연성을 높이기 위해 객체들을 정의하고, 이 객체들이 서로 상호작용하며 기능을 수행하도록 설계한다.

객체지향 프로그래밍의 주요 개념

  1. 객체(Object): 현실 세계의 개체(예: 사람, 자동차, 책 등)를 소프트웨어 안에서 표현한 것으로, 데이터(속성)와 행동(메소드)을 포함한다.
  2. 클래스(Class): 객체를 생성하기 위한 설계도이다. 클래스는 객체가 가져야 할 속성(프로퍼티)과 동작(메소드)을 정의한다. 객체는 클래스를 통해 만들어지며, 이를 인스턴스(instance)라고도 한다.

OOP의 네 가지 핵심 원칙

  1. 추상화(Abstraction): 복잡한 시스템을 단순화하여 중요한 부분만을 모델링한다. 객체의 속성과 행동을 정의하여 필요하지 않은 세부 사항은 숨긴다.
  2. 캡슐화(Encapsulation): 객체의 상태(데이터)를 보호하고, 객체의 내부 구현을 외부에서 직접 접근하지 못하도록 한다. 대신 메소드를 통해서만 객체의 상태를 조작할 수 있도록 한다.
  3. 상속(Inheritance): 하나의 클래스가 다른 클래스의 특성과 동작을 물려받아 재사용하고 확장하는 방식이다. 이를 통해 코드의 재사용성을 높이고, 중복을 줄일 수 있다.
  4. 다형성(Polymorphism): 같은 동작을 서로 다른 형태로 구현할 수 있도록 하는 기능이다. 예를 들어, 같은 이름의 메소드가 클래스에 따라 다른 동작을 하도록 한다. 다형성은 상속과 함께 사용되어 객체의 다양성과 유연성을 제공한다.

객체지향 프로그래밍의 장점

  • 코드의 재사용성: 클래스와 객체를 통해 코드의 재사용성을 극대화할 수 있다.
  • 유지보수성: 코드가 모듈화되어 있어, 각 객체나 클래스의 수정이 다른 부분에 미치는 영향을 최소화할 수 있다.
  • 유연성과 확장성: 새로운 기능이나 객체를 쉽게 추가하고 확장할 수 있다.

간단한 예시

class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func makeSound() {
        print("Animal makes a sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Bark")
    }
}

let myDog = Dog(name: "Buddy")
myDog.makeSound()  // 출력: Bark

위의 예시에서는 Animal이라는 클래스가 있고, 이를 상속받아 Dog 클래스를 정의한다.
Dog 클래스는 makeSound 메소드를 재정의(override)하여 다형성을 구현한다.


요약하자면, 객체지향 프로그래밍은 객체라는 개념을 통해 복잡한 프로그램을 구조적으로 설계하고, 네 가지 원칙을 활용하여 코드의 재사용성, 유연성, 유지보수성을 높이는 프로그래밍 방식이다.

profile
Reciprocity lies in knowing enough

0개의 댓글