[Swift / Lazy] 진짜 필요할 때만 씁시다!

박준혁 - Niro·2023년 11월 17일
6
post-thumbnail

안녕하세요 Niro 🚗 입니다!

🔗 [Swift/Lazy] 지연 저장 프로퍼티, 너 누구니?

의 후속편으로 저장 지연 프로퍼티인 Lazy 에 대해 알아보았고 모든 프로퍼티에 Lazy 를 사용하면 좋은거 아닌가 라고 결론을 내렸습니다.

정확한 이유를 찾지 못하고 더 고민해본 내용으로 진짜로 Lazy 가 장점만 있는지 확인해보고자 합니다!


❓ 과연 장점만 있을까?

자, 아주 짧게 복기를 해봅시다.

Lazy 는 변수의 값이 필요할 때까지 초기화하지 않고 실제로 사용될 때 초기화하도록 하는 프로그래밍 기법입니다.

Lazy 키워드를 사용하여 우리가 얻을 수 있는 것은 메모리를 효율적으로 사용할 수 있고, 순환 참조를 방지 할 수 있고, 더욱 안전한 코딩을 가능하게 한다는 점입니다.

그럼 막 사용해도 되는거잖아!


😓 Lazy, 너 알다가도 모르겠다...

단점이 존재하지 않을것만 같았던 Lazy 도 아주 취약한 부분이 존재합니다!

역시.. 모든 만물에는.. 장점이 있으면 단점이 있는거 같습니다....

그럼 어디에 취약할까요? 쉽게 설명하고자 예시를 가져왔습니다.

🚨 멀티 쓰레딩에 취약한 Lazy

우리는 비동기 동작을 위해 GCD를 사용하거나 Async / await 을 사용하게 됩니다!

이처럼 DispatchQueue.global().async 구문으로 인해 2개의 다른 쓰레드가 lazy 로 선언된 value 라는 프로퍼티에 접근하는 것을 볼 수 있습니다.

lazy 프로퍼티의 경우 접근할 때 초기화가 되는 특성을 갖고 있는데... 다수의 쓰레드에서 접근을 하게 되면 어떤 현상이 벌어질까요?

위의 이미지를 통해 먼저 보셨겠지만 실행 순서를 확인해보자면

  1. Thread 1lazy 프로퍼티에 접근
  2. value 프로퍼티의 초기화 코드 실행
  3. Thread 2lazy 프로퍼티에 접근
  4. Thread 1 의 초기화가 완료 되기 전에 Thread 2 의 초기화 코드가 실행
  5. Thread 1Thread 2 는 모두 다른 값을 갖게 되고
  6. 초기화 코드가 두번 실행되어 "Initializing value..." 가 두번 출력

하게 됩니다!

다중 쓰레드 조건에서 Lazy 프로퍼티에 접근하기 위해서든 다른 해결책이 필요해 보입니다...

운영체제 시간에 배운 Lock 이나 Semaphore 등 여러 매커니즘을 사용하면 Lazy 프로퍼티의 초기화를 한번만 진행시킬 수 있을거 같아보입니다.... 아마도...?

🧑🏻‍🏫 Semaphore 를 통해 접근을 제한 시켜보자!

이번에는 동시 접근을 제한하기 위해 Semaphore 를 사용한 예시를 갖고 왔습니다.

굉장히..어려워 보이죠...?

private let semaphore = DispatchSemaphore(value: 1)

해당 코드를 통해 DispatchSemaphore 의 값은 1 로 설정되어 있고 결론적으로 통과할 수 있는 쓰레드의 수를 1 로 제한한다는 의미입니다.

하지만.... 생각한대로 이루어지지는 않았습니다......

위의 이미지와 같이... 초기화 과정이 2번 이루어졌고 lazy 프로퍼티에 대해서는 쓰레드에 안전하지 않다는 것을 확인할 수 있었습니다...

제가 미쳐 생각하지 못한 부분이 있을 수도 있고 예시 코드 자체가 문제가 있을 수도 있습니다... 여러번 시도를 해보았지만 어떠한 방법으로도.. lazy 프로퍼티에 대한 동시 접근을 막지 못했습니다...


💭 Lazy, 생각하고 사용하자!

