수정) willSet 심화 / 온도 조절 시스템

sonny·2024년 10월 2일
2

TIL

목록 보기
9/48

willSet 의 심화로 온도조절 시스템을 만들어보자

개인적으로 나는 28도 이상이면 덥고 16도 이하면 춥기 때문에 온도 설정을 그에 맞게 지정해주었다. willSet을 이용하여 온도가 변할 예정임을 알려주는 멘트와 변경 된 후 didSet을 이용해 변경이 되었음을 안내해주는 걸 설정했고, 아래로 이어서 코드를 작성해갔다.

Bool을 왜 false로 설정해두어야 할까

var isHeatingOn: Bool = falsevar isCoolingOn: Bool = false는 난방과 냉방 시스템이 켜져 있는지 여부를 나타내기 위한 Bool 을 선언한 것이다.

Bool 타입 단순히 참(true) 또는 거짓(false)을 나타내는 자료형인데, 난방 시스템이 켜졌는지(isHeatingOn), 냉방 시스템이 켜졌는지(isCoolingOn) 여부를 확인하는 데 논리형 값이 적합하기 때문에 사용했고, true일 때는 시스템이 켜져있음 과 false일 때는 꺼져 있음을 나타내준다.

객체가 처음 생성될 때는 일반적으로 난방이나 냉방 시스템이 꺼져 있는 상태로 시작하는 것이 자연스럽기 때문에 초기값을 false로 설정한다. 예를 들어, TemperatureController 클래스가 처음 생성될 때는 아무것도 켜져 있지 않으므로 난방과 냉방 시스템의 상태를 false로 설정하는 것이다.

난방을 켜고 싶을 때는 isHeatingOn = true로 설정하고, 냉방을 켤 때는 isCoolingOn = true로 설정하여 해당 시스템이 켜졌음을 표시한다. 코드에서 trueOnHeating 메서드와 trueOnCooling 메서드가 이를 담당하고 있다.

즉, false로 시작하는 것은 시스템이 꺼져 있는 상태를 나타내며, 후에 조건에 따라 true로 변경되어 해당 시스템이 작동 중이라는 것을 나타내는 것이다.

Bool도 좋은데 swift에서는 toggle을 추천해주네..?

아래 코드는 내가 원래 작성했던 코드이다.

처음에 작성한 코드로도 물론 값이 잘 나오지만, 기왕 해 보는거 swift가 추천해준 toggle을 이용해서도 기능을 만들어보고 싶었다.

toggle()Bool 값의 truefalse로, falsetrue로 자동으로 바꿔주는 기능을 한다.

예를 들어, isHeatingOn이나 isCoolingOn의 값을 직접 true로 설정하는 대신 toggle()을 사용해 값을 전환할 수 있는 것이다.

toggle을 사용한 결과

왼쪽이 전, 오른쪽이 후 이다.

처음에는 toggle을 쓰는 것이 더 간결하다고 생각했던 이유가 그저 스위치를 껏다가 켜는 것처럼 코드를 실행할 때마다 toggle()이 한 번의 메서드 호출로 true에서 false, 또는 false에서 true로 상태를 자동으로 반전시키기 때문이었다.

따로 isHeatingOn = trueisHeatingOn = false 같은 할당을 할 필요가 없어서 상태를 전환할 때 편리하다는 의미에서 그렇게 생각 했던 듯 하다.

하지만 실제 코드의 길이를 보면 toggle()을 사용할 때 if 문을 사용해 켜거나 끄는 메시지를 출력하는 부분이 추가되어 더 길어졌으니 코드 길이 부분에서는 간결하진 않았다.

"간결하다" 는 표현은 코드를 작성하는 관점에서 상태 전환이 단순해진다는 의미라는데, 나는 그저 코드 길이가 짧아지면 간결하다라고 생각했다. toggle의 간결함은 '상태 전환 편리함'의 간결함이었다.

따라서 단순히 켜기만 하는 것이 아니라 온도 변화에 따라 시스템을 켜고 끄는 기능을 구현해야 한다면 toggle()이 더 나은 선택인 건 맞다.

