Swift에서의 객체지향 프로그래밍: 애플의 현대적 OOP 세계로 풍덩! 🍎

안녕하세요 여러분! 오늘은 2025년 3월, 봄이 시작되는 이 시점에 Swift의 객체지향 프로그래밍(OOP)에 대해 함께 알아볼게요. 🌸 Swift가 버전 6.0을 향해 달려가는 지금, 객체지향의 개념부터 실전 활용까지 완전 꿀팁 대방출할 예정이니 끝까지 함께해요! ㅋㅋㅋ
코딩 실력 업그레이드하고 싶은 분들, Swift로 iOS 앱 개발 시작하고 싶은 분들에게 완전 꿀정보가 될 거예요. 재능넷에서도 Swift 관련 멘토링이 인기 폭발하고 있다던데, 이 글 읽고 나면 여러분도 Swift 고수 재능 판매자가 될 수 있을지도...? 👀✨
📚 목차
- 객체지향 프로그래밍(OOP)이란 뭐야?
- Swift에서의 클래스와 구조체: 닮은 듯 다른 두 형제
- 프로퍼티와 메서드: 객체의 특성과 행동
- 상속과 다형성: OOP의 꽃
- 프로토콜과 익스텐션: Swift의 특별한 OOP 기능
- 접근 제어와 캡슐화: 안전한 코드 작성법
- Swift의 메모리 관리와 ARC
- 실전 예제: 미니 앱 구조 설계하기
- Swift 6.0에서의 OOP 변화 (2025년 최신 업데이트)
1. 객체지향 프로그래밍(OOP)이란 뭐야? 🤔
객체지향 프로그래밍... 뭔가 어렵게 들리죠? 근데 실생활에서 이미 우리는 객체지향적으로 생각하고 있어요! 예를 들어볼게요.
여러분이 좋아하는 아이폰을 생각해보세요. 아이폰은 하나의 '객체'예요. 이 객체는:
- 속성(Properties): 색상, 용량, 화면 크기, 배터리 잔량
- 기능(Methods): 전화하기, 사진 찍기, 앱 실행하기
- 상태(State): 켜짐/꺼짐, 충전 중/미충전
이런 것들을 가지고 있죠! 이처럼 실제 세계의 '물건'이나 '개념'을 코드로 표현한 것이 바로 객체지향 프로그래밍의 핵심이에요.
객체지향 프로그래밍의 4가지 핵심 원칙, 진짜 중요하니까 기억해두세요! 👇
1️⃣ 캡슐화 (Encapsulation)
객체의 데이터(속성)와 기능(메서드)을 하나로 묶고, 외부에서 직접 접근하지 못하게 숨기는 것. 마치 캡슐약처럼 내용물을 보호하는 거죠! 정보 은닉(Information Hiding)의 개념도 여기에 포함돼요.
2️⃣ 상속 (Inheritance)
기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 것. 부모 클래스에서 자식 클래스로 특성이 전달되는 개념이에요. 코드 재사용성이 엄청 높아져요!
3️⃣ 다형성 (Polymorphism)
같은 이름의 메서드가 다른 기능을 할 수 있다는 개념. "다양한 형태를 가질 수 있다"는 뜻이에요. 오버라이딩(Overriding)과 오버로딩(Overloading)이 대표적인 예시죠.
4️⃣ 추상화 (Abstraction)
복잡한 시스템에서 핵심적인 개념이나 기능을 간추려내는 것. 불필요한 세부 사항은 숨기고 중요한 것만 보여주는 거예요. 인터페이스에 집중할 수 있게 해줍니다.
이 네 가지 원칙은 Swift뿐만 아니라 Java, C++, Python 같은 다른 객체지향 언어에서도 똑같이 적용돼요. 근데 Swift는 이런 기본 원칙에 더해서 프로토콜 지향 프로그래밍(POP)이라는 특별한 개념도 추가했어요. 이건 나중에 더 자세히 알아볼게요! 😉
2. Swift에서의 클래스와 구조체: 닮은 듯 다른 두 형제 👯♂️
Swift에서 객체를 만들 때 가장 많이 사용하는 두 가지 타입이 바로 클래스(Class)와 구조체(Struct)예요. 둘 다 비슷해 보이지만 실제론 완전 다른 특성을 가지고 있어요. 헷갈리는 분들 많으니까 확실히 짚고 넘어가자구요! 🧐
자, 이제 코드로 직접 비교해볼까요? 같은 개념을 클래스와 구조체로 각각 구현해보면 이렇게 됩니다:
📱 클래스 예제
class Phone {
var model: String
var color: String
var batteryLevel: Int
init(model: String, color: String, batteryLevel: Int = 100) {
self.model = model
self.color = color
self.batteryLevel = batteryLevel
}
func makeCall(to person: String) {
print("\(person)에게 전화 거는 중...")
batteryLevel -= 5
}
deinit {
print("\(model) 객체가 메모리에서 해제됩니다.")
}
}
💳 구조체 예제
struct CreditCard {
var number: String
var holderName: String
var expiryDate: String
var cvv: String
// 멤버와이즈 이니셜라이저 자동 생성됨!
mutating func updateExpiryDate(to newDate: String) {
expiryDate = newDate
}
}
🔄 참조 타입 vs 값 타입 차이점 살펴보기
// 클래스 사용 예제 (참조 타입)
let iPhone = Phone(model: "iPhone 16 Pro", color: "Titanium")
let myPhone = iPhone // 참조가 복사됨
myPhone.color = "Space Black"
print(iPhone.color) // "Space Black" 출력 (원본이 변경됨)
// 구조체 사용 예제 (값 타입)
var myCard = CreditCard(number: "1234-5678-9012-3456", holderName: "홍길동",
expiryDate: "12/28", cvv: "123")
var friendCard = myCard // 값이 복사됨
friendCard.holderName = "김철수"
print(myCard.holderName) // "홍길동" 출력 (원본은 그대로)
Swift에서는 구조체를 더 선호하는 편이에요. 애플의 공식 가이드라인에서도 특별한 이유가 없다면 구조체를 먼저 사용하라고 권장하고 있어요. 왜냐하면:
- 값 타입이라 예측 가능한 동작을 보장해요
- 메모리 관리가 더 효율적이에요
- 스레드 안전성이 더 높아요
- 2025년 현재 Swift 컴파일러 최적화가 구조체에 더 잘 적용돼요
그래도 클래스가 필요한 상황도 있어요:
- 상속이 필요할 때
- 참조 타입이 필요할 때 (여러 곳에서 같은 인스턴스를 공유해야 할 때)
- 소멸자(deinit)가 필요할 때
- Objective-C와 상호 운용성이 필요할 때
이런 차이점을 잘 이해하고 상황에 맞게 선택하는 게 중요해요! 요즘 재능넷에서 Swift 멘토링 받으시는 분들도 이 부분에서 많이 헷갈려 하시더라구요. 근데 이제 여러분은 완전 정복! ㅋㅋㅋ 👍
3. 프로퍼티와 메서드: 객체의 특성과 행동 🏃♀️
객체지향 프로그래밍에서 객체는 프로퍼티(속성)와 메서드(행동)로 구성돼요. Swift에서는 이 두 가지를 정말 다양하게 활용할 수 있어요!
🔖 프로퍼티 종류
1. 저장 프로퍼티 (Stored Properties)
가장 기본적인 프로퍼티로, 클래스나 구조체 인스턴스의 일부로 저장되는 상수나 변수예요.
struct Person {
let name: String // 상수 저장 프로퍼티
var age: Int // 변수 저장 프로퍼티
lazy var biography: String = { // 지연 저장 프로퍼티
// 복잡한 초기화 로직
return "\(name)의 전기..."
}()
}
2. 연산 프로퍼티 (Computed Properties)
실제 값을 저장하지 않고, getter와 선택적으로 setter를 통해 다른 프로퍼티를 기반으로 값을 계산해요.
struct Circle {
var radius: Double
// 연산 프로퍼티
var area: Double {
get {
return Double.pi * radius * radius
}
}
// getter와 setter가 모두 있는 연산 프로퍼티
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
}
3. 프로퍼티 옵저버 (Property Observers)
프로퍼티 값의 변화를 관찰하고 반응하는 코드를 추가할 수 있어요.
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("곧 \(totalSteps)에서 \(newValue)로 변경될 예정입니다.")
}
didSet {
if totalSteps > oldValue {
print("\(totalSteps - oldValue)걸음 추가되었습니다!")
}
}
}
}
4. 타입 프로퍼티 (Type Properties)
인스턴스가 아닌 타입 자체에 속하는 프로퍼티예요. static 키워드로 선언해요.
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
// 사용 방법
print(SomeStructure.storedTypeProperty) // "Some value." 출력
🛠 메서드 종류
1. 인스턴스 메서드 (Instance Methods)
특정 타입의 인스턴스에 속한 함수로, 인스턴스의 프로퍼티에 접근하고 수정할 수 있어요.
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
2. 타입 메서드 (Type Methods)
타입 자체에 속하는 메서드로, static 또는 class 키워드로 선언해요.
class SomeClass {
// 상속 불가능한 타입 메서드
static func someTypeMethod() {
print("타입 메서드 호출됨")
}
// 상속 가능한 타입 메서드 (클래스에서만 사용 가능)
class func anotherTypeMethod() {
print("상속 가능한 타입 메서드 호출됨")
}
}
// 사용 방법
SomeClass.someTypeMethod() // "타입 메서드 호출됨" 출력
3. 변경 메서드 (Mutating Methods)
구조체나 열거형에서 self를 변경할 수 있는 메서드예요. mutating 키워드를 사용해요.
struct Point {
var x = 0.0, y = 0.0
// 구조체의 프로퍼티를 변경하는 메서드는 mutating 키워드 필요
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("현재 위치: (\(somePoint.x), \(somePoint.y))")
// "현재 위치: (3.0, 4.0)" 출력
🎮 재미있는 사실!
Swift 5.9부터는 매크로를 사용해서 프로퍼티와 메서드 생성을 자동화할 수 있게 되었어요. 2025년 현재 Swift 6.0에서는 이 기능이 더욱 강화되어 @Observable 매크로를 사용하면 SwiftUI에서 상태 관리가 훨씬 편해졌답니다! 이런 최신 기능들을 배우고 싶다면 재능넷에서 Swift 전문가를 찾아보는 것도 좋은 방법이에요. 😉
4. 상속과 다형성: OOP의 꽃 🌸
객체지향 프로그래밍의 가장 강력한 특징 중 하나가 바로 상속(Inheritance)과 다형성(Polymorphism)이에요. 이 개념들을 이해하면 코드를 더 효율적으로 구성할 수 있어요!
👨👦 상속 (Inheritance)
Swift에서 상속은 클래스에서만 가능해요. 한 클래스가 다른 클래스의 특성(프로퍼티, 메서드 등)을 물려받는 것을 말합니다.
상속을 사용하면 코드 중복을 줄이고 계층적인 관계를 표현할 수 있어요. 위 다이어그램을 코드로 표현해볼게요:
// 기본 클래스 (부모 클래스)
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "시속 \(currentSpeed)km로 이동 중"
}
func makeNoise() {
// 기본 구현은 아무 소리도 내지 않음
}
}
// 자식 클래스 (Car)
class Car: Vehicle {
var gear = 1
override func makeNoise() {
print("부릉부릉! 🏎️")
}
}
// 자식 클래스 (Bicycle)
class Bicycle: Vehicle {
override func makeNoise() {
print("따르릉! 🔔")
}
}
// 손자 클래스 (ElectricCar)
class ElectricCar: Car {
var batteryLevel = 100
override func makeNoise() {
print("웅... (조용히) 🔋")
}
}
상속의 주요 특징:
- 오버라이딩(Overriding): 자식 클래스에서 부모 클래스의 메서드, 프로퍼티, 서브스크립트를 재정의할 수 있어요.
override
키워드를 사용해요. - super: 자식 클래스에서 부모 클래스의 메서드, 프로퍼티, 서브스크립트에 접근할 때 사용해요.
- final: 클래스, 메서드, 프로퍼티, 서브스크립트 앞에 붙여서 상속/오버라이딩을 방지할 수 있어요.
🦄 다형성 (Polymorphism)
다형성은 "여러 형태를 가질 수 있는 능력"이에요. Swift에서는 주로 상속과 프로토콜을 통해 다형성을 구현해요.
다형성 예제:
// 다형성 활용 예제
let vehicles: [Vehicle] = [
Car(),
Bicycle(),
ElectricCar()
]
// 각 객체는 자신의 타입에 맞는 makeNoise() 메서드를 호출함
for vehicle in vehicles {
vehicle.makeNoise()
}
// 출력:
// 부릉부릉! 🏎️
// 따르릉! 🔔
// 웅... (조용히) 🔋
위 예제에서 vehicles
배열은 Vehicle
타입의 배열이지만, 실제로는 다양한 자식 클래스의 인스턴스를 담고 있어요. 각 객체는 makeNoise()
메서드를 호출할 때 자신의 클래스에 맞는 구현을 실행하는데, 이것이 바로 다형성의 핵심이에요!
다형성의 장점은 코드를 더 유연하고 확장 가능하게 만든다는 거예요. 새로운 Vehicle
타입(예: Motorcycle
, Truck
등)을 추가해도 기존 코드를 변경할 필요 없이 시스템에 통합할 수 있어요.
💡 실무 팁!
상속은 강력하지만 남용하면 코드가 복잡해질 수 있어요. Swift에서는 상속보다 컴포지션(Composition)과 프로토콜을 더 선호하는 경향이 있어요. "상속보다 컴포지션"이라는 원칙을 기억하세요! 특히 2025년 현재 Swift 생태계에서는 프로토콜 지향 프로그래밍이 더 주목받고 있어요.
5. 프로토콜과 익스텐션: Swift의 특별한 OOP 기능 ✨
Swift의 객체지향 프로그래밍에서 가장 특별한 부분이 바로 프로토콜(Protocols)과 익스텐션(Extensions)이에요. 이 두 가지는 Swift를 다른 언어와 차별화하는 핵심 기능이죠!
📜 프로토콜 (Protocols)
프로토콜은 특정 기능이나 요구사항의 청사진(blueprint)을 정의해요. 클래스, 구조체, 열거형이 이 프로토콜을 채택(adopt)하면 프로토콜에서 요구하는 기능을 반드시 구현해야 해요.
프로토콜 기본 예제:
// 프로토콜 정의
protocol Chargeable {
var batteryLevel: Int { get set }
func charge()
func showBatteryStatus()
}
// 프로토콜 채택 및 구현
struct SmartPhone: Chargeable {
var batteryLevel: Int = 0
func charge() {
print("충전 중... ⚡")
batteryLevel = 100
}
func showBatteryStatus() {
print("현재 배터리: \(batteryLevel)%")
}
}
// 다른 타입도 같은 프로토콜 채택 가능
class Laptop: Chargeable {
var batteryLevel: Int = 50
func charge() {
print("노트북 충전 중... 🔌")
batteryLevel = 100
}
func showBatteryStatus() {
print("노트북 배터리: \(batteryLevel)%")
}
}
프로토콜의 강력한 점은 타입 추상화를 가능하게 한다는 거예요. 구체적인 타입보다 프로토콜에 의존하면 코드가 더 유연해져요:
// 프로토콜 타입으로 다양한 객체 다루기
func chargeDevice(_ device: Chargeable) {
device.charge()
device.showBatteryStatus()
}
let myPhone = SmartPhone()
let myLaptop = Laptop()
chargeDevice(myPhone)
chargeDevice(myLaptop)
프로토콜의 고급 기능:
- 프로토콜 상속: 프로토콜은 다른 프로토콜을 상속할 수 있어요.
- 프로토콜 컴포지션: 여러 프로토콜을 조합해서 사용할 수 있어요 (A & B).
- 프로토콜 익스텐션: 프로토콜에 기본 구현을 제공할 수 있어요.
- 타입으로서의 프로토콜: 변수, 상수, 함수 파라미터 등의 타입으로 사용할 수 있어요.
🔌 익스텐션 (Extensions)
익스텐션은 기존 타입(클래스, 구조체, 열거형, 프로토콜)에 새로운 기능을 추가하는 방법이에요. 심지어 원본 소스 코드에 접근할 수 없는 타입도 확장할 수 있어요!
익스텐션 기본 예제:
// String 타입에 기능 추가하기
extension String {
// 문자열이 이메일 형식인지 확인하는 메서드 추가
func isValidEmail() -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self)
}
// 문자열의 첫 글자만 대문자로 변환하는 계산 프로퍼티 추가
var capitalized: String {
return prefix(1).uppercased() + dropFirst()
}
}
// 사용 예
let myEmail = "swift@example.com"
print(myEmail.isValidEmail()) // true 출력
let name = "swift"
print(name.capitalized) // "Swift" 출력
익스텐션으로 할 수 있는 일들:
1. 계산 프로퍼티 추가
기존 타입에 새로운 계산 프로퍼티를 추가할 수 있어요.
2. 메서드 추가
인스턴스 메서드와 타입 메서드를 추가할 수 있어요.
3. 이니셜라이저 추가
새로운 초기화 방법을 제공할 수 있어요.
4. 서브스크립트 추가
타입에 새로운 서브스크립트를 정의할 수 있어요.
5. 중첩 타입 추가
클래스, 구조체, 열거형 내부에 새로운 중첩 타입을 추가할 수 있어요.
🚀 프로토콜 지향 프로그래밍 (Protocol-Oriented Programming)
Swift는 "프로토콜 지향 프로그래밍"이라는 패러다임을 강조해요. 이는 상속보다 프로토콜과 익스텐션을 조합하여 코드를 구성하는 방식이에요.
프로토콜 지향 프로그래밍 예제:
// 프로토콜 정의
protocol Identifiable {
var id: String { get }
}
// 프로토콜 익스텐션으로 기본 구현 제공
extension Identifiable {
func identify() {
print("ID: \(id)")
}
}
// 구조체에서 프로토콜 채택
struct User: Identifiable {
var id: String
var name: String
}
// 클래스에서도 같은 프로토콜 채택
class Product: Identifiable {
var id: String
var price: Double
init(id: String, price: Double) {
self.id = id
self.price = price
}
}
// 사용 예
let user = User(id: "user123", name: "홍길동")
let product = Product(id: "prod456", price: 99.9)
user.identify() // "ID: user123" 출력
product.identify() // "ID: prod456" 출력
이 방식의 장점은:
- 값 타입(구조체, 열거형)에서도 코드 재사용이 가능해요
- 다중 상속과 유사한 효과를 얻을 수 있어요
- 타입 안전성이 높아져요
- 테스트하기 쉬워져요
2025년 현재 Swift 개발 트렌드는 확실히 프로토콜 지향 프로그래밍 쪽으로 기울고 있어요. 재능넷에서도 Swift 관련 멘토링이나 프로젝트를 찾아보면 POP 방식을 많이 활용하고 있답니다! 😊
6. 접근 제어와 캡슐화: 안전한 코드 작성법 🔒
객체지향 프로그래밍의 중요한 원칙 중 하나인 캡슐화(Encapsulation)를 Swift에서는 접근 제어(Access Control)를 통해 구현해요. 이를 통해 코드의 세부 구현을 숨기고 안전하게 관리할 수 있어요.
🔐 Swift의 접근 수준
Swift는 5가지 접근 수준을 제공해요 (가장 제한적인 것부터 나열):
1. private
선언된 범위 내에서만 접근 가능. 같은 타입의 익스텐션에서도 접근 불가.
2. fileprivate
정의된 소스 파일 내에서만 접근 가능.
3. internal (기본값)
정의된 모듈 내에서만 접근 가능. 별도 지정이 없으면 이 수준이 적용됨.
4. public
어디서든 접근 가능하지만, 다른 모듈에서 상속/오버라이드 불가.
5. open
어디서든 접근 가능하며, 다른 모듈에서도 상속/오버라이드 가능. 클래스와 클래스 멤버에만 적용 가능.
접근 제어 예제:
// BankAccount 클래스 예제
open class BankAccount {
public let accountNumber: String
public var accountHolder: String
private var _balance: Double
// 계산 프로퍼티로 잔액 접근 제공
public var balance: Double {
get {
return _balance
}
}
public init(accountNumber: String, accountHolder: String, initialBalance: Double) {
self.accountNumber = accountNumber
self.accountHolder = accountHolder
self._balance = initialBalance
}
// 입금 메서드
public func deposit(amount: Double) {
if amount > 0 {
_balance += amount
print("\(amount)원이 입금되었습니다. 현재 잔액: \(_balance)원")
}
}
// 출금 메서드
public func withdraw(amount: Double) -> Bool {
if amount > 0 && _balance >= amount {
_balance -= amount
print("\(amount)원이 출금되었습니다. 현재 잔액: \(_balance)원")
return true
} else {
print("출금 실패: 잔액 부족 또는 유효하지 않은 금액")
return false
}
}
// 내부 구현용 메서드
fileprivate func applyInterest(rate: Double) {
let interest = _balance * rate / 100
_balance += interest
print("이자 \(interest)원이 추가되었습니다. 현재 잔액: \(_balance)원")
}
// 상속 가능한 메서드
open func getAccountInfo() -> String {
return "계좌번호: \(accountNumber), 예금주: \(accountHolder), 잔액: \(_balance)원"
}
}
위 예제에서:
- -
_balance
는private
으로 선언되어 외부에서 직접 접근/수정할 수 없어요. - -
balance
계산 프로퍼티를 통해 잔액을 읽을 수만 있어요 (읽기 전용). - -
deposit()
과withdraw()
메서드를 통해서만 잔액을 변경할 수 있어요. - -
applyInterest()
는fileprivate
으로 같은 파일 내에서만 호출 가능해요. - -
getAccountInfo()
는open
으로 선언되어 다른 모듈에서도 오버라이드할 수 있어요.
💡 접근 제어 모범 사례
- 최소 권한의 원칙: 필요한 것만 공개하고 나머지는 숨기세요.
- 변수는 가능한
private
으로 선언하고, 필요한 경우에만 접근자/설정자를 제공하세요. - API를 설계할 때는
public
과open
의 차이를 신중하게 고려하세요. - 내부 구현 세부 사항은
private
또는fileprivate
으로 숨기세요.
2025년 현재 Swift 개발에서는 접근 제어를 통한 캡슐화가 더욱 중요해졌어요. 특히 여러 개발자가 함께 작업하는 대규모 프로젝트나 라이브러리를 개발할 때 접근 제어를 잘 활용하면 코드의 안정성과 유지보수성이 크게 향상돼요. 재능넷에서 Swift 프로젝트를 의뢰하실 때도 이런 부분을 잘 고려하는 개발자를 찾으시면 좋을 것 같아요! 😊
7. Swift의 메모리 관리와 ARC 🧠
객체지향 프로그래밍에서 메모리 관리는 정말 중요한 주제예요. Swift는 ARC(Automatic Reference Counting)라는 시스템을 사용해서 메모리를 자동으로 관리해요. 이건 개발자의 부담을 크게 줄여주지만, 완전히 이해하고 있어야 메모리 누수(memory leak)를 방지할 수 있어요!
🔄 ARC란 무엇인가요?
ARC는 클래스 인스턴스가 더 이상 필요하지 않을 때 자동으로 메모리에서 해제하는 시스템이에요. 인스턴스에 대한 참조 카운트를 추적하고, 참조 카운트가 0이 되면 메모리를 해제해요.
ARC 기본 예제:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 인스턴스가 생성되었습니다.")
}
deinit {
print("\(name) 인스턴스가 메모리에서 해제되었습니다.")
}
}
// 참조 카운트 변화 살펴보기
var reference1: Person?
var reference2: Person?
var reference3: Person?
// Person 인스턴스 생성 (참조 카운트: 1)
reference1 = Person(name: "홍길동")
// 다른 변수에서도 같은 인스턴스 참조 (참조 카운트: 3)
reference2 = reference1
reference3 = reference1
// 참조 제거 (참조 카운트: 2)
reference1 = nil
// 참조 제거 (참조 카운트: 1)
reference2 = nil
// 마지막 참조 제거 (참조 카운트: 0, 메모리 해제)
reference3 = nil
// "홍길동 인스턴스가 메모리에서 해제되었습니다." 출력
⚠️ 순환 참조 (Reference Cycles)
ARC의 가장 큰 문제점은 순환 참조가 발생할 수 있다는 거예요. 두 인스턴스가 서로를 강하게 참조하면 참조 카운트가 절대 0이 되지 않아 메모리 누수가 발생해요.
순환 참조 예제:
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) 인스턴스가 생성되었습니다.")
}
deinit {
print("\(name) 인스턴스가 메모리에서 해제되었습니다.")
}
}
class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
print("\(unit) 인스턴스가 생성되었습니다.")
}
deinit {
print("\(unit) 인스턴스가 메모리에서 해제되었습니다.")
}
}
// 순환 참조 생성
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
// 상호 참조 설정
john?.apartment = unit4A
unit4A?.tenant = john
// 참조 제거 시도
john = nil
unit4A = nil
// deinit이 호출되지 않음 (메모리 누수 발생!)
위 코드에서는 Person
과 Apartment
인스턴스가 서로를 참조하고 있어요. 변수 john
과 unit4A
를 nil
로 설정해도 두 인스턴스는 여전히 서로를 참조하고 있기 때문에 메모리에서 해제되지 않아요.
🔨 순환 참조 해결 방법
Swift에서는 순환 참조를 해결하기 위한 두 가지 방법을 제공해요:
1. 약한 참조 (Weak References)
weak
키워드를 사용해 참조 카운트를 증가시키지 않는 참조를 만들 수 있어요. 약한 참조는 참조하는 인스턴스가 메모리에서 해제되면 자동으로 nil
이 돼요. 따라서 항상 옵셔널 타입이어야 해요.
class Apartment {
let unit: String
weak var tenant: Person? // weak 키워드 사용
init(unit: String) {
self.unit = unit
print("\(unit) 인스턴스가 생성되었습니다.")
}
deinit {
print("\(unit) 인스턴스가 메모리에서 해제되었습니다.")
}
}
2. 미소유 참조 (Unowned References)
unowned
키워드를 사용해 참조 카운트를 증가시키지 않는 참조를 만들 수 있어요. 미소유 참조는 참조하는 인스턴스가 항상 자신보다 오래 존재한다고 가정해요. 참조하는 인스턴스가 메모리에서 해제되면 위험한 상태가 되므로 주의해야 해요.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) 인스턴스가 생성되었습니다.")
}
deinit {
print("\(name) 인스턴스가 메모리에서 해제되었습니다.")
}
}
class CreditCard {
let number: String
unowned let customer: Customer // unowned 키워드 사용
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("카드 번호 \(number) 인스턴스가 생성되었습니다.")
}
deinit {
print("카드 번호 \(number) 인스턴스가 메모리에서 해제되었습니다.")
}
}
언제 weak vs unowned를 사용해야 할까요?
- - weak: 참조하는 인스턴스가 먼저 메모리에서 해제될 수 있을 때 사용해요. (예: 델리게이트 패턴)
- - unowned: 참조하는 인스턴스가 항상 자신보다 오래 존재할 때 사용해요. (예: 부모-자식 관계에서 자식이 부모를 참조할 때)
🔄 클로저에서의 순환 참조
클로저에서도 순환 참조가 발생할 수 있어요. 클로저는 자신이 정의된 컨텍스트의 변수를 캡처하는데, 이때 클래스 인스턴스가 클로저를 프로퍼티로 가지고 있고, 그 클로저가 인스턴스를 캡처하면 순환 참조가 발생해요.
클로저에서의 순환 참조 해결:
class HTMLElement {
let name: String
let text: String
// 클로저를 저장하는 프로퍼티
lazy var asHTML: () -> String = { [weak self] in
// [weak self] 캡처 리스트를 사용해 약한 참조로 self를 캡처
guard let self = self else { return "" }
return "<\(self.name)>\(self.text)\(self.name)>"
}
init(name: String, text: String) {
self.name = name
self.text = text
}
deinit {
print("\(name) 인스턴스가 메모리에서 해제되었습니다.")
}
}
// 사용 예
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello, Swift!")
print(paragraph!.asHTML())
// 참조 제거
paragraph = nil // "p 인스턴스가 메모리에서 해제되었습니다." 출력
클로저에서 순환 참조를 방지하기 위해 캡처 리스트(Capture List)를 사용해요. [weak self]
또는 [unowned self]
를 클로저의 시작 부분에 추가하면 돼요.
💡 메모리 관리 팁
- 클래스 설계 시 참조 관계를 신중하게 고려하세요.
- 디버깅 도구(Xcode의 Memory Graph Debugger)를 활용해 메모리 누수를 찾으세요.
- 델리게이트 패턴에서는 거의 항상
weak
를 사용하세요. - 클로저에서
self
를 사용할 때는 캡처 리스트를 고려하세요. - 2025년 현재 Swift 6.0에서는 메모리 관리를 위한 더 많은 도구가 제공되고 있으니 최신 문서를 참고하세요!
8. 실전 예제: 미니 앱 구조 설계하기 📱
지금까지 배운 Swift의 객체지향 프로그래밍 개념을 활용해서 간단한 음악 플레이어 앱의 구조를 설계해볼게요! 이 예제를 통해 실제로 OOP 원칙들이 어떻게 적용되는지 볼 수 있을 거예요.
🎵 음악 플레이어 앱 구조
이제 각 컴포넌트를 코드로 구현해볼게요! 🧑💻
1. 모델 (Models)
// Song 모델
struct Song: Identifiable {
let id: UUID = UUID()
let title: String
let artist: String
let albumName: String
let duration: TimeInterval
let artworkURL: URL?
var isFavorite: Bool = false
}
// Album 모델
struct Album: Identifiable {
let id: UUID = UUID()
let title: String
let artist: String
let releaseYear: Int
let artworkURL: URL?
var songs: [Song]
}
// Artist 모델
struct Artist: Identifiable {
let id: UUID = UUID()
let name: String
let bio: String?
let imageURL: URL?
var albums: [Album]
}
// Playlist 모델
class Playlist: Identifiable, ObservableObject {
let id: UUID = UUID()
let name: String
let createdBy: String
let createdDate: Date
@Published var songs: [Song]
init(name: String, createdBy: String, songs: [Song] = []) {
self.name = name
self.createdBy = createdBy
self.createdDate = Date()
self.songs = songs
}
func addSong(_ song: Song) {
songs.append(song)
}
func removeSong(at index: Int) {
guard index >= 0 && index < songs.count else { return }
songs.remove(at: index)
}
}
2. 서비스 (Services)
// 음악 플레이어 서비스 프로토콜
protocol MusicPlayerServiceProtocol {
var currentSong: Song? { get }
var isPlaying: Bool { get }
var currentTime: TimeInterval { get }
func play(_ song: Song)
func pause()
func resume()
func stop()
func next()
func previous()
}
// 음악 플레이어 서비스 구현
class MusicPlayerService: MusicPlayerServiceProtocol, ObservableObject {
@Published private(set) var currentSong: Song?
@Published private(set) var isPlaying: Bool = false
@Published private(set) var currentTime: TimeInterval = 0
private var timer: Timer?
private var queue: [Song] = []
private var currentIndex: Int = 0
func play(_ song: Song) {
currentSong = song
isPlaying = true
currentTime = 0
startTimer()
print("재생 중: \(song.title) - \(song.artist)")
}
func pause() {
isPlaying = false
stopTimer()
print("일시정지됨")
}
func resume() {
if currentSong != nil {
isPlaying = true
startTimer()
print("재생 재개")
}
}
func stop() {
isPlaying = false
currentSong = nil
currentTime = 0
stopTimer()
print("정지됨")
}
func next() {
guard !queue.isEmpty else { return }
currentIndex = (currentIndex + 1) % queue.count
play(queue[currentIndex])
}
func previous() {
guard !queue.isEmpty else { return }
currentIndex = (currentIndex - 1 + queue.count) % queue.count
play(queue[currentIndex])
}
func setQueue(_ songs: [Song]) {
queue = songs
currentIndex = 0
}
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self, self.isPlaying, let song = self.currentSong else { return }
self.currentTime += 1
// 노래가 끝나면 다음 곡으로
if self.currentTime >= song.duration {
self.next()
}
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
deinit {
stopTimer()
}
}
// 음악 라이브러리 서비스
class MusicLibraryService {
private(set) var songs: [Song] = []
private(set) var albums: [Album] = []
private(set) var artists: [Artist] = []
private(set) var playlists: [Playlist] = []
func fetchLibrary() async throws {
// 실제로는 API나 로컬 DB에서 데이터를 가져오는 로직
// 여기서는 더미 데이터로 대체
try await Task.sleep(nanoseconds: 1_000_000_000) // 1초 지연
// 더미 데이터 생성
let dummySongs = createDummySongs()
let dummyAlbums = createDummyAlbums(with: dummySongs)
let dummyArtists = createDummyArtists(with: dummyAlbums)
// 메인 스레드에서 UI 업데이트
await MainActor.run {
self.songs = dummySongs
self.albums = dummyAlbums
self.artists = dummyArtists
self.playlists = [
Playlist(name: "내가 좋아하는 노래", createdBy: "나", songs: Array(dummySongs.prefix(5))),
Playlist(name: "드라이브 음악", createdBy: "나", songs: Array(dummySongs.suffix(3)))
]
}
}
// 더미 데이터 생성 메서드들...
private func createDummySongs() -> [Song] {
// 더미 Song 객체들 생성
return []
}
private func createDummyAlbums(with songs: [Song]) -> [Album] {
// 더미 Album 객체들 생성
return []
}
private func createDummyArtists(with albums: [Album]) -> [Artist] {
// 더미 Artist 객체들 생성
return []
}
}
3. 뷰모델 (ViewModels)
// 플레이어 뷰모델
class PlayerViewModel: ObservableObject {
@Published var currentSongTitle: String = ""
@Published var currentArtist: String = ""
@Published var currentAlbum: String = ""
@Published var isPlaying: Bool = false
@Published var currentTime: TimeInterval = 0
@Published var duration: TimeInterval = 0
@Published var progress: Float = 0
private let playerService: MusicPlayerServiceProtocol
private var cancellables = Set<anycancellable>()
init(playerService: MusicPlayerServiceProtocol) {
self.playerService = playerService
// 서비스의 상태 변화 관찰
if let service = playerService as? MusicPlayerService {
service.$currentSong
.sink { [weak self] song in
guard let song = song else {
self?.resetUI()
return
}
self?.updateUI(with: song)
}
.store(in: &cancellables)
service.$isPlaying
.assign(to: &$isPlaying)
service.$currentTime
.sink { [weak self] time in
guard let self = self else { return }
self.currentTime = time
if self.duration > 0 {
self.progress = Float(time / self.duration)
}
}
.store(in: &cancellables)
}
}
func playSong(_ song: Song) {
playerService.play(song)
}
func togglePlayPause() {
if isPlaying {
playerService.pause()
} else {
playerService.resume()
}
}
func nextSong() {
playerService.next()
}
func previousSong() {
playerService.previous()
}
private func updateUI(with song: Song) {
currentSongTitle = song.title
currentArtist = song.artist
currentAlbum = song.albumName
duration = song.duration
}
private func resetUI() {
currentSongTitle = ""
currentArtist = ""
currentAlbum = ""
duration = 0
progress = 0
}
}</anycancellable>
4. SwiftUI 뷰 (간단한 예시)
import SwiftUI
struct PlayerView: View {
@ObservedObject var viewModel: PlayerViewModel
var body: some View {
VStack(spacing: 20) {
// 앨범 아트워크
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 300, height: 300)
.overlay(
Text("앨범 아트")
.font(.title)
.foregroundColor(.gray)
)
// 곡 정보
VStack(spacing: 8) {
Text(viewModel.currentSongTitle.isEmpty ? "재생 중인 곡 없음" : viewModel.currentSongTitle)
.font(.title)
.fontWeight(.bold)
Text(viewModel.currentArtist.isEmpty ? "-" : viewModel.currentArtist)
.font(.title3)
.foregroundColor(.secondary)
Text(viewModel.currentAlbum.isEmpty ? "-" : viewModel.currentAlbum)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
// 진행 상태 바
ProgressView(value: viewModel.progress)
.padding(.horizontal)
// 시간 표시
HStack {
Text(formatTime(viewModel.currentTime))
Spacer()
Text(formatTime(viewModel.duration))
}
.font(.caption)
.padding(.horizontal)
// 컨트롤 버튼
HStack(spacing: 40) {
Button(action: viewModel.previousSong) {
Image(systemName: "backward.fill")
.font(.title)
}
Button(action: viewModel.togglePlayPause) {
Image(systemName: viewModel.isPlaying ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 60))
}
Button(action: viewModel.nextSong) {
Image(systemName: "forward.fill")
.font(.title)
}
}
.padding()
}
.padding()
}
private func formatTime(_ timeInterval: TimeInterval) -> String {
let minutes = Int(timeInterval) / 60
let seconds = Int(timeInterval) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
🏗️ 아키텍처 설명
위 예제에서 사용한 아키텍처는 MVVM(Model-View-ViewModel) 패턴이에요. 이 패턴은 SwiftUI와 함께 사용하기 아주 좋은 패턴이죠!
- Model: 데이터와 비즈니스 로직을 담당해요. 여기서는
Song
,Album
,Artist
,Playlist
구조체/클래스가 해당돼요. - View: UI를 담당해요. SwiftUI 뷰인
PlayerView
가 해당돼요. - ViewModel: 뷰에 필요한 데이터를 모델로부터 가져와 변환하고, 사용자 입력을 처리해요.
PlayerViewModel
이 해당돼요. - Service: 비즈니스 로직과 데이터 접근을 담당하는 계층이에요.
MusicPlayerService
와MusicLibraryService
가 해당돼요.
이 구조의 장점은:
- - 관심사 분리: 각 컴포넌트가 자신의 역할만 담당해요.
- - 테스트 용이성: ViewModel과 Service는 View와 독립적이라 단위 테스트하기 쉬워요.
- - 코드 재사용: Service 계층은 여러 ViewModel에서 재사용할 수 있어요.
- - 유지보수성: UI 변경이 비즈니스 로직에 영향을 주지 않아요.
💡 실전 팁!
실제 앱 개발에서는 의존성 주입(Dependency Injection)을 활용하면 더 유연하고 테스트하기 쉬운 코드를 작성할 수 있어요. 위 예제에서도 PlayerViewModel
이 구체적인 MusicPlayerService
클래스가 아닌 MusicPlayerServiceProtocol
프로토콜에 의존하도록 했어요.
또한, 2025년 현재 Swift 생태계에서는 Combine과 async/await를 함께 활용하는 패턴이 많이 사용되고 있어요. 위 코드에서도 이 두 가지를 함께 사용했답니다!
9. Swift 6.0에서의 OOP 변화 (2025년 최신 업데이트) 🆕
2025년 현재, Swift 6.0이 정식 출시되면서 객체지향 프로그래밍과 관련된 여러 가지 새로운 기능과 개선 사항이 추가되었어요! 이 섹션에서는 최신 변화들을 살펴볼게요.
✨ 주요 새 기능
1. 매크로 시스템 강화
Swift 5.9에서 처음 도입된 매크로 시스템이 Swift 6.0에서 크게 강화되었어요. 이제 더 복잡한 코드 생성과 분석이 가능해졌어요.
// @Observable 매크로 사용 예
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
var age: Int = 0
// 이전에는 ObservableObject 프로토콜과 @Published를 사용해야 했지만,
// 이제 @Observable 하나로 모든 프로퍼티가 자동으로 관찰 가능해졌어요!
}
2. 구조체 상속 (제한적)
Swift 6.0에서는 구조체에서도 제한적인 형태의 '상속'이 가능해졌어요. 정확히는 상속은 아니지만, 구조체 합성(Struct Composition)이라는 새로운 기능을 통해 비슷한 효과를 낼 수 있어요.
// 구조체 합성 예제
struct BaseFeatures {
var id: UUID = UUID()
var createdAt: Date = Date()
func log() {
print("ID: \(id), Created: \(createdAt)")
}
}
struct User {
// 기존 구조체의 모든 프로퍼티와 메서드를 포함
@Compose var base: BaseFeatures
// 추가 프로퍼티
var name: String
var email: String
}
// 사용 예
var user = User(name: "홍길동", email: "hong@example.com")
print(user.id) // BaseFeatures의 id에 접근
user.log() // BaseFeatures의 메서드 호출
3. 향상된 프로토콜 기능
Swift 6.0에서는 프로토콜이 더욱 강력해졌어요. 특히 프로토콜 연관 타입의 기본값과 프로토콜 상속 시 요구사항 재정의 기능이 추가되었어요.
// 프로토콜 연관 타입 기본값
protocol Container {
associatedtype Item = Any
var count: Int { get }
mutating func add(_ item: Item)
}
// 프로토콜 요구사항 재정의
protocol Animal {
func makeSound() -> String
}
protocol Pet: Animal {
// Animal의 makeSound() 요구사항을 재정의하여 기본 구현 제공
func makeSound() -> String { "Some generic pet sound" }
// 새로운 요구사항 추가
var name: String { get }
}
4. 메모리 관리 개선
Swift 6.0에서는 ARC의 성능이 크게 개선되었으며, 특히 순환 참조 감지 기능이 강화되었어요. 컴파일러가 잠재적인 순환 참조를 더 정확하게 감지하고 경고해줘요.
// 컴파일러가 이제 이런 잠재적 순환 참조를 감지해요
class Parent {
var child: Child?
func setupChild() {
let newChild = Child()
newChild.parent = self // 컴파일러 경고: "Potential reference cycle detected"
self.child = newChild
}
}
class Child {
var parent: Parent?
}
5. 속성 래퍼(Property Wrapper) 개선
Swift 6.0에서는 속성 래퍼가 더욱 강력해졌어요. 이제 속성 래퍼 합성과 매개변수가 있는 속성 래퍼 투영이 가능해졌어요.
// 속성 래퍼 합성 예제
@propertyWrapper
struct Clamped<value: comparable> {
private var value: Value
private let range: ClosedRange<value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
init(wrappedValue: Value, range: ClosedRange<value>) {
self.value = min(max(range.lowerBound, wrappedValue), range.upperBound)
self.range = range
}
}
@propertyWrapper
struct Logged<value> {
private var value: Value
var wrappedValue: Value {
get { value }
set {
print("값이 \(value)에서 \(newValue)로 변경됨")
value = newValue
}
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
// 속성 래퍼 합성 사용
class GameSettings {
// Logged와 Clamped를 함께 사용
@Logged @Clamped(range: 0...100)
var volume: Int = 50
}</value></value></value></value:>
⚡ 성능 개선
Swift 6.0에서는 객체지향 프로그래밍 관련 성능도 크게 개선되었어요:
1. 메서드 디스패치 최적화
Swift 6.0에서는 가상 메서드 테이블(Virtual Method Table) 최적화를 통해 메서드 호출 성능이 약 15-20% 향상되었어요. 특히 깊은 상속 계층에서의 성능 향상이 두드러져요.
2. 프로토콜 준수 검사 최적화
프로토콜 준수 검사(Protocol Conformance Checking)가 더 효율적으로 변경되어, 프로토콜을 많이 사용하는 코드의 성능이 향상되었어요.
3. 구조체와 클래스 초기화 성능 개선
구조체와 클래스의 초기화 과정이 최적화되어 객체 생성 속도가 빨라졌어요. 특히 많은 프로퍼티를 가진 복잡한 객체에서 효과가 커요.
🌟 2025년 Swift OOP 모범 사례
Swift 6.0 시대에 맞는 객체지향 프로그래밍 모범 사례를 정리해봤어요:
- 구조체를 기본으로 사용하고, 필요할 때만 클래스를 사용하세요. Swift 6.0에서는 구조체의 기능이 더욱 강화되어 이전보다 더 많은 상황에서 구조체를 사용할 수 있어요.
- 프로토콜과 프로토콜 익스텐션을 적극 활용하세요. 상속보다는 컴포지션과 프로토콜 지향 프로그래밍 방식을 선호하세요.
- @Observable 매크로를 활용하여 상태 관리를 단순화하세요. SwiftUI와 함께 사용할 때 특히 유용해요.
- async/await와 Actor 모델을 활용하여 동시성 코드를 안전하게 작성하세요. 이는 객체지향 프로그래밍과 함께 사용할 때 강력한 시너지를 발휘해요.
- 속성 래퍼를 활용하여 반복되는 패턴을 추상화하세요. Swift 6.0의 향상된 속성 래퍼 기능을 최대한 활용하세요.
🔮 앞으로의 전망
Swift의 객체지향 프로그래밍은 계속해서 진화하고 있어요. 앞으로 예상되는 변화는:
- - 더 강력한 메타프로그래밍 기능: 매크로 시스템이 더욱 발전하여 코드 생성과 분석이 더 쉬워질 거예요.
- - 값 타입과 참조 타입 간의 경계 모호화: 구조체와 클래스의 기능 차이가 점점 줄어들고 있어요.
- - 함수형 프로그래밍과의 통합 강화: 객체지향과 함수형 패러다임의 장점을 결합한 하이브리드 접근 방식이 더 보편화될 거예요.
- - 더 스마트한 컴파일러 최적화: 컴파일러가 객체지향 코드를 더 효율적으로 최적화할 수 있게 될 거예요.
이러한 변화들은 Swift를 더욱 강력하고 유연한 언어로 만들어갈 거예요. 재능넷에서도 이런 최신 트렌드를 따라가는 Swift 개발자들의 수요가 계속 증가하고 있답니다! 😊
마무리: Swift OOP 마스터하기 🏆
여기까지 Swift의 객체지향 프로그래밍에 대해 깊이 있게 알아봤어요! 처음에는 어려워 보일 수 있지만, 기본 개념부터 차근차근 이해하고 실습해보면 충분히 마스터할 수 있어요.
요약하자면:
- Swift는 클래스(참조 타입)와 구조체(값 타입)를 통해 객체지향 프로그래밍을 지원해요
- 프로퍼티와 메서드를 통해 객체의 상태와 행동을 정의할 수 있어요
- 상속과 다형성을 통해 코드 재사용성과 확장성을 높일 수 있어요
- 프로토콜과 익스텐션을 활용한 프로토콜 지향 프로그래밍은 Swift의 특별한 강점이에요
- 접근 제어를 통해 캡슐화를 구현하고 안전한 코드를 작성할 수 있어요
- ARC를 이해하고 순환 참조를 방지하여 메모리 누수를 예방할 수 있어요
- Swift 6.0의 새로운 기능들을 활용하면 더 효율적인 객체지향 코드를 작성할 수 있어요
Swift의 객체지향 프로그래밍은 단순히 기술적인 지식을 넘어 코드를 구조화하고 문제를 해결하는 사고방식이에요. 이 개념들을 잘 이해하고 적용하면 더 유지보수하기 쉽고, 확장 가능하며, 버그가 적은 코드를 작성할 수 있을 거예요!
앞으로도 Swift는 계속 발전할 테니, 공식 문서와 커뮤니티를 통해 최신 트렌드를 따라가는 것이 중요해요. 재능넷에서도 Swift 관련 멘토링이나 프로젝트를 찾아보면 실력 향상에 큰 도움이 될 거예요! 🚀
여러분의 Swift 여정에 행운을 빕니다! 질문이나 피드백이 있으시면 언제든지 댓글로 남겨주세요. 함께 성장해요! 😄
1. 객체지향 프로그래밍(OOP)이란 뭐야? 🤔
객체지향 프로그래밍... 뭔가 어렵게 들리죠? 근데 실생활에서 이미 우리는 객체지향적으로 생각하고 있어요! 예를 들어볼게요.
여러분이 좋아하는 아이폰을 생각해보세요. 아이폰은 하나의 '객체'예요. 이 객체는:
- 속성(Properties): 색상, 용량, 화면 크기, 배터리 잔량
- 기능(Methods): 전화하기, 사진 찍기, 앱 실행하기
- 상태(State): 켜짐/꺼짐, 충전 중/미충전
이런 것들을 가지고 있죠! 이처럼 실제 세계의 '물건'이나 '개념'을 코드로 표현한 것이 바로 객체지향 프로그래밍의 핵심이에요.
객체지향 프로그래밍의 4가지 핵심 원칙, 진짜 중요하니까 기억해두세요! 👇
1️⃣ 캡슐화 (Encapsulation)
객체의 데이터(속성)와 기능(메서드)을 하나로 묶고, 외부에서 직접 접근하지 못하게 숨기는 것. 마치 캡슐약처럼 내용물을 보호하는 거죠! 정보 은닉(Information Hiding)의 개념도 여기에 포함돼요.
2️⃣ 상속 (Inheritance)
기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 것. 부모 클래스에서 자식 클래스로 특성이 전달되는 개념이에요. 코드 재사용성이 엄청 높아져요!
3️⃣ 다형성 (Polymorphism)
같은 이름의 메서드가 다른 기능을 할 수 있다는 개념. "다양한 형태를 가질 수 있다"는 뜻이에요. 오버라이딩(Overriding)과 오버로딩(Overloading)이 대표적인 예시죠.
4️⃣ 추상화 (Abstraction)
복잡한 시스템에서 핵심적인 개념이나 기능을 간추려내는 것. 불필요한 세부 사항은 숨기고 중요한 것만 보여주는 거예요. 인터페이스에 집중할 수 있게 해줍니다.
이 네 가지 원칙은 Swift뿐만 아니라 Java, C++, Python 같은 다른 객체지향 언어에서도 똑같이 적용돼요. 근데 Swift는 이런 기본 원칙에 더해서 프로토콜 지향 프로그래밍(POP)이라는 특별한 개념도 추가했어요. 이건 나중에 더 자세히 알아볼게요! 😉
2. Swift에서의 클래스와 구조체: 닮은 듯 다른 두 형제 👯♂️
Swift에서 객체를 만들 때 가장 많이 사용하는 두 가지 타입이 바로 클래스(Class)와 구조체(Struct)예요. 둘 다 비슷해 보이지만 실제론 완전 다른 특성을 가지고 있어요. 헷갈리는 분들 많으니까 확실히 짚고 넘어가자구요! 🧐
자, 이제 코드로 직접 비교해볼까요? 같은 개념을 클래스와 구조체로 각각 구현해보면 이렇게 됩니다:
📱 클래스 예제
class Phone {
var model: String
var color: String
var batteryLevel: Int
init(model: String, color: String, batteryLevel: Int = 100) {
self.model = model
self.color = color
self.batteryLevel = batteryLevel
}
func makeCall(to person: String) {
print("\(person)에게 전화 거는 중...")
batteryLevel -= 5
}
deinit {
print("\(model) 객체가 메모리에서 해제됩니다.")
}
}
💳 구조체 예제
struct CreditCard {
var number: String
var holderName: String
var expiryDate: String
var cvv: String
// 멤버와이즈 이니셜라이저 자동 생성됨!
mutating func updateExpiryDate(to newDate: String) {
expiryDate = newDate
}
}
🔄 참조 타입 vs 값 타입 차이점 살펴보기
// 클래스 사용 예제 (참조 타입)
let iPhone = Phone(model: "iPhone 16 Pro", color: "Titanium")
let myPhone = iPhone // 참조가 복사됨
myPhone.color = "Space Black"
print(iPhone.color) // "Space Black" 출력 (원본이 변경됨)
// 구조체 사용 예제 (값 타입)
var myCard = CreditCard(number: "1234-5678-9012-3456", holderName: "홍길동",
expiryDate: "12/28", cvv: "123")
var friendCard = myCard // 값이 복사됨
friendCard.holderName = "김철수"
print(myCard.holderName) // "홍길동" 출력 (원본은 그대로)
Swift에서는 구조체를 더 선호하는 편이에요. 애플의 공식 가이드라인에서도 특별한 이유가 없다면 구조체를 먼저 사용하라고 권장하고 있어요. 왜냐하면:
- 값 타입이라 예측 가능한 동작을 보장해요
- 메모리 관리가 더 효율적이에요
- 스레드 안전성이 더 높아요
- 2025년 현재 Swift 컴파일러 최적화가 구조체에 더 잘 적용돼요
그래도 클래스가 필요한 상황도 있어요:
- 상속이 필요할 때
- 참조 타입이 필요할 때 (여러 곳에서 같은 인스턴스를 공유해야 할 때)
- 소멸자(deinit)가 필요할 때
- Objective-C와 상호 운용성이 필요할 때
이런 차이점을 잘 이해하고 상황에 맞게 선택하는 게 중요해요! 요즘 재능넷에서 Swift 멘토링 받으시는 분들도 이 부분에서 많이 헷갈려 하시더라구요. 근데 이제 여러분은 완전 정복! ㅋㅋㅋ 👍
3. 프로퍼티와 메서드: 객체의 특성과 행동 🏃♀️
객체지향 프로그래밍에서 객체는 프로퍼티(속성)와 메서드(행동)로 구성돼요. Swift에서는 이 두 가지를 정말 다양하게 활용할 수 있어요!
🔖 프로퍼티 종류
1. 저장 프로퍼티 (Stored Properties)
가장 기본적인 프로퍼티로, 클래스나 구조체 인스턴스의 일부로 저장되는 상수나 변수예요.
struct Person {
let name: String // 상수 저장 프로퍼티
var age: Int // 변수 저장 프로퍼티
lazy var biography: String = { // 지연 저장 프로퍼티
// 복잡한 초기화 로직
return "\(name)의 전기..."
}()
}
2. 연산 프로퍼티 (Computed Properties)
실제 값을 저장하지 않고, getter와 선택적으로 setter를 통해 다른 프로퍼티를 기반으로 값을 계산해요.
struct Circle {
var radius: Double
// 연산 프로퍼티
var area: Double {
get {
return Double.pi * radius * radius
}
}
// getter와 setter가 모두 있는 연산 프로퍼티
var diameter: Double {
get {
return radius * 2
}
set {
radius = newValue / 2
}
}
}
3. 프로퍼티 옵저버 (Property Observers)
프로퍼티 값의 변화를 관찰하고 반응하는 코드를 추가할 수 있어요.
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("곧 \(totalSteps)에서 \(newValue)로 변경될 예정입니다.")
}
didSet {
if totalSteps > oldValue {
print("\(totalSteps - oldValue)걸음 추가되었습니다!")
}
}
}
}
4. 타입 프로퍼티 (Type Properties)
인스턴스가 아닌 타입 자체에 속하는 프로퍼티예요. static 키워드로 선언해요.
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
// 사용 방법
print(SomeStructure.storedTypeProperty) // "Some value." 출력
🛠 메서드 종류
1. 인스턴스 메서드 (Instance Methods)
특정 타입의 인스턴스에 속한 함수로, 인스턴스의 프로퍼티에 접근하고 수정할 수 있어요.
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
2. 타입 메서드 (Type Methods)
타입 자체에 속하는 메서드로, static 또는 class 키워드로 선언해요.
class SomeClass {
// 상속 불가능한 타입 메서드
static func someTypeMethod() {
print("타입 메서드 호출됨")
}
// 상속 가능한 타입 메서드 (클래스에서만 사용 가능)
class func anotherTypeMethod() {
print("상속 가능한 타입 메서드 호출됨")
}
}
// 사용 방법
SomeClass.someTypeMethod() // "타입 메서드 호출됨" 출력
3. 변경 메서드 (Mutating Methods)
구조체나 열거형에서 self를 변경할 수 있는 메서드예요. mutating 키워드를 사용해요.
struct Point {
var x = 0.0, y = 0.0
// 구조체의 프로퍼티를 변경하는 메서드는 mutating 키워드 필요
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("현재 위치: (\(somePoint.x), \(somePoint.y))")
// "현재 위치: (3.0, 4.0)" 출력
🎮 재미있는 사실!
Swift 5.9부터는 매크로를 사용해서 프로퍼티와 메서드 생성을 자동화할 수 있게 되었어요. 2025년 현재 Swift 6.0에서는 이 기능이 더욱 강화되어 @Observable 매크로를 사용하면 SwiftUI에서 상태 관리가 훨씬 편해졌답니다! 이런 최신 기능들을 배우고 싶다면 재능넷에서 Swift 전문가를 찾아보는 것도 좋은 방법이에요. 😉
4. 상속과 다형성: OOP의 꽃 🌸
객체지향 프로그래밍의 가장 강력한 특징 중 하나가 바로 상속(Inheritance)과 다형성(Polymorphism)이에요. 이 개념들을 이해하면 코드를 더 효율적으로 구성할 수 있어요!
👨👦 상속 (Inheritance)
Swift에서 상속은 클래스에서만 가능해요. 한 클래스가 다른 클래스의 특성(프로퍼티, 메서드 등)을 물려받는 것을 말합니다.
상속을 사용하면 코드 중복을 줄이고 계층적인 관계를 표현할 수 있어요. 위 다이어그램을 코드로 표현해볼게요:
// 기본 클래스 (부모 클래스)
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "시속 \(currentSpeed)km로 이동 중"
}
func makeNoise() {
// 기본 구현은 아무 소리도 내지 않음
}
}
// 자식 클래스 (Car)
class Car: Vehicle {
var gear = 1
override func makeNoise() {
print("부릉부릉! 🏎️")
}
}
// 자식 클래스 (Bicycle)
class Bicycle: Vehicle {
override func makeNoise() {
print("따르릉! 🔔")
}
}
// 손자 클래스 (ElectricCar)
class ElectricCar: Car {
var batteryLevel = 100
override func makeNoise() {
print("웅... (조용히) 🔋")
}
}
상속의 주요 특징:
- 오버라이딩(Overriding): 자식 클래스에서 부모 클래스의 메서드, 프로퍼티, 서브스크립트를 재정의할 수 있어요.
override
키워드를 사용해요. - super: 자식 클래스에서 부모 클래스의 메서드, 프로퍼티, 서브스크립트에 접근할 때 사용해요.
- final: 클래스, 메서드, 프로퍼티, 서브스크립트 앞에 붙여서 상속/오버라이딩을 방지할 수 있어요.
🦄 다형성 (Polymorphism)
다형성은 "여러 형태를 가질 수 있는 능력"이에요. Swift에서는 주로 상속과 프로토콜을 통해 다형성을 구현해요.
다형성 예제:
// 다형성 활용 예제
let vehicles: [Vehicle] = [
Car(),
Bicycle(),
ElectricCar()
]
// 각 객체는 자신의 타입에 맞는 makeNoise() 메서드를 호출함
for vehicle in vehicles {
vehicle.makeNoise()
}
// 출력:
// 부릉부릉! 🏎️
// 따르릉! 🔔
// 웅... (조용히) 🔋
위 예제에서 vehicles
배열은 Vehicle
타입의 배열이지만, 실제로는 다양한 자식 클래스의 인스턴스를 담고 있어요. 각 객체는 makeNoise()
메서드를 호출할 때 자신의 클래스에 맞는 구현을 실행하는데, 이것이 바로 다형성의 핵심이에요!
다형성의 장점은 코드를 더 유연하고 확장 가능하게 만든다는 거예요. 새로운 Vehicle
타입(예: Motorcycle
, Truck
등)을 추가해도 기존 코드를 변경할 필요 없이 시스템에 통합할 수 있어요.
💡 실무 팁!
상속은 강력하지만 남용하면 코드가 복잡해질 수 있어요. Swift에서는 상속보다 컴포지션(Composition)과 프로토콜을 더 선호하는 경향이 있어요. "상속보다 컴포지션"이라는 원칙을 기억하세요! 특히 2025년 현재 Swift 생태계에서는 프로토콜 지향 프로그래밍이 더 주목받고 있어요.
5. 프로토콜과 익스텐션: Swift의 특별한 OOP 기능 ✨
Swift의 객체지향 프로그래밍에서 가장 특별한 부분이 바로 프로토콜(Protocols)과 익스텐션(Extensions)이에요. 이 두 가지는 Swift를 다른 언어와 차별화하는 핵심 기능이죠!
📜 프로토콜 (Protocols)
프로토콜은 특정 기능이나 요구사항의 청사진(blueprint)을 정의해요. 클래스, 구조체, 열거형이 이 프로토콜을 채택(adopt)하면 프로토콜에서 요구하는 기능을 반드시 구현해야 해요.
프로토콜 기본 예제:
// 프로토콜 정의
protocol Chargeable {
var batteryLevel: Int { get set }
func charge()
func showBatteryStatus()
}
// 프로토콜 채택 및 구현
struct SmartPhone: Chargeable {
var batteryLevel: Int = 0
func charge() {
print("충전 중... ⚡")
batteryLevel = 100
}
func showBatteryStatus() {
print("현재 배터리: \(batteryLevel)%")
}
}
// 다른 타입도 같은 프로토콜 채택 가능
class Laptop: Chargeable {
var batteryLevel: Int = 50
func charge() {
print("노트북 충전 중... 🔌")
batteryLevel = 100
}
func showBatteryStatus() {
print("노트북 배터리: \(batteryLevel)%")
}
}
프로토콜의 강력한 점은 타입 추상화를 가능하게 한다는 거예요. 구체적인 타입보다 프로토콜에 의존하면 코드가 더 유연해져요:
// 프로토콜 타입으로 다양한 객체 다루기
func chargeDevice(_ device: Chargeable) {
device.charge()
device.showBatteryStatus()
}
let myPhone = SmartPhone()
let myLaptop = Laptop()
chargeDevice(myPhone)
chargeDevice(myLaptop)
프로토콜의 고급 기능:
- 프로토콜 상속: 프로토콜은 다른 프로토콜을 상속할 수 있어요.
- 프로토콜 컴포지션: 여러 프로토콜을 조합해서 사용할 수 있어요 (A & B).
- 프로토콜 익스텐션: 프로토콜에 기본 구현을 제공할 수 있어요.
- 타입으로서의 프로토콜: 변수, 상수, 함수 파라미터 등의 타입으로 사용할 수 있어요.
🔌 익스텐션 (Extensions)
익스텐션은 기존 타입(클래스, 구조체, 열거형, 프로토콜)에 새로운 기능을 추가하는 방법이에요. 심지어 원본 소스 코드에 접근할 수 없는 타입도 확장할 수 있어요!
익스텐션 기본 예제:
// String 타입에 기능 추가하기
extension String {
// 문자열이 이메일 형식인지 확인하는 메서드 추가
func isValidEmail() -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self)
}
// 문자열의 첫 글자만 대문자로 변환하는 계산 프로퍼티 추가
var capitalized: String {
return prefix(1).uppercased() + dropFirst()
}
}
// 사용 예
let myEmail = "swift@example.com"
print(myEmail.isValidEmail()) // true 출력
let name = "swift"
print(name.capitalized) // "Swift" 출력
익스텐션으로 할 수 있는 일들:
1. 계산 프로퍼티 추가
기존 타입에 새로운 계산 프로퍼티를 추가할 수 있어요.
2. 메서드 추가
인스턴스 메서드와 타입 메서드를 추가할 수 있어요.
3. 이니셜라이저 추가
새로운 초기화 방법을 제공할 수 있어요.
4. 서브스크립트 추가
타입에 새로운 서브스크립트를 정의할 수 있어요.
5. 중첩 타입 추가
클래스, 구조체, 열거형 내부에 새로운 중첩 타입을 추가할 수 있어요.
🚀 프로토콜 지향 프로그래밍 (Protocol-Oriented Programming)
Swift는 "프로토콜 지향 프로그래밍"이라는 패러다임을 강조해요. 이는 상속보다 프로토콜과 익스텐션을 조합하여 코드를 구성하는 방식이에요.
프로토콜 지향 프로그래밍 예제:
// 프로토콜 정의
protocol Identifiable {
var id: String { get }
}
// 프로토콜 익스텐션으로 기본 구현 제공
extension Identifiable {
func identify() {
print("ID: \(id)")
}
}
// 구조체에서 프로토콜 채택
struct User: Identifiable {
var id: String
var name: String
}
// 클래스에서도 같은 프로토콜 채택
class Product: Identifiable {
var id: String
var price: Double
init(id: String, price: Double) {
self.id = id
self.price = price
}
}
// 사용 예
let user = User(id: "user123", name: "홍길동")
let product = Product(id: "prod456", price: 99.9)
user.identify() // "ID: user123" 출력
product.identify() // "ID: prod456" 출력
이 방식의 장점은:
- 값 타입(구조체, 열거형)에서도 코드 재사용이 가능해요
- 다중 상속과 유사한 효과를 얻을 수 있어요
- 타입 안전성이 높아져요
- 테스트하기 쉬워져요
2025년 현재 Swift 개발 트렌드는 확실히 프로토콜 지향 프로그래밍 쪽으로 기울고 있어요. 재능넷에서도 Swift 관련 멘토링이나 프로젝트를 찾아보면 POP 방식을 많이 활용하고 있답니다! 😊
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개