본문 바로가기
스파르타코딩 클럽/사전 캠프

11. 스파르타 코딩클럽 [사전캠프 - Struct와 Class/Protocol]

by UDDT 2025. 2. 8.

 

(스타터 과정이 아닌, 마스터 과정을 정리한 내용)

 

 

기본

Struct와 Class

 

1. Struct(구조체)

     - 값 타입(Value Type). 메모리에서 값만 복사해 사용하므로 독립된 인스턴스(각자의 값을 가지고 있음) 생성.

     - 상속이 불가능함.

     - 주로 데이터를 저장하거나 간단한 로직을 수행할 때 사용

struct StructName {
    property(속성)
    method(메소드)
    initializer(생성자)
    subscript(서브스크립트)
}

/*
property ~ subscript까지를 member라고 부름. member의 요소는 lowcamelCase로 작성
멤버는 필요한만큼 선언하고, 불필요한 요소는 생략해도 됨

property(속성) : Global Scope나 Local Scope에서 선언하면 변수, Type 안에서 선언하면 property
method(메소드) : Global Scope나 Local Scope에서 선언하면 함수, Type 안에서 선언하면 method

Type 선언에서 첫번째 Level에는 선언만 와야함.
*/
struct Person {
    var name: String
    var age: Int
    
    func speak() {
    	print("Hello")
    }
}

let p = Person(name: "Uddt", age: 25)       // 인스턴스 만들기
p.name       // 속성에 접근(Uddt)
p.age        // 속성에 접근(25)

구조체는 '값'만 복사하는 것.

  2. Class(클래스)

     - 참조 타입(Reference Type). 메모리에서 동일한 인스턴스를 여러 곳에서 참조(값을 변경하면 다른 변수에도 영향을 줌).

     - 상속 가능

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

class ClassName {
    property(속성)
    method(메소드)
    initializer(생성자)
    deinitializer(디이니셜라이저)
    subscript(서브스크립트)
}

/*
property ~ subscript까지를 member라고 부름. member의 요소는 lowerCamelCase로 작성
멤버는 필요한만큼 선언하고, 불필요한 요소는 생략해도 됨
*/
class Person {
    var name: String
    var age: Int

    func speak() {
        print("Hello")
    }

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let p = Person(name: "Uddt", age: 25)

클래스는 '메모리'를 참조하는 것. 같은 메모리를 바라보면 값도 같음.



 3. Struct와 Class 비교

특징 Struct Class
메모리 구조 Stack Heap
타입 값 타입(Value Type) 참조 타입(Reference Type)
상속 불가능 가능
성능 성능 우수(메모리 복사가 빠름) 성능 낮음(메모리 참조)
생성자 컴파일러가 자동으로 인식 init을 반드시 써줘야함
인스턴스 독립적 동일한 값을 공유

 

Protocol

  1. Protocol(프로토콜)

     - 특정 기능을 정의한 청사진

     - Struct, Class, Enum(열거형)에서 프로토콜을 채택(adopt)하고 구현(Implement)할 수 있음.

     - 용도 : 코드를 재사용 가능하고 유연하게 설계, 프로토콜 기반의 설계 방식을 가능하게 함.

protocol Greetable {
    func greet() -> String
}

struct Person: Greetable {
    var name: String
    
    func greet() -> String {
        return "Hello, \(name)!"
    }
}

let person = Person(name: "김유저")
print(person.greet()) // "Hello, 김유저!"

 

실습

 Struct 구현하기

이름과 나이를 저장하는 User Struct를 작성하고, 두 명의 독립적인 사용자 인스턴스를 생성해주세요.
두 인스턴스의 값을 변경한 후, 각 인스턴스가 서로 독립적임을 확인하세요.
struct User {
    var name: String
    var age: Int
}

// 두 개의 독립적인 사용자 인스턴스 생성
var user1 = User(name: "이유저", age: 25)
var user2 = User(name: "김유저", age: 30)

// 두 인스턴스의 값을 변경
user1.name = "박유저"
user2.age = 35

// 독립성을 확인
print("name: \(user1.name), age: \(user1.age)")
print("name: \(user2.name), age: \(user2.age)")

// name: 박유저, age: 25
// name: 김유저, age: 35

 

 Class 구현하기

이름과 나이를 저장하는 User Class를 작성하고, 두 개의 사용자 참조를 생성해주세요.
한 참조의 값을 변경한 후, 두 참조가 동일한 값을 공유하는지 확인하세요
class UserClass {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 두 개의 사용자 참조 생성
var user1Inform = UserClass(name: "김유저", age: 25)
var user2Inform = user1Inform

// user1Inform의 이름을 변경
user1Inform.name = "박유저"

// user2Inform도 동일하게 변경되었음
print("1번 유저 name: \(user1Inform.name), 2번 유저 name: \(user2Inform.name)")

// 1번 유저 name: 박유저, 2번 유저 name: 박유저

// 두 참조는 동일한 값을 공유함

 

Protocol 구현하기

greet() 메서드를 포함한 Greetable 프로토콜을 정의합니다.
Person Struct와 Robot Class에서 이 프로토콜을 채택하고 구현하세요.
Person은 이름을 활용하여 인사를 출력, Robot은 고유 ID를 활용해 인사를 출력합니다.
// 프로토콜 선언
protocol Greetable {
    func greet() -> String
}

// struct Type에 프로토콜 Type을 입력하여 함수 사용을 강제
struct People: Greetable {
    var name: String

    func greet() -> String {
        return "Hello, \(name)"
    }
}

// 인스턴트 생성 후 출력
let user1 = People(name: "김유저")
print(user1.greet())

// Hello, 김유저
// 프로토콜 선언
protocol Greetable {
	func greet() -> String
}

// Robot의 Type을 프로토콜로 설정, 함수 사용을 강제
class Robot: Greetable {
    var ID : String

