이호준 멘토님의 우당탕탕 멘토링 -4-

멘토링을 진행한 것은 지금 글을 쓰는 시점으로부터 약 2주 전. 한 주는 제주도 여행을 다녀오느라, 한 주는 philosophers 과제를 끝내느라 글을 쓰지 못하고 있다가 이제서야 글을 쓴다 !!

이번 시간에 멘토링한 것은 GIS 등 위치 기반 서비스에 도움을 주는 개념을 학습하고, 파일을 기기에 다운해보는 것. 마지막으로 LocalDB를 이용해보는 것! 그럼 GIS 부터 살펴봅시다~~

 

GIS LBS TIS

swift에서는 위, 경도 좌표를 넣으면 행정구역 정보로 바꾸어주는 reverseGeocoding이라는 함수를 제공한다.

이들은 무엇과 관련이 있을까? 한번 살펴보자.

 

GIS란 무엇일까

GIS란 Geographic Inforamation System의 약자로, 쉽게말해 지리정보시스템이다.

이 GIS는 위도, 경도와 같은 위치자료(spatial data)를 지도, 도표 및 그림과 같은 정보로 바꾸어 나타내준다.

활용분야는 토지, 교통, 환경 등 가릴 것 없이 지리와 관련된 데에는 모두 사용할 수 있다.

특히 모든 지리정보는 수치 데이터로 수집, 저장되며 필요한 때에 필요한 형식에 맞추어 출력할 수 있다.

 

이 GIS 를 이해하기 전에 먼저 GeoCoding에 대한 이해가 필요하다.

 

(Reverse)Geocode이란

Geocoding이란 경상남도 김해시 삼계동 XXX번지(행정구역 상의 주소)37.433, 127.113처럼 위 경도 좌표처럼 지리좌표정보(geographic coordinates)로 바꾸어 나타내는 것이다.

 

ReverseGeoCoding이란 말 그대로 그 반대다. 좌표같은 지리정보를 넣으면 그것에 맞는, 사람이 읽을 수 있는 정보(Human Readable Address)를 내뱉아준다.

 

위에서 설명했듯 geoCoding과 reverseGeoCoding은 주소 <-> 좌표 정보를 얻어내는 데에 쓰인다.

이 때 이 서비스를 제공하는 종류로는 HttpGeoCodingAPI, GoogleGeoCodingAPI 등이 있다.

 

Geocoding의 원리

Geocoding은 어떤 원리를 이용해서 위, 경도 좌표로부터 행정구역의 데이터를 가져올까?

먼저 GIS 서비스를 제공하는 업체에서는 행정구역정보나 산과 같은 정보, 강에 대한 정보 등 다양한 정보들을 넣어 놓고 그것들과 좌표정보를 매칭시켜 놓는다.

이 매칭된 정보가 있기 때문에 좌표로 행정구역을 얻어낼 수 있고, 또 행정구역을 좌표로 변환해낼 수 있는 것이다.

 

스크린샷 2021-04-12 오후 1 25 01

이미지 출처 : https://www.cdc.go.kr/filepath/boardDownload.es?bid=0021&list_no=127736&seq=270

 

GIS 정보는 누가 제공할까

통계청은 통계지리정보서비스(SGIS)를 구축하고 있으며, 국민들에게 통계주제도, 대화형 통계지도, 분석지도 등을 서비스하고 있음.

또한 기타 공공기관의 경우 정부에서 국가공간정보통합서비스(www.nsdi.go.kr)를 구축한 후, 해당 서비스를 이용해 국가공간정보통합체계, 공간빅데이터, 부동산종합공부시스템, 한국토지정보시스템, 국가공간정보유통시스템, 지적재조사시스템, 공간정보사업 공유 및 관리시스템, 국토공간계획지원체계, 온나라부동산포털, 공간정보오픈플랫폼 등 다양한 공공기관에서 GIS 서비스를 제공함.

출처

 

LBS란 무엇일까

LBS란 Location Based Service의 약자로, 직역하면 위치기반서비스가 된다.

