시작하기에 앞서
저는 LINE에서 iOS Timeline 개발 업무를 하고 있는 이건홍입니다. 이번 블로그에서는 서비스의 기능과 히스토리가 많아져서 유지보수가 어려웠던 iOS Timeline의 성능과 구조를 개선했던 경험을 공유 드리고자 합니다.
구현 동기
우선 구현하게 된 동기부터 말씀드리겠습니다.
LINE Timeline은 친구들의 다양한 social 활동을 보여주는 공간으로서, 하나의 post는 이론적으로는 수십 개 이상의 다양한 형태로 노출됩니다. 또한 동일한 post라도 사용자가 보고 있는 화면, 상황, 속성에 따라 사용자 이벤트 또한 다르게 처리해야 하는 상황이 빈번하게 요구됩니다.
iOS Timeline에서는 이러한 요구사항들을 처리하기 위해 Apple사에서 제공하는 개발 guideline이 아닌 LINE Timeline(이하 Timeline)만의 개발 guideline에 따라서 post를 개발, 관리하고 있었습니다.
시간이 지남에 따라 Timeline 서비스의 기능이 추가되고 변경되면서 아래와 같은 다양한 부작용이 발생하기 시작했습니다.
- 사용자의 요구사항
- "타임라인이 느려요"
- 사용자가 스크롤 할 때 화면 끊김이 발생
- 개발자의 요구사항
- "느린 부분 어디를 어떻게 수정해야 하죠?"
- Post의 생성과 데이터의 흐름이 코드 상에 명확하게 보이지 않아, 성능 저하의 정확한 원인을 규명하기 어려운 상황
- "개발이 왜 이렇게 오래 걸리죠?"
- Apple사의 guideline과 Timeline의 guideline을 맞추는 과정에서 유지 보수 비용 증가
- "하나를 수정했는데, 다른 화면이 이상해졌어요"
- View들 간의 관계가 서로 중첩되어 있는 상황
- View를 수정하거나 추가할 경우, 개별 view의 구조가 아닌 Timeline의 전체 구조 및 해당 view가 사용되는 모든 곳을 이해해야 올바른 수정이 가능
- "느린 부분 어디를 어떻게 수정해야 하죠?"
이러한 요구사항이 발생하면서 여러 문제점을 해결하기 위해 구조변경을 시작하게 되었습니다.
기존 Timeline feed list의 구조
Timeline에서는 post 정보를 사용자에게 보여주기 위해서 서버로부터 전달받은 각각의 post data와 UITableView의 cell을 1:1로 mapping하여 화면에 노출했습니다.
- 각 cell은 여러 개의 view들로 구성되어 있고, 하나의 view는 다시 여러 개의 sub view들로 이루어진 구조
- view로 data를 전달하기 위해 cell에 존재하는 모든 subview들을 순회
- 모든 view의 data를 구성한 후 화면에 노출
이와 같이 Timeline에 존재하는 다양한 view들을 동일한 방식으로 관리하기 위해서 Timeline의 guideline에 따라 data를 전달하고 view를 관리하는 구조였습니다.
성능 저하와 높은 유지 보수 비용의 원인
각 Cell의 낮은 재사용성
하나의 cell이 다양하고 많은 view들로 구성되어 있기 때문에 data를 다른 방식으로 보여주기 위해서 view들에 사소한 변경이 필요하면 새로운 형태의 cell을 생성해야 하는 단점이 존재하였습니다. 따라서 cell의 재사용성이 낮아 새로운 cell을 자주 생성해야 하므로 성능 저하가 발생하였습니다.
높은 Data 전달 비용
Cell은 다양하고 많은 하위 view들로 구성되어 있기 때문에 하위 view들에 data를 전달하기 위해서 view들을 순회하는 과정에서 UI freezing이 발생합니다. 또한 view가 어느 시점에 어떤 data를 전달받는지 파악하기 어려워서 문제 발생 시 해결책을 찾는 데 많은 시간이 소요되었습니다.
많은 UI Thread 작업
각 view에서 필요한 data를 알 수 없으므로 사전에 view들에 필요한 데이터를 가공할 수 없는 구조입니다. view에서 화면에 그리는 데 필요한 data를 가공하는 작업이 반복적으로 UI Thread에서 일어나기 때문에 UI 성능 저하의 원인이 되었습니다.
Timeline의 독자적인 개발 guideline
Apple에서 제공하는 개발 guideline이 아닌 Timeline만의 개발 guideline에 따라서 개발했기 때문에 두 가지의 다른 guideline을 연동하는 과정에서 유지 보수 비용이 많이 발생할 뿐만 아니라 성능 저하의 원인이 된다고 예상하였습니다.
Timeline feed list 구조 개선 아이디어
기존의 구조적인 문제점을 개선하기 위해 다음과 같은 아이디어를 생각해봤습니다.
작은 크기의 cell
하나의 post를 하나의 cell로 보여주는 것이 아니라 최대한 많은 개수의 cell로 쪼개서 사용할 수 있도록 하면, cell의 재사용성을 극대화해서 성능 향상을 가져올 수 있습니다. 이로 인해, 크고 복잡한 cell을 작게 쪼개는 과정에서 불필요하거나 잘못된 구조들을 쉽게 제거하거나 바로 잡을 수 있게 되며, 작은 크기의 cell이기 때문에 cell에 필요한 data를 파악하기가 수월해집니다.
단일화된 view model protocol
Timeline에는 종류나 기능이 다른 많은 view가 존재하기 때문에 각 view에 필요한 작업들을 view controller에서 한다면 view controller가 점차 무거워집니다. 따라서 tableview에 필요한 cell을 생성하기 위해서 공통으로 필요한 기능들을 정의한 PostTableViewModel protocol을 정의하였습니다. 그리고 아래와 같은 방법으로 data를 사용하고 view를 그릴 수 있도록 하였습니다.
- Post data를 가공해서 가지고 있는 protocol을 구현한 개별 view model 개발
- View controller에서는 data나 view에 바로 접근하는 것이 아니라 view model들을 protocol만을 이용해서 접근해 view를 생성하고 화면에 그림
위와 같은 방법을 이용한다면 view를 그리는 데 필요한 모든 logic이 view model 안에서 구현될 수 있습니다. 따라서 view controller에서는 view를 알 필요가 없고, view에서는 다른 view들과는 관계없이 view model과 view의 구조만 고려하면 되기 때문에 전체 구조가 간결해집니다.
Background thread 최적화
최대한 많은 작업을 UI Thread가 아니라 Background thread에서 쉽게 처리할 수 있도록 변경하였습니다. 또한, view에 필요한 data들을 background thread에서 가공한 다음 필요할 때마다 재사용할 수 있게 하여 성능 향상을 가져옵니다.
Low background thread priority
사용자가 UI를 조작했을 때 빠르게 결과를 보여주기 위해 post data를 얻어와서 가공하는 background thread의 priority가 높게 설정되어 있었습니다. 높은 background thread priority를 유지하면 사용자에게 빠르게 결과를 보여줄 수 있지만 사용자의 다른 UI 조작에 영향을 미치게 됩니다.
따라서 사용자가 스크롤과 같은 동작을 했을 때 실행한 결과를 바로 보여주기보다 background job을 실행하고 thread priority를 낮춰, 결과가 사용자에게 늦게 노출되는 상황이 발생하더라도 사용자의 다른 UI 조작에는 성능상 영향이 없도록 합니다.
새롭게 구성된 Timeline feed list의 구조
위와 같이 수정된 구조에서는 아래와 같은 과정을 거쳐 view를 화면에 그립니다.
- 서버에서 post data를 받아오면 해당 post data의 array를 생성
- Post data들의 array를 가공해서 view model의 array를 생성
- Tableview는 post data가 아니라 view model array에 접근해 작은 크기의 cell을 만들어내고 view model이 가진 data를 이용해서 view를 그림
View model들은 동일한 protocol을 구현하고 있기 때문에 tableview에서는 data의 종류에 관계없이 protocol만을 통해서 view model에 접근할 수 있습니다. View model은 cell에 사용되어야 할 view들을 알고 있기 때문에 사전에 view들에 필요한 data를 미리 가공해서 view model 안에 보관할 수 있습니다. 또한 작은 크기의 cell로 나누어졌기 때문에 view model과 cell, view의 관계만 파악하면 쉽게 view를 수정하거나 교체할 수 있습니다.
View model 생성 과정
기존의 구조에서는 server에서 post data들을 가져오면 DataResult Controller가 가져온 post data들을 array 형태로 보관하며 view controller에서는 이 array에 접근해서 post 정보를 사용하는 형태였습니다. 하지만 수정된 구조에서는 얻어 온 post data들을 바로 사용하지 않고 factory를 통해 view model들로 만들어내고 이렇게 만들어진 view model에 접근해 data를 사용하게 됩니다. 이렇게 post 정보들을 factory를 이용해서 view model로 만들어내는 모든 과정이 background thread에서 일어납니다.
이러한 구조이기 때문에 view model이 post data에서 view에 필요한 data를 꺼내서 가공하도록 구현되어 있다면 필요한 data를 가공하는 모든 작업이 쉽게 background thread 작업으로 수행됩니다.
View model 자료 구조
이렇게 post data들을 view model로 가공하게 되면 새로운 post data를 이용해서 insert, delete, update 등의 작업을 할 때 post에 해당하는 view model들 간의 관계를 관리해야 할 필요성이 있습니다.
Post와 view model의 관계를 관리하기 위해서 아래와 같은 두 가지 방법으로 구현할 수 있습니다.
- 특정 post에 해당하는 view model들이 view model array에서 어떤 위치를 차지하고 있는지 연산하여 실시간으로 추적하는 방식
- Post와 view model 사이에 관계 자료 구조를 두어서 post에 해당하는 view model들을 따로 관리하는 방식
검색 성능으로는 1.과 같이 실시간으로 post와 view model 간의 위치 정보들을 파악하는 것이 유리합니다. 하지만 이 방법은 연산 과정이나 동기화 부분에서 문제가 발생하면 문제가 발생한 부분뿐만 아니라 다른 post data의 관계 정보들에서도 문제가 발생할 수 있으며 하나의 data가 수정되었을 때 다른 모든 관계들을 갱신해야 하는 단점이 존재합니다.
Timeline은 특정한 post의 insert, delete, update보다는 access과 data array의 append가 높은 빈도로 발생합니다. 그래서 post의 변경이 있을 때마다 post와 view model 간의 관계를 담당하는 관계 자료 구조를 갱신하고, 필요할 때 관계 자료 구조를 검색해서 post에 해당하는 view model을 찾아내는 방식이 더 견고하다고 판단하여 2.와 같은 구조로 구현하게 되었습니다.
Post를 이용해서 view model을 만드는 과정은 DataResult Controller 안에 포함해서 post를 insert하거나 update하게 될 때 자동으로 view model array와 view model relation을 갱신하도록 구현하였습니다. 따라서 view model들에 직접 접근해서 삭제하거나 수정하는 것이 아니라 post의 삭제나 수정이 이루어진다면 자동적으로 view model array와 관계 자료 구조들이 동시에 수정됩니다.
새로운 구조의 장,단점
장점
Cell의 재사용성 증가
각 Cell의 크기가 작아지고 재사용성이 증가함으로써 새로운 cell을 만들어내는 비용이 줄어들기 때문에 성능 향상이 가능합니다.
명시적인 View생성과 Data 전달
모든 view를 순회하면서 data를 전달하여 각 view가 필요한 data를 꺼내서 사용하는 방식이 아니라 cell에 필요한 view들을 view model에서 사전에 알고 있습니다. 그래서 data가 필요한 view에만 가공된 형태의 data를 전달함으로써 data를 전달하는 과정이 단순해져 가독성과 성능이 향상됩니다.
Background thread 최적화
View model의 생성은 background thread에서 생성된다는 것을 구조적으로 보장합니다. 따라서 개발자는 view model을 생성하는 부분에 데이터 처리 로직만 추가하면 view에 필요한 작업을 어떤 시점, 어느 위치에서 해야 하는지에 관한 고민 없이 background thread 작업을 최대화할 수 있습니다.
구조적 유연성
Post data를 그대로 사용하는 것이 아니라 post data를 가공한 view model들을 이용해서 view를 나타내기 때문에 만약 같은 data를 다르게 보여주고 싶은 상황에서는 view model의 설정이나 data를 변경하거나, 다른 view model로 변경해서 화면에 그릴 수 있습니다. 따라서 요구사항의 변경이나 다양한 스펙에 구조적으로 유연하게 대처할 수 있습니다.
단점
Post와 view model 간의 관계 관리 비용
Post들을 바로 사용하는 것이 아니라 post들을 가공한 view model들을 만들어내고 이러한 view model들을 사용하는 2개의 자료구조로 이루어져 있습니다. 따라서 view model을 추가로 생성하는 비용과 post data와 view model 간의 관계들을 관리해야 하는 비용이 발생합니다. 또한, 두 개의 자료구조를 동기화해야 하는 부담이 발생합니다.
코드 및 파일 개수 증가
단순한 view를 추가하더라도 새로운 view를 추가할 때마다 기존의 구조에 맞춰 view를 사용하는 데 필요한 view model과 cell을 추가 구현해야 하므로 코드의 양이나 파일의 개수가 증가할 수 있습니다.
성능 비교
구조 변경 이후 UI의 freezing이 줄어들고 사용자가 느끼는 체감 성능이 많이 향상되었으나 화면에 그려지는 view의 단위가 변경되었기 때문에 특정한 기준으로 성능 향상을 비교하기는 어려운 상황이었습니다.
QA(quality assurance)에서는 사용자가 체감할 수 있는 구체적인 성능 향상 측정을 위해 다양한 post 데이터가 있는 환경에서 사용자가 실제로 스크롤했을 때 모든 post를 불러와 화면에 그리는 데 소요된 총 시간을 측정했는데 이전 버전과의 비교 측정 결과는 아래와 같습니다.
구조 변경이 되기 이전의 버전을, 구조 변경이 이루어진 이후 버전과 비교해 봤을 때 post들을 화면에 그리는 데 19% ~ 44%의 성능 향상이 나타난 것을 측정 결과에서 확인할 수 있었습니다.
이전 버전 측정 시간 | 이후 버전 측정 시간 | 개선정도 | |
---|---|---|---|
Home 화면 (Post 170개) | 1분 31.86초 | 1분 13.95초 | 19% |
Timeline 화면 (Post 200개) | 1분 38.07초 | 1분 13.15초 | 25% |
Note 화면 (Post 1012개) | 8분 40.50초 | 4분 49.98초 | 44% |
추가 개선 아이디어
- Cell을 화면에 그리기 위해서 Cell의 높이를 계산해야 하는데, 이 계산 과정은 UI Thread에서 수행됩니다. 이 과정을 미리 Background thread에서 수행하고 그 수행 결과만 UI Thread에서 사용할 수 있다면 추가 성능 향상을 기대할 수 있습니다.
- 현재 정의된 View model protocol은 dequeueCell method에서 cell의 생성과 data 설정 과정을 모두 정의하고 있으나, WillDisplayCell에 사용할 수 있는 추가 method를 protocol에 정의할 수 있습니다. 그래서 dequeueCell method에서는 cell의 생성만, WillDisplayCell method에서는 data 설정만 한다면 화면에 view가 보이는 시점에만 data를 사용하기 때문에 추가 성능 향상을 기대할 수 있습니다.
마무리하며
앞서 기존 구조의 단점과 개선한 구조의 장점, 성능 비교 내용을 말씀드렸습니다. 앞으로도 다양한 서비스들의 개발에 도움이 될 수 있도록 지속적인 업데이트를 할 예정이며, 블로그를 통해서 또 공유하길 기대해봅니다.
LINE Timeline에 많은 관심 부탁드립니다.