프롤로그
코드를 작성할 때면 코드 한 줄 한 줄은 치열한 고민이 있어야 한다. 시간 내에 빨리 제출해야 하는 테스트 패스용 코드가 아닌 이상은 각 코드의 한 줄 한 줄은 고민이 있어야 한다. 그리고 경력자의 코드 한 줄 한 줄이 대충 짜인 것처럼 보일지라도 경력동안 해 온 고민들이 합쳐져 만들어진 결과물이어야 한다. 강연을 하면서 'Loop 혹은 Branch를 위한 코드가 비단 for와 if 뿐만이 아닌 것은 while과 switch의 용도가 분명하기 때문이다'라는 말을 자주 한다.
내가 작성하는 코드는 1차적으로 창조한 사람의 의도를 읽어야 한다. 가령 Python을 예로 들면, 귀도 반 로섬이 Python을 만들 때에 담고자 했던 철학을 이해해야 한다. Java라면 제임스 고슬링이 이 프로젝트를 시작했을 때에 담고자 했던 철학을 이해해야 한다. 그다음은 경험과 사고가 필요하다. 둘 중에 하나만 있어서는 안 된다. 둘이 같이 있어야 한다. 경험만 있는 사람은 작성한 코드의 양은 많을지 모르나, 코드의 품질이 떨어지고 개선이 되지 않는다. 사고만 하는 사람은 좋은 코드에 대한 합당한 사유들은 있으나, 상황에 맞게 응용할 줄 모르니 트렌드 한 것만 쫓게 된다.
필자도 여러 프로젝트를 하면서 처음 2년은 아무 생각 없이 개발하기 바빴다. 물론 학부 때에 배운 것을 적용하면서 화려한 경력을 지닌 선배들과는 다른 방향으로의 존재 가치적 어필을 시도했다. 물론 이것이 신입이었던 시기에 먹히긴 했다. 하지만 이때는 사고만 있고 경험이 없던 시기였다. 그래서 코드는 화려하고 료과적이었으나 확장성이 그닥 좋지 않았다. 나중에 경험을 하면서 사고했던 코드의 용도를 먼저 생각하고, 화려한 코드보다는 완급 조절을 하게 되었다. 이러한 과정 속에서 얻은 교훈은 기초에 충실해야 한다는 것과 뭔가 새로운 기술을 터득할 때는 이것이 왜 필요한지를 고민해봐야 한다는 것이었다. 기초와 이유에만 충실해도 위에서 얘기했던 것의 시작 단계로 더할 나위가 없다.
Code Smell은 왜 생기는가?
예전에 작성했던 회고록에도 나오지만 처음에 신입으로 입사를 하고 나면 적응하기 바쁘다. 모든 것이 신세계이고 새로운 조직에 일원이 되었다는 왠지 모를 뿌듯함과 설렘으로 정신없다. 그리고 초반에 배운 것으로 업무가 어느 정도 되면 자신이 능력자가 된듯한 착각에 빠지기 쉽다. 중요한 것은 이때 배운 것이 오래간다는 사실이다. '세 살 버릇 여든 간다.'라는 말이 있듯이 이때에 생긴 습관이나 업무 처리 방식이 오래간다. 예를 들어서 필자도 휴가를 쓸 때면, 여기저기 보고하듯이 알리고 떠난다. 요즘 같은 때는 거의 통보식으로 알리고 떠나는데, 아직도 뭔가 승인받는듯한 뉘앙스를 풍기면서 알린다. 이처럼 경력 초반의 습관이 오래간다. 문제는 이때 습관을 잘못들이면 고치기 어렵다. 이때에 좋은 것도 많이 보고, 여러 가지를 비교하면서 비판적인 사고로 모든 것을 바라봐야 한다. 그리고 왜 그렇게 되었는지를 이해해야 한다. 자신이 능력자가 된 착각에 빠져서 허우적거리다 보면 남들보다 뒤처져있는 자신을 발견하게 된다. 종국에는 자위적인 해석과 상대의 의중을 헤아리지 못하고 자신의 부족함을 가리기에 바빠진다. 그럼, 우리가 최소한으로 지양해야하는 코드의 패턴을 알아보자.
Code Smells
구조체의 무분별한 사용
가장 먼저 얘기할 주제는 구조체이다. 다양한 Swift 예제 코드를 살펴보다보면 구조체를 활용한 데이터 모델이 많이 사용된다. 아무 비판없이 그냥 따라하다보면 모든 데이터를 담는 데이터 모델들을 모두 구조체로 짜게 되버릴 수 있다. 하지만 이렇게 되면 mutating 설정도 해줘야하고, let으로 넘겨 받은 구조체를 var로 다시 선언하고 값을 복사하는 등으로 넘겨야하는 코드들이 추가로 발생한다. 심지어 코드를 읽다보면은 구조체라는 것을 망각하고 작업하다가 변경된 데이터가 적용이 제대로 되지 않아서, 의도치 않은 버그로 인해서 낭패를 보기도 한다. 구조체로 만든 데이터 모델은 항상 상수로만 사용하길 권장한다. 불변성을 유지하고 race condition으로부터 안전하게 사용하기 위해서 구조체를 사용하길 바란다. 물론 구조체에서 mutating 키워드를 지원하는 것은 변수를 사용할 수 있도록 확장성을 제공하기 위함이다. 하지만 runtime에서 예측 불가능한 변수를 조장할 필요는 없다고 생각합니다.
그럼, 데이터 모델의 값이 변경이 필요하면 어떻게 해야할까....? 클래스와 객체를 이용하라. 마치 Swift 초기에는 클래스와 객체를 이용해서 데이터를 전달하면 죄를 짓는 듯 한 생각을 가지는 시절이 있었다. 하지만 데이터의 변경이 필요하면 클래스와 객체가 편하지 않은가? 이쪽이 구현 시에 훨씬 코드의 양도 적고, 다른 개발자들과 협업하기에 수월하고 예측 가능한 코드를 만들게 된다.
아니면 구조체를 사용하고 싶다면….? 순수함수를 이용한 함수형 파이프라인을 작성한 후에 데이터 변경이 일어나면 매번 새로운 구조체를 생성하여 반환하는 코드로 작성하도록한다. 상수로만 유지되는 구조체의 값을 바꾸는 방법은 새로 구조체를 생성해서 반환하면 된다. 대신에 이 경우에는 구조체를 어딘가에 저장하고 여러 곳에서 참조하도록 하면 안된다. 오로지 함수형 파이프라인을 태우고, 순수함수 내의 지역변수로 다룬 후에 반환되는 값에 대해서만 활용하도록 하자.
final class와 extension의 사용 의미
final class와 extension을 왜 사용하는 지에 대해서 분별없이 사용하는 것을 목격한 적이 드물게 있다. 이 부분에 대해서 예쩐에 근무하던 조직에서는 심도 깊게 논의한 적이 있다. 이런 부분이 아무렇지 않아 보이지만, 작은 부분이라도 기술적 논의가 불가능하거나 무관심하게 치부하는 조직의 코드는 불보듯 뻔하기 쉽상이다. 기초적인 부분을 놓칠수록 copy만 양산할 수 있다. 일단 2가지를 사용하는 공통적인 이유는 속도와 편의성에 있다. Swift는 기본적으로 table dispatch를 사용한다. 객체는 각각 테이블(목격자 테이블)이 있고, 이를 참조하여 메소드를 호출한다. 해당 객체의 테이블에 해당 메소드가 존재하지 않는다면, 슈퍼 클래스의 테이블로 참조를 옮겨서 메소드를 찾는다. 이렇게 runimte에서 메소드를 dispatch해서 동작하는 방식을 dynamic dispatch라고 한다. Objective-C에서는 string을 이용한 keypath로 접근해서 사용했고, 이 때문에 runtime에서 keypath를 통한 dispatch를 실패할 경우에 unrecognized selector 같은 크래시를 유발했다. Swift는 타입을 추론하여 이 부분을 컴파일 타임에 인식하여 크래시를 예방한다. 아무튼 테이블을 이용한 dynamic dispatch 방식은 속도 측면에서 상대적으로 느리다. 이를 피해서 static dispatch를 사용하기 위해서 final 혹은 extension을 사용한다.
그럼, 둘의 차이는 무엇일까? final은 컴파일러에세 상속이 불가능한 말단 클래스라는 것을 알리는 키워드이다. 그래서 final을 붙이면 static dispatch로 메소드에 접근하지만 해당 클래스의 상속이 안된다. 여기서 또 중요한 것은 상속이 안된다는 것이다. 앱의 속도를 높이기 위해서 무분별하게 내가 만든 모든 클래스에 final을 붙이면 안된다는 뜻이다. 여기서부터는 설계에 영역인데, 우리가 어떤 리스트와 검색창이 존재하는 SearchViewController를 만든다고 가정하자. 그리고 이 클래스는 검색창과 리스트가 존재하는 화면마다 상속을 받아서 구현할 때에 편의성을 제공한다면....? 당연히 final을 붙이면 안된다. 우리는 상속을 적절히 사용하여 설계를 하게된다. 보통 설계를 제대로 하지않고 속도를 위해서 모든 클래스에 final을 붙인다면 확장성과 구조적인 측면에서 비용이 낭비될 수 있다.
extension은 클래스에 부가적인 기능을 부여하기 위해서 메소드를 구현할 때에 쓴다. 여기서 고려해야할 부분은 class에는 final이 안 붙어있기 때문에 상속은 가능하다. 단지 extension으로 묶인 부분은 static dispatch가 적용되어 상속했을 경우, subclass에서 호출이 불가능하다는 점이다. 말그대로 extension이다. 이러한 부분을 고려해서 설계할 때에 적절히 final과 extension을 구분해서 사용하자.
guard let OOO = XXX else { return }
제목만 보고 바로 감이 오는 분들이 있을 것이다. 무분별한 guard-let-return은 자제했으면 한다. 이는 코드 구현시에는 편하다. 단지 나중에 이러한 코드들이 ANR (App Not Responded)라고 불리는 freezing 현상을 만든다. 앱이 그냥 아무것도 안 한다. 원래 HIG에서는 모든 사용자의 이벤트에 대해서 무조건적인 피드백을 주도록 가이드했다. 성공이면 당연히 사용자가 의도한 동작을 해야겠지만, 실패했어도 아무것도 안하는 것이 아니라 무언가 동작을 해야한다. 보통 이 경우는 실패 메시지를 던지겠지만, 실패 메시지만 던져서는 향후에 개발자가 유지보수하는 입장에서 에러나 버그를 추적하기 쉽지 않다. 필자의 경우는 Error 객체를 정의해서 전달하도록한다. 이는 서버 개발을 하면서 얻은 노하우이다. 보통 클라이언트를 개발하는 분들은 Error에 대해서 가벼이 여기는 경향이 있다. 하지만 화면 단이 없는 서버에서는 Erorr 정의가 구체적이고 명확하지 않으면, 증상을 재현해서 수정하기가 여간 까다로운 것이 아니다. 무지성의 guard-let-return은 자제하고 명확하게 정의된 Error를 return함으로써 의도치 않은 에러와 버그를 관리하여 유지보수가 쉬운 코드를 만들도록 하자.
'Programming > Mac & iOS' 카테고리의 다른 글
| [iOS] 센서 이야기 - 자이로스코프 (0) | 2025.11.14 |
|---|---|
| [iOS] Developer Common Knowledge (0) | 2025.11.05 |
| [iOS] Developer‘s Note (0) | 2025.11.05 |
| [iOS] AVFoundation Foundation (0) | 2025.11.02 |
| [iOS] Core Video Foundation (0) | 2025.10.30 |