이 LBS는 GIS와 크게 관계를 맺고 있다. 왜냐하면 이 위치기반서비스라는 것이 사용자나 기기의 위치를 데이터로 하여 서비스를 제공하는 것인데, 위치를 가공하여 서비스로 제공하기 위해서 사용하는 것이 GIS이기 때문이다.

 

LBS의 예제로는 무엇이 있을까?

이 LBS의 예에 대해 위키에서는 이렇게 표현하고 있다:

  • 현금출납기나 식당 등 가까운 위치의 서비스나 시설 정보를 조회
  • 할인 중인 주유소 위치 정보나 교통 정체상황 경고 등 알림 서비스들
  • 친구 위치 찾기
  • 이동통신 사업자는 위치 기반지출 (통행 요금 자동 지불) 등
  • 지역적으로 분산된 자원의 관리 : 택시, 배달원, 대여 장비, 병원(의사), 선단 등
  • (주위에서) 사람이나 물건 위치 찾기 : 필요한 서비스 제공자(의사 등), 서비스 사업체, 이동 경로, 날씨, 교통 상황, 숙소 예약, 분실 휴대전화, 비상 구조 서비스 등
  • 목표 근접 시 알림 기능(Push형 또는 Pull형) : 목표 지정 광고, 친구 리스트, 데이트 상대 찾기, 공항 접근 시 자동 체크인
  • 목표 근접 시 자동 수행(Push형 또는 Pull형) : 위치 기반 자동

위 예시가 조금 딱딱하다면 당근마켓이나 네비게이션 앱을 떠올리면 쉽게 이해가 갈 것이다.

 

LBS 관련해서 사용자의 위치를 받아오는 데에도 여러 가지 기술이 쓰인다고 한다.

보통은 인공위성으로부터 정보를 받아내는데, 이것이 meter단위까지 정확하게 받아오기에는 무리가 있어서 인접 이동통신 기지국과의 거리 등 다른 정보들을 추가로 이용하는 A-GPS를 얻어오는 측위 기술도 있고 위치정보센터를 따로 두기도 한다고 한다. 그리고 이 위치정보센터를 관리하는 곳이 OMA(Open Mobile Alliance)라고 해서 삼성, 마이크로소프트, 선마이크로시스템즈 등 회사들이 참여하는 연합이라고 한다. (알아는두어야겠다.)

 

ITS라는 것도 있다!

ITS라는 개념도 있다. Intelligent Transport Systems 라는 용어의 약어로, 한글로 지능형교통체계 정도로 번역할 수 있다.

이 지능형 교통체계는 교통수단 및 교통시설에 통신 기술을 접목하여 교통 정보를 받아내고 그것을 활용하는 시스템이다.

 

여기 블로그에 따르면 우리 주변의 ITS 활용 예시로 버스정류장마다 설치되어 있는 버스 도착 안내 시스템, 교차로에서 교통량에 따라 자동으로 차량 신호가바뀌는 시스템(이런 게 있는지도 몰랐다...!), 하이패스 등이 있다.

 

파일 다운로드하고 가져오기

파일을 다운로드하고 가져와보자.

image 파일을 다운하는 것과 pdf 파일을 다운하는 두 가지 버젼으로 진행해볼 것이다 .

 

app이 파일을 다운로드 할 때 가지는 한계

이번 단락을 쓰기 전에 수많은 방법으로 파일을 내 디바이스에 저장해보려고 했지만 아무리 저장하고 저장해도 디바이스의 파일 앱에서는 내가 분명히 저장한 파일을 찾을 수 없었다.

이게 어떻게 된 일인지 한참을 뒤적거리다 ios의 File System에 대해 알게되었고, 이것을 이해하고 나서야 왜 내가 그토록 다운로드한 파일을 찾을 수 없었는지 알 수 있었다.

 

FileSystem

그럼 ios의 FileSystem이란 무엇일까?

먼저 ios 어플리케이션이 접근할 수 있는 범위에 대해 알아보자.

 