음..

이렇게 변경 전에 어떤 행동을 준비할 수 있게 도와주는 것이 willSet의 역할이었고, 현재 온도와 새로 설정될 온도를 출력하는 메시지를 통해, 사용자는 어떤 변화가 있을지를 미리 알 수 있었다.

그리고 newValuewillSet 내에서 새로 설정될 값에 접근할 수 있는 특별한 변수였던 것도 알게 되었는데, 이걸 통해 현재의 temperature와 비교하여 조건문을 사용할 수 있었고, 간단하게 새 값을 참조하는 방법을 배울 수 있었다.

추가로 toggle()은 간결함과 편리함을 제공하는 것이고, 직접적으로 true 또는 false를 설정하는 것은 명확성과 의도를 중시하는 상황에서 유리하다는 걸 알게 됐다. 상황에 맞게 선택하여 사용하는 것이 중요할 듯 하다.

결론적으로 willSet은 상태 변화에 대한 피드백을 제공과 코드를 더 읽기 쉽게 만들어주고, 시스템의 작동 방식에 대한 이해를 돕는 중요한 기능이라는 것을 알게 된 공부였다.




10/3 수정내용

온도를 1초마다 내리게 하여 적정온도가 되었을 때 자동으로 냉방을 종료하는 시스템을 만들어보았다.

만약 온도에 따라 냉방 또는 난방을 각각 켜고 끄는 복잡한 조건을 추가하고 싶다면 단순히 toggle()을 사용하는 것보다는 상황에 맞게 isHeatingOn = true 처럼 상태를 명확히 제어하는 것이 더 나은 경우인 듯 하여 그렇게 했다.

상태 전환이 단순한 상황에서는 toggle()이 유용하지만, 제어 코드가 복잡해질 때는 명시적인 상태 설정이 더 안전할 수 있다고 하니 toggle()을 빼버리기로 결정.

우선 맨 처음 작성한 코드들과 다른 점은 첫 줄 부터 다르다.

Import Foundation 이 추가 되었는데, swift에서는 기본적으로 Int, Double, String, Array, Dictionary 등의 타입을 지원하기 때문에 전에 코드를 작성했던 것처럼 import Foundation이 없어도 실행이 되고 사용할 수 있었다.

하지만 코드 내에 날짜, 시간, 파일 처리, 네트워킹 등 고급 기능을 사용할 때는 반드시 import Foundation이 필요하다는 것을 알게 되었고 바로 적용한 것이다.

Import Foundation 가 없으면 Timer에 오류가 나는 것을 확인 할 수 있다.

그리고 그 뒤로 class에서 willSet의 내용이었던

willSet {
   print("\(roomName)의 온도가 곧 \(temperature)도 에서 \(newValue)도 변경될 예정입니다.")
}

해당 코드를 뺐다.

이유는 해동 코드를 작성하게 되면 타이머가 작동할 때마다 알림이 뜨게 되니 실행할 때 지저분해 보일뿐더러, 굳이 1초마다 내려가는 온도를 미리 예고할 필요는 없기 때문이었다.

그리고 var properTemperature: Int = 24 라고 작성하여 적정온도를 설정해주는 변수 선언을 해주었다.

추가로 아래 var timer: Timer? 을 추가 해주었는데, 옵셔널이 붙은 이유는 timer가 초기화되지 않았거나 나중에 중지된 후 nil이 될 수 있기 때문이다. Timer는 처음에 설정되지 않을 수 있다. 그래서 타이머가 언제 시작될지 모르기 때문에 처음에는 nil 상태로 두고 필요할 때 타이머를 생성 할 수 있도록 해주는 것.

그리고 타이머가 필요 없어지면 invalidate()(*무효화 라는 뜻) 를 호출해서 타이머를 중지시킬 수 있다. 이때 더 이상 타이머가 유효하지 않으므로 nil로 설정하여 타이머가 존재하지 않음을 확인할 수 있도록 한다. 옵셔널이 아니면 중지된 타이머에도 값이 남아있어 혼란을 줄 수 있으니 말이다.

