Lv.1 ~ Lv.3 : 내가 작성한 답안과 해설 답안이 크게 다르지 않았다.
0
으로 나누려고 할 때 Error 처리를 return 0
으로 하신 것과 enum
을 통해 연산을 분기하신 거까지 같았을 때 그래도 여기까지는 잘 생각해냈구나 싶었다.
다만, 내가 Lv.3
요구사항에서 이해하지 못했던 말인 class 간의 관계
가 언급되었다. day.02
에 Calculator 클래스 안에서 인스턴스를 생성하여 사용하는 것 자체가 관계 형성 아닌가?
라고 생각했는데, 결국 그게 맞았다. 근데 그 행위가 어떤 개념으로 잡혀있다는 걸 몰랐기 때문에 저것 외에 다른 조치가 더 필요한 줄 알았다.
위에서 말한 클래스 내부에서 인스턴스를 생성하여 사용하는 것은 합성 관계
라고 한다.
composition
의 번역 때문에 한국어 명칭이 유난히 많이 갈리는 개념이었다. 검색을 했을 때 아주 다양항 이름으로 불리고 있었다. 합성, 구성, 복합, 포함, 조합... 이에 관한 흥미로운 포스팅(AI도 다 다르게 대답함) 결국 아예 컴포지션
이라고 영어 발음 그대로 불러버리는 경우도 종종 발견했다. 내가 여기서 합성
이라고 부르는 이유는, 과제 해설에서 사용된 명칭이기 때문이고, 나는 그걸 통해 배우는 입장이기 때문이다.
합성 관계는 클래스가 다른 클래스를 소유하면서 강한 종속성을 가지는 관계다. 전체 객체
가 부분 객체
를 포함하고 있어, 부분 객체
의 생명주기는 전체 객체
에 의존한다. 전체 객체
와 생성과 소멸을 같이 하는 것이다.
부분 객체
가 전체 객체
밖에서 독립적으로 기능할 일이 없을 때 적용하는 것 같다. 연산 기능
이 계산기
밖에 존재할 이유가 없는 것처럼 말이다. 때문에, 주로 private
으로 관리한다.(캡슐화)
class Calculator {
// 합성 관계 : 강한 참조를 사용함.
private let addOperation = AddOperation()
private let subtractOperation = SubtractOperation()
private let multiplyOperation = MultiplyOperation()
private let divideOperation = DivideOperation()
func operate(_ operation: Operation, _ a: Double, _ b: Double) -> Double {
switch operation {
case .add: addOperation.add(a, b)
// ... //
}
}
// ... //
}
Lv.4 : (option 1)
상속
을 통한 구현
해설 영상에 제시된 Lv.4
에 대한 모범 답안은 2가지가 있었는데, 그 중 class 간의 관계
를 상속
으로 맺은 답안이 첫번째였다.
이 관계는 단어에서 오는 느낌 그대로다. 물려주는 것
. 일반화(Generalization)
관계라고 불리는 경우도 봤다.
어떤 클래스를 상속 받는 자식 클래스는 부모 클래스가 정의한 프로퍼티
와 메소드
를 물려받아 사용할 수 있다. 그리고, 그것들을 자신 만의 기능으로 재정의
, 확장
하여 사용할 수 있다.(다형성) 부모 클래스(super class
)는 자식 클래스(sub class
)를 여러 개 가질 수 있지만, 자식 클래스는 하나의 부모 클래스만 상속할 수 있다.
계산기
과제에서는 추상 연산 클래스
가 부모 클래스
, 더하기, 빼기, 곱하기, 나누기 연산 클래스들
이 자식 클래스
인 것이다.
// super class
class AbstractOperation {
func operate(_ a: Double, _ b: Double) -> Double {
return -1
}
}
// sub class : 뒤에 AbstractOperation을 입력함으로써 '상속'함
class AddOperation: AbstractOperation {
// super class에서 물려받은 operate 함수를 재정의
override func operate(_ a: Double, _ b: Double) -> Double {
return a + b
}
}
class SubtractOperation: AbstractOperation { // ... // }
class MultiplyOperation: AbstractOperation { // ... // }
class DivideOperation: AbstractOperation { // ... // }
class Calculator {
// super class로 type을 선언해두면 sub class들 중 하나를 주입하여 사용할 수 있다.
var operation: AbstractOperation?
}
Lv.4 : (option 2)
프로토콜
을 통한 구현
두번째 답안은 프로토콜을 채택한 것이었다.
많이 언급되는 관계는 아니다. 클래스와 클래스
간의 관계가 아닌 프로토콜과 클래스
간의 관계기 때문인건가. 나는 이론에 관심이 많은 편은 아니지만 이왕 이론적인 걸 찾아보는 김에 프로토콜을 채택하는 행위도 일종의 관계 맺기인데, 이건 뭐라고 부를까? 궁금해져서 찾아보니 찾아진 개념이다.
Implementation Relationship
이라고도 부른다고 한다.
어떤 클래스가 프로토콜을 채택
했다면, 그 프로토콜이 지정한 규약을 실현함으로써 준수
해야 한다. 프로토콜을 여러개 채택
할 수도 있다. 또, 프로토콜과 실현 관계인 클래스들을 사용할 때, 의존성 역전 원칙에 의해 직접 참조 대신 프로토콜을 통해 접근한다.(추상화)
// 프로토콜은 클래스가 구현해야 하는 함수의 시그니처(이름, 파라미터, 반환값의 정의부) 지정
protocol OperationType {
func operate(_ a: Double, _ b: Double) -> Double
}
// 클래스 이름 끝에 :를 붙이고 그 뒤에 프로토콜 이름을 입력함으로써 '채택'함
class AddOperation: OperationType {
// 프로토콜이 지정한 operate 함수를 정의(실현)함으로써 규약을 준수
func operate(_ a: Double, _ b: Double) -> Double {
return a + b
}
}
// ... //
class Calculator {
// 연산 기능의 추상화
var operation: OperationType?
}
이로써 과제는 끝났다. 이번 과제를 통해서는
Relationship
중에Composition
,Inheritance
,Realization
3가지를 접했는데, 나머지 개념인Dependency
,Aggregation
,Association
을 아래에 마저 정리한다.
어떤 클래스가 다른 클래스를 참조하여 사용하되, 합성 관계처럼 강한 결합이 아니라 특정 작업을 위해 일시적으로 참조하여 사용하는 느슨한 결합
을 통하는 경우를 가리킨다. 참조 대상은 클래스에 한정되지 않고, 구조체나 프로토콜이 될 수도 있다. 구현체를 교체
하여 사용할 수 있는 유연성이 특징이다.
class UserService {
private let networkManager: NetworkManager
init(networkManager: NetworkManager) {
self.networkManager = networkManager
}
func fetchUser(id: String) {
// networkManager를 사용하여 사용자 정보 가져오는 기능 구현 //
}
}
한 클래스가 다른 클래스를 소유, 포함
한다는 점에서는 합성 관계와 공통점이 있지만, 구성요소가 컨테이너 밖에서도 독립적으로 존재
한다는 점에서 차이가 있다. (강한 결합과 약한 결합의 차이)
또, 부분 객체
는 여러 전체 객체
에 속할 수 있다.
// 학생은 학급에 속하지만, 독립적인 존재다. 학급이 사라져도 학생은 계속 존재한다.
class Student {
let name: String
let studentId: String
init(name: String, studentId: String) {
self.name = name
self.studentId = studentId
}
}
class ClassRoom {
let className: String
// 소유 관계 : 약한 결합
var students: [Student]
init(className: String, students: [Student]) {
self.className = className
self.students = students
}
func addStudent(_ student: Student) {
students.append(student)
}
func removeStudent(withId studentId: String) {
students.removeAll { $0.studentId == studentId }
}
}
var emily = Student(name: "Emily", studentId: "2024001")
var millie = Student(name: "Millie", studentId: "2024002")
var class1A = ClassRoom(className: "1-A", students: [emily, millie])
// 학급에서 제외되더라도 Emily는 살아있다.
class1A.removeStudent(withId: "2024001")
연관 관계는 집합 관계보다 느슨한 관계성
을 가진 것으로 보인다. 소유 관계도 아니고 말그대로 서로 연관만 되어있는 정도다. 집합 관계는 has-a
, 연관 관계는 uses-a
관계다.
객체들은 서로 독립적으로 존재하며 필요할 때만 상호작용
한다. 이 관계는 일시적일 수 있고, 양방향 또는 단방향일 수 있다.
내일배움캠프의 학생과 튜터 간 관계로 예를 들어봤다. 이번 주에 튜터님과의 면담이 있었던 것에서 영감을 받았다. 평소에 학생은 공부를 하고, 튜터님들은 튜터링 및 특강을 하신다.(독립적으로 존재
) 그러다가 어느날 면담이 잡히고, 학생마다 면담할 튜터님이 배정된다. 하지만 학생과 튜터는 서로 소유 관계가 아니다. 면담이 취소될 수도, 면담이 끝나면 또 볼일이 없을 수도 있다.
class Student {
let name: String
let studentId: String
// 학생은 면담할 튜터님을 한명 배정 받는다.
private var tutorThisSession: Tutor?
init(name: String, studentId: String) {
self.name = name
self.studentId = studentId
}
// 공부하기
func study() {
print("Study hard.")
}
// 이번 주 면담 일정이 잡힌다.
func makeAppointment(_ tutor: Tutor) {
self.tutorThisSession = tutor
tutor.makeAppointments(self)
}
// 면담이 끝나면 일시적이었던 관계가 끝난다. 다음 면담이 언제인지도 모르고, 다른 튜터님과 하게될 수도 있다.
func completeSession() {
tutorThisSession?.completeSession(self)
tutorThisSession = nil
}
}
class Tutor {
let name: String
let tutorId: String
// 튜터님들은 면담할 학생이 여러명이다.
private var studentsThisSession: [Student]
init(name: String, tutorId: String) {
self.name = name
self.tutorId = tutorId
self.studentsThisSession = []
}
// 특강하기
func lecture() {
print("Lecture about Algorithm")
}
// 이번 주 면담 일정이 잡힌다.
func makeAppointments(_ student: Student) {
studentsThisSession.append(student)
}
// 학생과의 단일 면담 종료
func completeSession(_ student: Student) {
studentsThisSession.removeAll { $0.id == student.id }
}
}
let tutor = Tutor(name: "Eddie", id: "T001")
let student1 = Student(name: "Emily", id: "S001")
let student2 = Student(name: "Millie", id: "S002")
// 튜터와 학생은 서로 독립적이며, 필요할 때만 연관된다.
student1.makeAppointment(tutor)
student2.makeAppointment(tutor)
student1.completeSession()
수요일까지 괜찮았으나 목요일 쯤부터 힘에 부치기 시작했는데 단순히 하루에 12시간을 모니터 앞에 앉아있는 것과 수면의 부족 때문에 육체적으로 지친 것도 있었지만 정신적으로도 조금 흔들렸던 것 같다. 나는 지금까지 실습을 통해 학습해왔는데, 그 방식이 내게 맞았고 좋았다. 직관적으로 눈에 보이는 것으로 인식하는 걸 좋아하고 직접 코드를 치고 실행하며 체득하는 것을 좋아하는데, 쏟아져내리는 이론과 개념에 버거움을 느꼈다. 객체 지향 프로그래밍, SOLID, 그리고 그에 대한 설명들과 같이 말로 줄줄 이어지는 것들은 내게 와닿기보다는 굉장히 추상적으로 느껴졌다. 내 국어 실력이 형편 없어서 그 "말"들을 이해 못하는 거면 모르겠는데, 난 국어를 못하지 않는다. 거기 쓰인 모든 어휘를 이해하지만, 그게 뭔지를 모르겠는 거다. 그래서 그걸 코드로 치면 뭐지? 이 개념이 사용된 예시는 뭐지? 내가 이걸 적용해서 코딩하려면 어떻게 써야하는 거지? 같은 생각이 계속 들었다. 자동차 같은 예시 말고 앱에서 쓸만한 코드 말이다. 오늘 정리하면서 쓴 예시 코드도 실습 코드와 구체적인 경우를 들어 작성하게 됐는데, 내가 저런 실제적인 걸 보고 나야 실감하고 이해해서 그런 거 같다.
그리고 이론을 접하고나니, 내가 연습용으로 만들기 시작한 계산기 앱에 대한 매우 큰 압박감을 느꼈다. 객체 지향적이고 솔리드 원칙을 지킨 근사한 구조 설계를 해야할 것만 같았다. 여기에 알고리즘 특강까지 더해졌다. 스택은 뭐고 큐는 뭐고, 특정 로직에는 정석적으로 쟤네를 적용한다는 것 같은데, 내가 구현하고자 하는 기능이 저기에 속한다면 저 방식을 채택해서 구현해야만 할 것 같았다.(이미 만들어 본 앱을 돌아보며 언제 다 뜯어고치지 라는 생각에 아득해진 건 덤이다.) 그러려면 저걸 먼저 공부한 다음에 저거대로 코딩을 해야하는 것이다. 그렇다보니 코딩에 손이 가지 않았다. 뷰를 완성하고 나니 거짓말처럼 의욕이 와르르 무너졌다. 현재 기능 구현 직전에 멈춰있다.
멍청한 코딩을 하고 싶지 않다. 그러려면 당연히 저 원칙들을 알고 따르는 것이 중요하다. 내가 캠프에 합류한 이유도 사실은 이런 학습이 필요해서다. 내 스스로가 iOS 트랙을 선택한 이유에 그렇게 적었다. 효율적이고 똑똑하게 코드를 잘 짜고 싶어서라고. 힘들고 버겁다고 했지만 그 끝엔 이 과정이 내게 꼭 필요하다는 생각으로 이어졌다. 언제까지 내가 좋아하는 방식으로만 학습할 수는 없다. 나의 결점이기 때문에 채우고 싶다고 말했던 바로 그 이론적인 부분을 이제는 직면해야 할 때다. 그게 다음 단계로 성장하는 거다.
남들보다 느리지만 클래스 관계에 대해 아주 천천히 정리해보며 그래도 결국엔 내 머릿속에 지식이 장착되는 걸 느꼈다. 이론적인 것에 관심 없는 나와 달리 매우 의욕적이고 열정적으로 탐구하는 다른 수강생 분들을 보면서, 나와 다른 유형의 인간을 바라보는 신기한 마음과 함께 대단하고 존경스럽다는 마음이 들었다. 그리고 그 분들과 상호작용을 할 수 있고, 지식을 공유할 수 있는 것이 아주 다행이고 감사하다.(그리고 절반쯤 알아들었지만 아주 유익한 지식을 쏟아부어주신 튜터님도!) 너무 힘들지만 캠프에 합류하길 잘했다는 결론과 함께 이번 주를 보내주겠다.
정성스러운 글이라 빠져서 정독했어요 에밀리님!! 내용 정리하느라 완전완전 고생하셨어요 덕분에 여러가지 관계에 대해 좀 짚어보게 됐어요 체고!!!!!