다음 이미지를 한번 보면서 이해하자. (이동건의 이유있는 코드zeddiOS라는 블로그의 도움을 많이 받았습니다.)

우리가 만든 어떤 iOS 애플리케이션은 디바이스 내의 디렉토리에 모두 접근할 수 있는 것이 아니다.

앱은 자신이 속한 Sandbox 안의 디렉토리에만 접근할 수 있다. 이 디렉토리들은 앱이 디바이스에 설치될 때 생성된다.

 

 

위 사진에서 우리가 확인할 수 있는 정보는 크게 Bundle Identifier, Data Container, iCloud Container이다.

  • Bundle Container: Bundle 컨테이너는 어플리케이션의 번들 정보를 담고 있다. 이 번들은 실행가능한 코드 및 xcode에서 프로젝트 폴더 내에 포함시킨 이미지, 소리 파일 등을 모두 가지고 있다.
  • Data Container: Data 컨테이너는 몇 개의 서브디렉토리를 가진 디렉토리이다. 이는 앱 내에서 접근할 수 있는 디렉토리이며, 이들을 제외하면 앱은 앱 외부의 디렉토리에 접근할 수 없다. 예외로 사용자 연락처나 음악 등에 접근하는 방법은 제공하고 있다.

 

Data Container

데이터 컨테이너는 아래와 같은 폴더로 구성된다. 그리고 해당 폴더들은 각각의 역할이 있다.

  • Documents/ : 앱이 사용자로부터 생성된 데이터들을 저장할 때 이 디렉토리에 저장한다. 여기에 있는 파일들은 앱 사용자가 생성하고 수정하고 지울 수 있다. 앱의 사용자가 다운로드 받은 비디오, 오디오 파일 같은 것들은 이 디렉토리에 저장되어야 한다. Documents/디렉토리의 데이터들은 iCloud나 iTunes에 백업된다.
  • Library/ : 사용자의 파일을 제외한 파일들이 저장된다. iOS 앱은 보통 Application SupportCaches 디렉토리를 사용하지만 개발자가 임의로 서브디렉토리를 생성할 수 있다. 여기에 있는 파일들은 주로 사용자에게 노출되지 말아야 하는 파일들이다. 또한 UserDefaults와 같은 데이터들도 이곳에 저장된다.
  • tmp/ : 앱이 실행되는 동안 만들어지는 임시 파일들이 위치하는 디렉토리이다. 여기에 들어있는 파일이 더이상 필요 없다고 판단되면 앱이 해당 파일을 삭제한다. 또한 앱이 실행되고 있지 않는 경우 이 디렉토리 내의 파일은 모두 사라진다.

 

핵심은 우리가 앱을 만들어서 접근할 수 있는 디렉토리는 Documents와 tmp뿐이라는 것이다.

 

FileManager를 이용해 사진 다운로드해보기

그렇다면 이들 폴더에 접근하고 파일을 쓰고 읽는 것들은 누가 담당할까? 바로 FileManager이다.

macOS 및 iOS에서는 경로명을 정할 때 표준 UNIX Convention을 따른다고 한다. 그러니까 상대경로와 절대경로 모두 UNIX 시스템과 같고, 루트 디렉토리 경로는 '/'가 된다.

 

이제 FileManager라는 것이 있다는 것도 알았으니 파일을 다운로드 해보자.

화면은 다음과 같이 구성했으며, image Download라는 버튼을 누르면 내가 미리 설정한 URL 이미지를 다운로드하고 화면에 표시까지 할 것이다.

(코드가 필요하신 분들은 맨 밑을 확인하시길 !)

 

Apr-11-2021 18-33-41

이미지를 다운로드 하는 데에는

FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first

이렇게 생긴 함수를 이용했다. for: 뒤에는 어디에 저장할 지를 적어주고, in: 뒤에는 도메인에 대해 적어준다.

(도메인에는 local, user 등이 있다고 하는데 아직은 살펴보지 않았다. 여기에 자세한 설명이 되어있다.)

 

테스트를 위한 코드에 다운로드 한 경로를 출력하도록 만들어놨고, 그 경로는 다음과 같았다.