그리고 바로 초기화를 진행하여 TemperatureController 클래스를 만들 때 방의 이름과 초기 온도를 설정하는 역할을 하게 해준다.

setTargetTemperature의 목적은, 적정 온도를 설정하는 역할을 해줌으로서 (*newTargetTemperature는 사용자가 설정하고자 하는 새로운 적정 온도를 의미한다) 새로 설정된 적정 온도를 출력해주고 확인할 수 있도록 도와줄 수 있게 했다.

그리고 나서 startCoolingOrHeating() 를 호출하여 현재 온도에 따라 에어컨이나 난방을 작동시키도록 설정해주었다.

그 뒤로 온도 조절 시작 메서드인 startCoolingOrHeating 에는 현재 온도에 따라 에어컨이나 난방을 켜고 조절해주는 역할을 한다. 만약 현재온도가 적정온도보다 높을 경우 에어컨을 켜고 온도를 낮추겠다고 출력하고, 그 반대일 경우 난방을 켜고 온동을 높이겠다고 출력할 것이다.

작동 방법은 단순하게 에어컨이 켜질 경우 isCoolingOntrue로, 난방이 켜질 경우 isHeatingOntrue로 설정하고, 반대는 false로 설정했다.

그리고 나서 1초마다 실행되는 타이머를 설정해주었는데 여기서 좀 복잡했다. 이 기능을 넣으려고 Import Foundation도 맨 위에 넣어주었으니 기왕이면 잘 사용해보자!

해당 코드는 1초마다 실행되는 타이머인데, 반복해서 호출될 때마다 온도를 증가시키거나 감소시키는 기능을 해주는 것이다.

Timer.scheduledTimer는 일정한 시간 간격으로 반복적이게 실행되는 타이머를 설정해준다. 이어서 나오는 withTimeInerval: 1 은 1초마다 실행된다는 것을 의미해주고, repeats: true 는 타이머가 반복해서 실행되도록 설정해주는 것인데 만약 false로 설정했다면 한번만 실행되고 끝날 것이다
repeats - 반복, scheduledTimer - 스케줄 타이머

{ [weak self] _ in ... } 이건 처음 써봤다.
타이머가 호출될 때마다 실행되는 것인데, self약하게 참조(weak) 하도록 설정했다는 것이란다.

알아보니 weak self를 사용하면, 타이머가 실행되더라도 self의 강한 참조를 유지하지 않기 때문에, 메모리 누수를 방지할 수 있다. 만약 self가 해제되면 타이머 클로저 내에서 self를 사용하지 않도록 한다. 때문에 안전하게 guard let self = self else { return }를 사용하여 selfnil인지 확인을 해줄 수 있다고 한다.


약한참조? 강한참조? 참조가 뭐길래 강약을 따질까?

자기 참조를 약하게 한다는 것은 메모리 관리와 관련된 개념으로 순환 참조(circular reference)를 방지하기 위해 객체를 강하게 참조하지 않도록 설정하는 것을 의미한다다고 한다.

비유하자면,,, 음

만약 A라는 사람이 B라는 친구를 "강하게" 소유하고 있다고 한다고 했을 때, B는 A의 소유물이라고 하자.
B가 A에게 의존하는 한 B는 항상 존재할 것이다.

하지만 A가 B를 "약하게" 소유한다면, B는 A와 독립적인 존재가 될 수 있다는 것이다. 그 말은, B가 필요 없게 되면 A는 그냥 자유롭게 사라질 수 있다는 것.

A는 B를 계속 필요로 하지 않을 수도 있으므로, B는 독립적으로도 존재할 수 있는 것이다.