    init(ID: String) {
        self.ID = ID
    }

    func greet() -> String {
        return "Your ID is \(ID)"
    }
}

// 인스턴스 생성
let user1ID = Robot(ID: "userID")
print(user1ID.greet())

// Your ID is userID

 

 Protocol 확장하기(Extension)

Greetable 프로토콜에 기본 구현을 추가하여 greet() 메서드가 기본적으로 "Hello!"를 반환하도록 하세요.
기본 구현을 사용하지 않고 고유 인사를 반환하는 Alien Struct를 추가로 구현하세요.
protocol Greetable {
    func greet() -> String
}

extension Greetable {
    func greet() -> String {
        return "Hello!"
    }
}

struct People: Greetable {
    var name: String

    func greet() -> String {
        return "Hello, \(name)"
    }
}

struct Alien: Greetable {
    var name: String

    func greet() -> String {
        return "Welcome My world, \(name) "     // 여기서 고유 인사를 overwrite
    }
}

class Robot: Greetable {
    var ID : String

    init(ID: String) {
        self.ID = ID
    }

    func greet() -> String {
        return "Your ID is \(ID)"
    }
}

let user1 = People(name: "김유저")
let user1ID = Robot(ID: "userID")
let alien = Alien(name: "guest")
print(user1.greet())
print(user1ID.greet())
print(alien.greet())

// Hello, 김유저
// Your ID is userID
// Welcome My world, guest

 

Extension 더 이해하기

protocol Greetable {
    func greet() -> String
}

extension Greetable {
    func greet() -> String {
        return "Hello!"
    }
}

struct Alien: Greetable {
    var name: String
    func greet() -> String {
        return "Greetings, \(name)!"
    }
}

let alien = Alien(name: "Zeorge")
print(alien.greet()) // "Greetings, Zeorge!"

struct Person: Greetable {
    var name: String
}
let person = Person(name: "Papa")
print(person.greet()) // "Hello!"

 

 위의 코드 중 이 부분을 확인해보자.

struct Person: Greetable {
    var name: String
}

 

 원래 protocol로 함수 사용을 강제하면, struct 안에도 함수를 적어야 한다.

여기서는 어떻게 그냥 사용할 수 있었을까?

 

 protocol를 확장하여 greet() 메서드를 기본 구현으로 설정했기 때문이다.

 기본 구현 덕분에 struct에서 별도의 메서드 설정이 없어도 alien.greet()를 하면 Hello! 가 출력된다.

extension Greetable {
    func greet() -> String {
        return "Hello!"
    }
}

let alien = Alien(name: "Guest")
print(alien.greet())

//Hello!

 

 따라서, extension을 사용하면 기본 구현을 그대로 사용하거나 필요에 따라 오버라이드해서 사용할 수 있는 장점이 있다.

 

심화

 왜 Protocol을 사용해야 할까?

   어플 개발을 한다고 가정해보자.

어플 시나리오: 그리기 앱
기본적인 그리기 도구들:
- 원 (Circle)
- 사각형 (Rectangle)
- 선 (Line)

 

  위의 도구말고도 도형의 종류(삼각형, 마름모 등)가 늘어날 수 있다.

프로토콜을 사용하면 기존 코드에 영향을 주지 않고 도형을 추가할 수 있는 코드를 만들 수 있다.

 

프로토콜이 없을 때와 있을 때 코드가 어떻게 변하는지 확인해보자.

 프로토콜 없이 코드를 작성할 때

class Circle {
    func draw() {
        print("Circle drawn")
    }
}

class Rectangle {
    func draw() {
        print("Rectangle drawn")
    }
}

class Line {
    func draw() {
        print("Line drawn")
    }
}

// as?로 Type 검사, Type을 검사하고 Type이 아니면 nil을 리턴
func drawShape(shape: Any) {
    if let circle = shape as? Circle {
        circle.draw()
    } else if let rectangle = shape as? Rectangle {
        rectangle.draw()
    } else if let line = shape as? Line {
        line.draw()
    } else {
        print("Unknown shape")
    }
}

let circle = Circle()
let rectangle = Rectangle()
let line = Line()

drawShape(shape: circle)       // Circle drawn
drawShape(shape: rectangle)    // Rectangle drawn
drawShape(shape: line)         // Line drawn

 

 - 발생하는 문제 :

    type이 추가될 때마다 조건문을 추가해줘야 함(코드가 점점 길어짐)

    Any Type을 사용했기 때문에, Type에 잘못된 값이 들어오면 오류가 발생할 수 있음

    

 프로토콜을 사용한다면

protocol Drawable {
    func draw()
}

class Circle: Drawable {
    func draw() {
        print("Circle drawn")
    }
}

class Rectangle: Drawable {
    func draw() {
        print("Rectangle drawn")
    }
}

class Line: Drawable {
    func draw() {
        print("Line drawn")
    }
}

func drawShape(shape: Drawable) {
    shape.draw()
}

// 도형들을 추가할 때마다 drawShape 함수는 변하지 않음
let circle = Circle()
let rectangle = Rectangle()
let line = Line()

drawShape(shape: circle)    // Circle drawn
drawShape(shape: rectangle) // Rectangle drawn
drawShape(shape: line)      // Line drawn

 

  삼각형을 그려주는 코드를 작성할 때도 굉장히 수월하게 작성이 가능함

class Triangle: Drawable {
    func draw() {
        print("Triangle drawn")
    }
}

let triangle = Triangle()
drawShape(shape: triangle) // Triangle drawn

최근댓글

최근글

skin by © 2024 ttuttak