destionationFileURL: file:///Users/choeganghun/Library/Developer/CoreSimulator/Devices/16C58D50-945A-4AF8-8BAD-B8B17D974016/data/Containers/Data/Application/3A2F5EC2-03E5-4882-948E-80A26B7E3BE6/Documents/hotecho2021-04-11%2009:33:30%20+0000.jpg

 

이것을 확인하는 방법은 코드로 출력할 수도 있지만 다음과 같은 방법으로 출력할 수 있었다.

 

스크린샷 2021-04-11 오후 6 39 18

 

스크린샷 2021-04-11 오후 6 40 13

+) 3. 클릭하면 나오는 Download container 클릭.

 

스크린샷 2021-04-11 오후 6 42 52

패키지 내용 보기 클릭.

 

스크린샷 2021-04-11 오후 6 44 20

 

파일 다운로드 해보기

사진 말고 일반 파일도 다운로드 해보자. 여기서 말하는 일반 파일이란 word나 PDF 같은 사진이 아닌 파일이다.

 

아래 사진처럼 PDF 다운로드 버튼과 PDF Show 버튼을 만들었고, 위, 아래 순서대로 버튼을 눌러보면 PDF 파일이 잘 보여진다.

(자세한 코드는 역시 맨 아래에 첨부한다. PDFKit을 이용했다.)

 

Image from iOS (2)

 

Image from iOS (3)

이것 역시 device 자체의 폴더들에는 접근할 수 없었고, 앱 sandbox범위의 DocumentDirectory에 저장했다.

아래 사진에서 맨 마지막 컨텐츠로 우리가 다운 받은 pdf 파일이 잘 받아져 있는 것을 확인할 수 있다.

 

스크린샷 2021-04-12 오후 12 30 04

 

SQLite를 이용해 Local DB 만들어보기

SqlLite를 이용해 간단한 local db 를 하나 만들어보았다. (너무 간단하지만. 만들어본 것에 의의를 둔다.)

다음 시간에는 이것을 이용해 내가 걸어다닌 좌표들을 저장하고 그것들을 이용해 지도에 그려볼 계획이다.



 

언젠가는 SQLite 대신 Realm이라는 것을 써야겠다.

Realm은 SQLite와 같이 Local DB를 사용하고자 할 때 쓸 수 있는 라이브러리이다. Realm은 SQLite보다 매우 빠른 속도를 자랑하고

더 적은 메모리를 차지하는 동시대를 살아가는 모든 Device DB 관련 기술 중에서는 최고의 기술이다.

다만 러닝커브가 SQLite보다 훨씬 높아서 이번 테스트에서는 사용하지 않았을 뿐이다.

(아래는 Realm이 Write하는 속도도 빠르고 Read, Query하는 속도도 빠르다는 것을 나타낸다.)

 

 

 

Realm의 속도가 빠른 이유는 SQLite는 lock 방식을 사용하는 데 반해 Realm은 thread + MVCC(MultiVersion Cuncurrency Control, 동시성 제어) 방식을 적용했기 때문이다.

이게 무슨 말이냐면 예를 들어 Nation이라는 테이블과 College라는 테이블이 있다고 했을 때 SQLite에서는 Nation 테이블에 접근하여 데이터를 쓰려고 할 때 다른 테이블에는 접근하지 못하게 Lock을 걸어버린다. 그러면 Nation 테이블에서 해야할 작업들을 모두 마친 후에야 College 테이블에서 해야 하는 작업들을 해야 하기 때문에 기다려야 하는 시간 이 발생한다.

반면에 Realm에서는 thread마다 테이블에 접근할 수 있는 권한을 주고 각 thread는 서로 간섭하지 못하게 하는 방식을 선택했다. 이 경우 테이블에 대해 서로 다른 스레드가 중복하여 접근하는 문제가 발생한다. SQLite에서는 lock을 걸어 이 문제를 해결했지만 Realm에서는 MVCC라고 해서 git에서 무언가를 add하고 commit하는 것처럼 어떤 변화를 commit하도록 만들어 문제를 해결했다. 그러니까 Commit을 하고 해당 commit의 요청이 안전하게 이루어질 수 있는지 확인한 다음에야 DB를 업데이트한다는 것이다. 이 로직을 잘 설명하는 이미지가 있어 아래에 첨부한다.

 