🔗 10: Lazy를 잘 안쓰는 이유

🔗 [SR-1042] Make "lazy var" threadsafe

요즘 세상은 검색을 잘하는 것도 하나의 능력인거 같습니다..

오랜 시간 Lazy 에 대해 찾아보니 좋은 블로그를 찾게 되었고 Swift 오픈 소스에서도 여러 논의가 있었다는 것도 확인하였습니다.

영어로 되어 있어 읽기 힘들었지만 제가 열심히 번역을 하였고 해당 이슈의 내용은 다음과 같습니다.

Lazy 프로퍼티를 사용하는 곳에서도 쓰레드를 안전하게 만들기 위해 따로 제공 해주자!

하지만... 여기서도 찬반이 나눠졌고 반대의 입장은 다음과 같습니다.

  • 안전한 접근을 위해 추가적인 로직이 필요하다.
  • 읽기를 수행할 때, 감시하는 값도 필요하다.
  • 동기화를 위해 추가적인 저장공간도 필요하다.
  • 결국... 비용이 너무 높다.

라는 의견이 있었고 해당 내용을 통해 어떻게 코드로 구현을 할 수 있을까... 고민 고민 끝에 만들어 보았고 정확하게 맞는 코드인지는 잘 모르겠습니다만...

Lazy 를 사용하지 않고 우회하는 방식으로 Lazy 처럼 동작하는 코드 입니다.

위에서 반대 입장에서 나온 '읽기를 수행할 때, 감시하는 값도 필요하다' 라는 의견에 따라 값을 감시하는 _value 프로퍼티를 만들었습니다.

_value 프로퍼티는 옵셔널 타입으로 초기값은 nil 입니다.

실제로 value 프로퍼티에서 접근하기 전까지는 추가적인 메모리를 사용하지 않는다고 합니다.
( 물론 lazy 처럼 아에 메모리를 사용하지 않는 것은 아니지만 무시할 정도로 작다 )

value 프로퍼티에 처음으로 접근하게 되면 초기화가 이루어지면서 _value 에 값이 할당되고 이후에 접근하는 값은 재사용하게 됩니다.

지금까지 보여드린 예시와는 달리 여러개의 쓰레드가 접근해도 값은 똑같이 28이 나온 것도 볼 수 있었습니다.

이 방식이 lazy 프로퍼티와 유사하게 메모리를 효율적으로 사용할 수 있고 쓰레드에 대해 안전을 보장할 수 있지만 해당 프로퍼티가 필요할 때까지 메모리 할당을 지연시키지는 않습니다..


📌 Lazy 최종 결론!

Lazy 프로퍼티는 '여러 쓰레드에서 접근이 취약하고 안전하지 못하다' 라는 것은 사실입니다!

전편에서 '그냥 Lazy 좋은데 고민하지 않고 다 쓰면 안돼?' 라는 무책임한.. 잘못된 결론을 지어버렸네요..

실제 서비스되는 앱에서는 많은 통신이 이루어지고 비동기 처리, 다중 쓰레드에서 작업하는 상황이 많이 있을거라 생각합니다.

결론적으로 Lazy 를 남발하지 않고 정말 필요한 상황에서 사용하고 여러개의 쓰레드가 접근하는 상황이 존재한다면 Lazy 는 꼭 사용하지 않는 것이 좋아보입니다!

위의 글에서 직접 예시를 만들다보니 잘못된 내용이 전달될 수 있습니다!
그러한 부분을 찾으셨다면 따끔한 충고 부탁드립니다!

긴글 읽어주셔서 감사하고 더 좋은 글로 찾아올게요!

profile
📱iOS Developer, 🍎 Apple Developer Academy @ POSTECH 1st, 💻 DO SOPT 33th iOS Part

2개의 댓글

comment-user-thumbnail
2023년 11월 17일

전혀 모르는 분야 라고 생각했는데, 비슷한 부분이 있어서 더 흥미롭고 재밌게 읽었던 거 같습니다. 추후 많이 도움 될 자료 인거 같습니다 :) 좋은 글 감사합니다!!

답글 달기
comment-user-thumbnail
2024년 4월 19일

새롭게 알게 되었습니다! 감사합니다

답글 달기