참조 종류들을 간단하게 적어봤다.

  1. 강한 참조 (Strong Reference)
  • 기본적으로 객체를 생성하면 그 객체는 강한 참조를 가진다. 이는 해당 객체가 메모리에 유지되도록 보장하는 것이다. 예를 들어, 객체 A가 객체 B를 강하게 참조하면, 객체 B는 객체 A가 존재하는 한 메모리에 계속 남아있게 된다.
  1. 약한 참조 (Weak Reference)
  • 약한 참조는 객체가 메모리에 존재하는 것을 보장하지 않는다. 즉, 약한 참조를 가진 객체가 메모리에서 해제될 수 있다. 객체 A가 객체 B를 약하게 참조하면, 객체 A가 메모리에서 해제될 때 객체 B의 메모리는 영향을 받지 않는다. weak 키워드를 사용하여 선언하며, 이는 ARC(Automatic Reference Counting)에서 순환 참조를 방지하는 데 도움이 된다.
  1. 순환 참조 (Circular Reference)
  • 두 개 이상의 객체가 서로를 강하게 참조하는 경우, 그 객체들은 서로 해제되지 않고 계속 메모리에 남아있게 된다. 이를 순환 참조라고 한다. 예를 들어, 객체 A가 객체 B를 강하게 참조하고, 동시에 객체 B가 객체 A를 강하게 참조한다면, 둘 다 메모리에서 해제될 수 없다.

채찍피티 감사합니다


다시 돌아오자면,

if self.isCoolingOn { ... } else if self.isHeatingOn { ... }

이 부분은 현재 냉방 또는 난방 시스템이 켜져 있는지 확인하고, 각 상태에 따라 온도를 변경하는 코드다.
냉방이 켜져 있으면 (self.isCoolingOn == true) -> self.temperature -= 1로 온도가 1도씩 내려갈 것이고, 난방이 켜져 있으면 (self.isHeatingOn == true) -> self.temperature += 1로 온도가 1도씩 올라갈 것이다.

아 그리고 사진에서 오류 난 것처럼 피를 흘리는 이유는 이 오류는 비동기 작업에서 self와 같은 참조 타입이 스레드 안전하지 않기 때문에 발생하는 오류라고 하는데.. 내용이 어려웠다.
실행은 잘 돼서 크게 신경쓰진 않았다.

그리고 이어서 아래에 현재 온도를 체크하는 코드를 작성했는데, checkTemperature()로 클래스 내에서 호출되어 현재 온도를 체크하는 역할을 해주고 if temperature == properTemperature현재 온도설정된 적정 온도와 같으면 조건이 참이 되어 print 코드가 실행되고,

isCoolingOnisHeatingOn 변수를 false로 설정해 놓았으니 에어컨과 난방 시스템이 꺼지도록 해준다.

timer?.invalidate()timernil이 아닐 경우 타이머를 정지시켜주는데, invalidate()는 타이머의 동작을 중지시키고 더 이상 호출되지 않도록 해준다.

TemperatureController 클래스의 새로운 인스턴스를 생성하고,livingRoom이라는 이름으로 저장한다.

roomName: "거실"이라는 문자가 전달되어 방의 이름을 설정.
temperature: 15라는 수가 전달되어 초기 온도 설정.

생성자가 호출되면 해당 방의 이름과 온도가 설정되고 초기 온도에 대한 메시지가 출력된다.

setTargetTemperature(24) 메서드를 호출하여 적정 온도를 24로 설정한다라는 것.

properTemperature24로 설정되면서 "적정 온도를 24도로 설정했습니다."라는 메시지가 출력된다.

그 후, startCoolingOrHeating() 이 호출되어 현재 온도와 적정 온도를 비교하고 이때 현재 온도가 15로 적정 온도인 24보다 낮기 때문에 난방 시스템이 켜지고 온도를 높이는 동작이 수행된다.

완료된 실행 화면

profile
iOS 좋아. swift 좋아.

2개의 댓글

comment-user-thumbnail
2024년 10월 2일

냉난방 켜는 함수도 print 부분을 didSet으로 변수에 넣고 함수 넣는 자리에 함수 호출 없이 변수 변경만 해줘도 되겟네요!

1개의 답글