스크린샷 2021-04-10 오후 4 41 04

출처 : https://academy.realm.io/kr/posts/threading-deep-dive/

 

스크린샷 2021-04-10 오후 4 41 20

출처: https://academy.realm.io/kr/posts/threading-deep-dive/

 

데이터 모델 구조 자체는 객체 컨테이너로 구성되어 있다. Realm rawSQL(생쿼리라고도 한다.)을 사용할 수 없으며 자체의 Realm API를 통해 실행된다. 그리고 그 방식은 ORM 방식이기 때문에 만약 Realm을 내가 배우게 된다면 생애 첫 ORM을 쓰게 되는 것이라 정말 한 번 사용해보고 싶었다.

 

그리고 Realm은 무복사(Zero-copy)를 적용하여 더 빠르게 DB로부터 데이터를 얻어올 수 있다고 하는데, 이게 무슨 말인지 잘 몰라서 한번 찾아보니 다음과 같았다.

 

무복사 - 어떤 파일을 전송할 때 유저는 커널에 파일을 요청하게 된다. 이 때 커널에서는 파일을 복사하여 클라이언트에게 반환해줘야 하고 또 소켓에 파일 데이터를 넣어줘야 한다. 그런데 이것은 같은 파일을 중복하여 넘겨주는 게 된다. 따라서 단순히 소켓에 데이터를 넣어주기만 하여 복사하는 행위를 없애는 것이 무복사이다.

소켓 - 소켓 - 소켓이란 네트워크상에서 동작하는 프로그램 간 통신의 종착점이(EndPoint)이다. 즉, 프로그램이 네트워크에서 데이터를 통신할 수 있도록 연결해주는 연결부라고 할 수 있다. Client와 Server 모두에 소켓이 생성되어야 한다. 서버는 특정 포트와 연결된 소켓을 가지고 컴퓨터 위에서 동작한다. 이 서버는 소켓을 통해 Client측 소켓의 연결이 있을 때까지 기다리고 있는다.(Listening) Client소켓에서 연결요청을 하면 Server 소켓이 허락을 하여 통신(연결)이 되는 것이다.

EndPoint - IP Address와 Port 번호의 조합. 최종 목적지를 의미한다. EndPoint의 예로 사용자의 디바이스나 서버가 있다.

 

 

후기

멘토링을 받으면서 점점 iOS 분야에 대한 자신감이 붙는 느낌이다. 특히 이번 시간의 경우 위치기반서비스에서 빠질 수 없는 GIS에 대해 공부할 수 있는 기회가 되었고, 또 iOS 개발자라면 반드시 알아야 할 local DB 및 File System에 대해 알 수 있었다. 이것들도 모르고 취업전선에 뛰어들었다면 나는 아마 매우 늦게 취직하거나 대우가 좋지 않은 곳에 취직을 했을 것이라고 확신한다.

이번 시간에는 Searching 시간이 너무 길어서, 42 network를 이용해 커뮤니티라도 하나 만들어 많은 사람들과 지식을 공유하는 장을 만들어봐야겠다는 생각이 많이 들었다. 최근에 커뮤니티 지원을 받기 시작하던데, 한번 지원해봐야겠다. :)

 

다음 시간에는 위치 정보를 받아서 로컬 DB에 저장하고, 그것을 지도에 표시해볼 것이다. 또한 당근마켓처럼 위치기반 푸시알림을 보내는 연습을 해볼 것이다.

 

 

이미지 다운 코드

import UIKit

class FileLoadDownloadViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()


    }


    @IBAction func touchUpDownloadButton(_ sender: Any) {

        let filenameIWant: String = "hotecho" + Date().description + ".jpg"
        guard let documentsURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        else {
            print("documentsURL을 가져올 수 없습니다.")
            return
        }
        let destinationFileURL =
            documentsURL.appendingPathComponent(filenameIWant)

        let nakimImageURL = "https://cdn.pixabay.com/photo/2020/09/11/10/46/landscape-5562780__480.jpg"
        let fileURL = URL(string: nakimImageURL)

        let sessionConfig = URLSessionConfiguration.default
        let session = URLSession(configuration: sessionConfig)


        let request = URLRequest(url: fileURL!)
        let task = session.downloadTask(with: request) {
            (tempLocalURL, response, error) in
            if let tempLocalURL = tempLocalURL, error == nil {
                // 성공 시
                if let statusCode = (response as? HTTPURLResponse)?.statusCode {
                    print("successfully downloaded.")
                    print("statusCode: \(statusCode)")
                }

                do {
                    let data = try Data(contentsOf: tempLocalURL)
                    try data.write(to: destinationFileURL)
                    print("destionationFileURL: \(destinationFileURL)")

                    do {
                        let savedImageData = try Data(contentsOf: destinationFileURL)
                        if let savedImage = UIImage(data: savedImageData) {
                            DispatchQueue.main.async{
                                self.imageView?.image = savedImage
                            }
                        }
                    } catch (let loadingImageError) {
                        print("Error - 이미지 로드중 에러")
                    }
                } catch (let writeError) {
                    print("Error - 파일을 쓰는 도중 에러가 남: \(writeError)")
                }
            } else {
                print("Error - 파일 다운로드 중 에러")
            }
        }
        task.resume()
    }

}

 

PDF 다운 코드

import UIKit
import PDFKit

class PDFDownloadViewController: UIViewController {

    var pdfURL: URL?
    let pdfView = PDFView()

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewDidLayoutSubviews() {
        self.pdfView.frame = self.view.frame
    }

    @IBAction func downloadButton(_ sender: Any) {
        guard let url = URL(string: "https://www.tutorialspoint.com/swift/swift_tutorial.pdf")
        else {return}
        let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
        let downloadTask = urlSession.downloadTask(with: url)
        downloadTask.resume()
    }

    @IBAction func openPDF(_ sender: Any) {
        self.view.addSubview(pdfView)
        if let document = PDFDocument(url: pdfURL!) {
            pdfView.document = document
        }
    }

}

extension PDFDownloadViewController: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("temp file downloaded to", location)
        guard let url = downloadTask.originalRequest?.url else {return}
        let docsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
        let destinationPath = docsPath?.appendingPathComponent(url.lastPathComponent)

        try? FileManager.default.removeItem(at: destinationPath!)
        do {
            try FileManager.default.copyItem(at: location, to: destinationPath!)
            self.pdfURL = destinationPath
            print("file downloaded to:", self.pdfURL ?? "failed")
        } catch let error {
            print(error.localizedDescription)
        }
    }
}

 

DB Creation 코드

import Foundation
import SQLite3

class DBHelper {
    var db: OpaquePointer?
    var path: String = "myDB.sqlite" // DB_name, 확장자 지킬 것.
    init() {
        self.db = createDB()
        self.createTable()
    }

    func createDB() -> OpaquePointer? {
        let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathExtension(path)

        var db: OpaquePointer? = nil

        // return something into db(2nd parameter)
        if sqlite3_open(filePath.path, &db) != SQLITE_OK {
            print("There is error in creating DB")
        } else {
            print("Database has been created with path \(path)")
            print("path to DB is : \(filePath.path)")
            return db
        }

        return db
    }

    func createTable() {
        let query = "CREATE TABLE IF NOT EXISTS grade(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, avg INTEGER, result TEXT, list TEXT);"
        var createTable: OpaquePointer? = nil

        if sqlite3_prepare_v2(self.db, query, -1, &createTable, nil) == SQLITE_OK {
            if sqlite3_step(createTable) == SQLITE_DONE {
                print("table creation success")
            }
            else {
                print("table creation Failed")
            }
        } else {
            print("sqlite3 prepare v2 error")
        }
    }
}
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기
// custom