- 총 20부작 응8 줄거리 입니다. 가능한한 내용을 모두 담으려 하다보니 내용이 좀 길어졌지만 3~4분이면 한 회를 읽을 수 있을 정도 입니다. 전철이나 조용한 도서관에서 눈으로 읽기에 적당한 분량입니다. 유튜브는 보고 나면 하나도 안 남죠? 공부하다가 일하다가 잠시 휴식할 때 눈으로 즐감하세요.
1988년, 서울 올림픽이 열리던 해. 그 시절, 기억나세요?
데모, 넉넉치 못한 환경, 마음만은 따뜻했던 아날로그 시대, 워크맨, 청바지, 신해철, 왕조현, 소머즈, 소피마르소, 키메라 선생님, 레밍턴스틸, 탐크루즈, 리처드기어, 뉴키즈 언더 블럭, 소방차, 다이하드, 인디아나 존스, 행복은 성적순이 아니잖아요, 라밤바, 빅, 지옥의 묵시록, 유콜잇러브, 탑건, 마지막 황제, 레인맨, 영웅본색2
고딩 2년...
덕선네 저녁 식사 - 오늘은 덕선이네 아빠 월급날, 금융 일을 하는 아빠, 하지만 마음씨 좋은 아빠가 빚보증을 잘못서서 집안 형편이 많이 안좋다. 가뜩이나 적어진 월급... 가져올 때마다 자꾸 형편 어려운 후배를 위해 책도 사주고, 물약도 사주고, 퇴근길에 할머니 콩나물도 한 솥 사오고... 그래서 엄마가 화내는 중. 이 집은 조용할 날이 없다.
아빠: "내가 어찌 모른 척 하겄는ㄱ...?"
엄마: "거(기)만 망했나?!! 우리도 망했다, 누가 누굴 도와주노, 지금!!"
아빠: "그래도 우린 가족도 다 건강하고... 그라고 뭐냐, 응, 공부도 잘하고!"
엄마: "덕선이 이번에 999등 했다, 노을이는 1000등이다 천등!!"
아빠: "우리 보라 있잖에, 보라, 대한민국에서 젤로 가는 서울대 학생아녀!!!"
성격 쾌활한 덕선이, 서울 올림픽 피켓걸로 뽑혀서 TV에 나올지도 몰라 밤 마다 피켓들고 워킹 연습 중, 학교에서는 2교시 수업만 하고 경기장에서 연습하고 그랬는데... 덕선이와 언니 보라는 성격이 180도. 웬수 지간.
정환이네 - 스틸 컷이라서 뒤에 정환이네 아빠가 잘 안 보이는데, 저거 부채도사 개그 하는 중임. 개그가 생활임.
아줌마는 그 개그들을 너무 너무 싫어해서 이혼 하겠다며 협박 하기도 하는데, 그래도 개그하다 간혹 얻어 터짐
이 동네에서 딱 하나 덕선이만 정환이네 아빠 개그에 정말 재밌게 리액션을 해 줌.
선우네 - 아빠 죽은 후 동생 진주와 엄마, 그렇게 셋 이서 살고 있음. 이 동네에서 제일 가난함.
선우네 엄마는 동네 잔일 찾아 다니며 돈이 없을 때 정환이네 엄마한테 자주 빌림. 선우는 전교 회장임.
정환이네 엄마가 이 동네 아줌마들의 언니 역할을 함
갖고 싶은게 많지만 덕선이 꺼는 별로 없어요. 집안 형편도 생각해야 하고. 그래도 성격 좋은 덕선이는 잘 참아요.
선우는 엄마한테 학교에서 있었던 일 하나도 빼지 않고 세세하게 다 얘기하는데, 정환이는 엄마한테 하나도 얘길 하지 않음. 그래서 애들 얘기 할 때 정환이네 엄마는 할 말이 없음. 공부는 반에서 상위권인데 시험 봤었는지, 학교에 무슨 행사가 있었는지, 친구들과 뭘 하는지, 준비물 뭐가 필요한지 아무것도 모름.
덕선 엄마 : "아이고, 우리 정환이 큰 일 날 뻔했네, 다친데는 없고?"
선우 엄마 : "다친덴 없고 (깡패한테) 돈하고 운동화만 빼앗겼단다. 우리 정환이가 즈그 엄가 걱정할까봐 말 안했나보네"
정환 엄마 : (...)
보라 생일 (며칠 차이 안나는 덕선이 생일도 겸사 겸사 한 꺼번에 하는 걸로)
생일상인데, 덕선이 왜 우냐고? 올해는 제발 생일상 따로 차려 달라고 했는데, 올해도 또 언니 생일에 촛불만 껐다 다시 켜놓고... 고2인데, 화가 안나겠어요? 서럽죠. 밥 먹을 때마다 계란 프라이는 노을이가 먹고, 덕선이 한테는 콩자반만 주고, 닭다리는 언니가 먹고...노을이만 아이스크림 사주고... 더군다나 오늘 덕선이는 피켓걸에서 잘렸어요. 마다가스카르가 정치문제로 올림픽 불참 통보를 해 왔거든요. 그 사실을 아무한테도 말 못하고 너무 속상했던 덕선이. 그러다가 생일상에서 서러움이 확 터져 버렸어요
이 날도 뭐 그리 특별한 건 없었다. 둘째 딸의 서러움이야 늘 그랬으니까... 세상의 모든 둘째들이 그렇듯이 언니는 언니라서 동생은 동생이라서 항상 양보하며 살아야 했다. 그래도 나의 숭고한 희생 정신을 엄마 아빠만은 당연히 알고 있을 거라 생각했었는데, 아니었다. 어쩌면 가족이 제일 모른다.
1988년 9월 17일 (제 24회) 서울 올림픽 개막!!
덕선이 정말 운 좋게도 벌점 많아 짤린 애 대신 선수단 피켓을 들게 됐다.
정환이 방 : 정환아, 엄마 한테 할 말 없니?
개막 행사 끝나고 집에 오는데, 덕선이를 기다리던 아빠.
생일 축하 다시 해주는 아빠. 아빠 엄마가 미안해! 잘 몰라서 그래. 첫째딸은 워떠케 갈쳤는지, 둘째는 워떠케 키웠는지 막둥이는 워떠케 사람 맹글어야 될 지 몰라서. 이 아빠도 태어날 때부터 아빠가 아니잖어. 아빠도 아빠가 처음인께. 그런께, 우리 딸이 쫌만 봐줘. 우리 딸이 언제 이렇게 예쁘게 잘 커서 텔레비전에도 나오고... 그나저나 우리 덕선이 시집가버리면 아빠 서러워서 워떠케 사나... ^^
결국 가족이 제일 모른다. 하지만 아는 게 뭐 그리 중요할까? 결국 벽을 넘게 만드는 건 시시콜콜 아는 머리가 아니라 손에 손잡고 끝끝내 놓지 않을 가슴인데 말이다. 결국 가족이다. 영웅, 아니 영웅 할배라도 마지막 돌아갈 제 자리는 결국 가족이다. 마지막에 보듬어 줄 내편, 결국 가족이다.
<<응답하라1988 줄거리 전체 목록>>
<<대조영 줄거리 전체 목록 >>
<<W 더블유 줄거리 전체 목록 >>
<<영화 줄거리들 전체 목록 >>
'(눈으로 읽는) 영화 드라마' 카테고리의 다른 글
[응8] 응답하라1988 줄거리 다시보기 (Reply 1988) - 3화 (유전무죄 무전유죄) (0) | 2024.06.25 |
---|---|
[응8] 응답하라1988 줄거리 다시보기 (Reply 1988) - 2화 (당신이 나에 대해 착각하는 한 가지) (2) | 2024.06.24 |
써로게이트 (Surrogates, 2009) - 98%가 아바타인 세상 (0) | 2024.06.19 |
[눈으로 보는 영화] 승리호(Space Sweepers) Review 줄거리, 2021 SF (0) | 2021.02.07 |
[눈으로 보는 영화] MINARI, 미나리 Review 줄거리, 2020 (0) | 2021.02.01 |
써로게이트 (Surrogates, 2009) - 98%가 아바타인 세상
써로게이트 (Surrogates)
감독 : 조너선 모스토
주연 : 브루스 윌리스
써로게이트 란?
사전적 뜻으로는 대리, 대행자를 의미합니다.
영화 속에서 '써로게이트'는 인간을 대신하는 아바타 로봇들을 말합니다. 자신의 아바타 로봇인 써로게이트는 기본형 모델이 대량 생산되어 구매자의 신체와 원하는 바에 따라 옵션을 추가하여 판매됩니다. 써로게이트 구매자는 성별, 나이, 외모등을 원하는 대로 바꿔 완벽한 인간 모습의 로봇을 받게 됩니다.
사용자는 의자에 누워 오로지 생각만으로 써로게이트를 조정하며 일도 하고 즐기기도 하는 일상을 살아갑니다. 실제 몸은 식사나 화장실 갈 때, 잠잘 때나 사용하는 거죠. 참고로 신체 기능과 근육은 점점 퇴화하게 됩니다. 써로게이트가 동작 중 완전히 부서져도 조정을 하던 사용자는 아무런 해가 없습니다. 무선 통신으로 제어만 하는 것이므로 당연히 써로게이트 로봇만 손상되는 거죠.
써로게이트가 일상에 거의 전부 보급된 이후로 99% 시민들의 생활은 모두 써로게이트들에 의해 이뤄지고 있고 범죄율은 현저하게 낮아졌습니다. 그런데... 그런 평화로운 일상에서 써로게이트 공격 사건이 발생하고 공격 당한 써로게이트의 손상이 실제 사용자의 죽음으로 까지 이어지는 말도 안되는 전대미문의 사건이 터지면서 영화가 시작됩니다.
▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒
줄거리
공격받은 써로게이트는 둘 이었는데, 갑부집 아들로 보이는 남성 써로게이트가 클럽에서 만난 여자와 함께 으슥한 골목길에서 한 무장 괴한이 쏜 의문의 전자 빔을 맞고 손상되어 있었다.
이에 출동한 FBI 요원, 그리어(브루스윌리스 분)와 피터스.
지역 경찰이 FBI 출동까지 요청한 이유는 남성 써로게이트의 사용자 등록 정보가 조회되지 않아 수상했기 때문.
함께 공격받은 여성 써로게이트는 안구가 완전히 전소되어 박살이 났고 사용자는 뚱뚱한 대머리 남자였는데 써로게이트를 조정하던 자세로 피를 흘리며 사망한 상태였다.
여기서 잠깐, 브루스윌리스가 좀 젊어 보이죠? 써로게이트 거든요~
퇴근한 그리어는 써로게이트를 충천 캡슐에 위치시킨 후 침대에서 실제 자신의 몸을 일으킵니다.
그리어의 아내는 교통 사고로 아들을 잃은 후 방에 틀어박혀 써로게이트 생활에서 거의 나오지 않고 있습니다. 아내의 얼굴 본지가 가물가물...
그리어 : "우리 너무 멀어진 것 같아"
아내 : "매일 보잖아"
그리어 : "써로게이트만 보잖아"
아내 : "그게 훨씬 나아"
며칠 뒤 또 다른 써로게이트가 같은 형태의 공격을 받고 사용자가 피를 흘리며 사망하는 사건이 재차 발생.
FBI 조사 중 밝혀진 사망자들 사인의 공통점은 써로게이트와 통신 중에 뇌가 녹아서 죽었다는 거.
그리고 최초 사건의 미등록된 남성 써로게이트 사용자의 아버지가 써로게이트 최초 개발자인 '라이오넬 캔터' 박사였음.
캔터 박사는 써로게이트 개발 및 생산업체인 VSI의 공동 대표였는데, 동업자와 의견이 맞지 않아 회사에서 쫒겨나 은둔중이었다. 그러던 중 아들에게 자신의 멋진 써로게이트를 빌려줬었던 것인데, 이번 사건으로 아들을 잃은 것이다.
(무장 괴한은 누군가의 사주를 받고 그 써로게이트를 조정하는 사람이 캔터 박사일 것이라 확신하고 공격했던 것임. 아버지 대신 아들이 죽었던 것. 아참, 써로게이트를 빌려주는 것은 불법임)
(바비는 써로게이트를 절대로 사용하지 않는 사람임 - 써로게이트가 자신의 뛰어난 두뇌를 감당 못한다나...)
그리어는 사건 조사차 VSI에 방문했다가 AS기사를 통해 이번 사건들과 유사한 손상이 군에서도 입고됐었다는 말을 듣고군이 연관되어 있음을 미루어 짐작한다. 그러나 군 관계자들은 전자총 개발 사실이 없다며 강하게 부인.
(그러나 브루스윌리스를 속일 순 없지요!!)
〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓
써로게이트를 배척하는 사람들은 없을까요? 당연히 있습니다. 시민들의 1% 정도 밖에 안되지만요. 이들 단체를 '드레드'라고 불렀는데 써로게이트 법 통과를 반대해 왔었죠. 그래서 그들을 중심으로 진짜 인간만 들어갈 수 있는 써로게이트 출입 금지 구역들('드레드들의 구역' = '보호구역')이 도시마다 조성되었고 기계라는 기계는 모두 배척했습니다. 일도 전통적인 방식으로만 했고 마차도 다니고... 때문에 보호구역은 굉장히 낙후된 지역처럼 보입니다. 그 지역들 전체의 리더는 '자이르파월' 입니다. 예언자라고도 불리네요.
처음에 써로게이트를 공격했던 무장 괴한이 강도 짓 하다가 잡혔으나 무슨 이유에서인지 경찰이 순순히 석방했다는 이력을 알게 된 그리어(브루스윌리스 분)는 헬기를 타고 다시 나타난 놈을 추적한다. 그 놈은 전자총의 출처, 경찰과 유착 관계들을 알 수 있는 키맨이니까.
궁지에 몰린 괴한은 또 다시 그 이상한 전자총을 쏴서 모든 경찰들(경찰도 모두 써로게이트임)을 무력화 시키고 헬기 까지 떨어뜨린 후 보호 구역으로 도망친다.
그리어 써로게이트가 보호 구역 끝까지 괴한을 추격했지만 그 곳 주민들에 의해 그리어 써로게이트만 완전 박살나고 괴한은 오히려 리더였던 자이르파월에 의해 무기의 출처와 누가 캘든 살해를 사주했는지 추궁당하다가 살해된다. 의도치 않게 써로게이트로 보호 구역까지 침범했던 그리어는 정직 처분을 받아 수사를 더 이상 진행할 수 없게 되었고 설상가상으로 피터스는 또 다른 괴한(캔터 박사의 비서 같아 보이는??)에 의해 살해되고 그녀의 써로게이트를 탈취당한다.
정직 상태였으나 그리어는 국방부로 찾아가 자신이 전자총이 있는 위치를 알고 있다며 알려주는 댓가로 전자총에 대한 정보를 요구한다.
'그 장치는 OD라는 과부하 장치요'
VSI가 개발했고 바이러스로 써로게이트를 불능에 빠뜨리는데, 예상치 않게 사용자까지 죽이는 것이 확인되어 테스트 직후 전량 폐기했단다. 그러나 한 대가 사라져 국방부에서도 찾고 있는 중이었단다. 그리어의 제보로 국방부가 OD를 회수하기 위해 보호 구역으로 출동하는 그 때, 보호 구역의 자이르파월은 추종자들에게 OD를 피터스 요원(써로게이트)에게 넘겨주도록 지시한다.
보호 구역에 투입된 군 병력이 OD 회수 작전을 수행 하던 중 자이르파월을 사살하게 됐는데, 이게 뭔 일이래?
써로게이트를 출입을 엄격하게 금지하고 인간들끼리 모여 살 던 그 지역의 지도자였던 자이르파월도 써로게이트 였음!!!
그리고 충격적인 게, 자이르파월의 사용자는 캔터 박사였음!!
캔터 박사는 사회적 약자들에게 도움을 주려고 써로게이터를 개발했던 것인데, 온 세상이 기계화 되고 기계들만 돌아다니는 삭막한 세상이 되어버려서 써로게이트 배척에 앞장서 왔던 것! 때문에 VSI에게는 눈엣 가시같은 존재가 된 것.
자이르파월이 캔터 박사였고 OD를 피터스에게 전하라고 했으니까... 설마 피터스도 역시 캔터 박사였군?!! 그럼 피터스를 죽인 범인도 캔터 박사네...
캔터 박사는 발 빠르게 움직였다. 피터스 써로게이트를 조정해 FBI 회계자료철에서 스톤 부서장이 배후임을 알아낸다.
회계 자료 상, 이번 사건을 일으킨 무장 괴한에게 FBI에서 지금 껏 월급을 지급해 오고 있었던 것. 말하자면 그 괴한은 FBI 요원이었던 것이다! 그러니까 이번 사건의 실상은 그러니까 VSI가 FBI 를 움직여 캔터 박사를 죽이려 했던 거...
〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓 〓
피터스 써로게이트는 아직 자신의 정체를 모르는 그리어를 만나 마침 스톤의 써로게이트 접속 코드를 알아낸 그리어를 통해 스톤의 정보에 접근해 OD의 작동 코드를 알아내고 그 즉시 교통사고로 위장해 그리어를 죽게 만들고 FBI 써로게이트 감시 통제실로 간다. (그리어 아들이 교통 사고로 죽었다는 걸 알고 있던 캔터 박사인데... 캔터 박사도 냉혈한이네...)
.....그러나 주인공 브루스윌리스는 안 죽는다......
감시 통제실은 곧바로 피터스 써로게이트(캔터 박사)에 의해 장악되고 피터스는 OD의 바이러스 정보를 업로드해서 모든 써로게이트와 사용자들을 없애려 든다. (그럼 시민들 99% 전부를? 완전 돌았군!!!)
달려 온 스톤이 캔터 박사를 알아보고 설득해 보려 하지만...
그러나 캔터 박사는 별 말 없이 아들이 죽은 것과 똑같은 방식으로 OD를 발사해 스톤을 죽여버린다.
한편, 죽지 않고 겨우 살아난 우리의 브루스윌리스는 캔터 박사의 저택으로 쳐들어 간다.
개발자 캔터 박사의 저택에는 자이르파월 뿐만 아니라 많은 종류의 써로게이트들이 있었다. 당연히 VSI 제품보다 더 진보된 것도 있었다.
캔터 박사는 OD의 바이러스가 100% 업로드된 것을 확인하자마자 독약 캡슐을 삼키고 자살해 버린다. 말릴 틈도 없었다. 그런데 지금 중요한 건, OD의 바이러스로 모든 써로게이트와 지구상의 모든 사용자가 살해될 거라는 거. 살해 프로세스가 이미 시작됐고 그리어의 아내도 써로게이트와 접속하고 있잖은가? 다급해진 그리어. 캔터 박사를 의자에서 밀어내고 피터스 써로게이트에 접속한다. 그리고 바비(감시 통제실 운영자. 현재 피터스가 수갑으로 기둥에 묶어놓은 상태라서 움직이지도 못함)의 안내에 따라 허겁지겁 동작 중지 신호를 보내려 애를 쓰는데... 일단 사용자들의 접속 부터 끊기 시작...!!!
'사용자들은 구했고 이제 써로게이트의 파괴를 막아야 해요!, 오른쪽 터미널에서 Yes를 누르세요!!'
시간 없는데, 모든 써로게이트가 파괴 될텐데... 그리어는 왜 망설이고 있냐고...
그 순간 통제실 입구에 FBI가 진입하여 피터스 써로게이트를 곧바로 사살한다. 그러나 피터스 써로게이트는 파괴되기 직전에 No 키를 눌렀다. 세상의 모든 써로게이트 여러분, 영원히 안뇽~
이로써 모든 써로게이트가 파괴돼 버렸다!
거리의 사람들도 쓰러지고 차들은 서로 충돌하고 지하철 승객도, 상점의 써로게이트들 모두가 쓰레기가 되어 버렸다.
단 한 사람의 사상자도 없이 사람들은 무사했다. 접속이 끊기자 무슨 일인가 하고 밖으로 나와 보는 진짜 사람들...
햇볕에 눈도 부시고 걷기도 힘들고 허리도 아프고... 이 얼마만의 외출들이냐?
그리어의 아내도 방문을 열고 거실로 나와 그리어와 만난다. (대체 몇 년 만인가?)
모든 써로게이트들이 파괴됐는데, 인간 생활은 퇴보하게 될까요?
FBI가 모든 써로게이트를 비밀리에 감시 통제하고 있는 것으로 봐서 서로게이트와 사용자에 대한 개인 정보 보호가 안전하고 사생활도 보장되고 있다는 건 전부 구라였음. 그러나 한편으로는 이런 통제가 없으면 사고에 대비하기가 어렵지...
그리고 써로게이트가 파손되어도 그 사용자에게는 아무런 영향이 없다는 것도 구라였음. 대량의 정보를 뇌와 써로게이트간에 강제적으로 주고 받아야 구현할 수 있는 기술이므로 영향이 전혀 없을 수도 없겠지...
써로게이트와 같은 시대는 반드시 도래할 것이다. VSI와 국가 기관간의 발생할 필연적 유착관계를 어떻게 봐야 할까?
어쩌면 우리가 죽기 전에 이런 아바타들의 세상이 펼쳐질 수도 있지 않을까요?
'(눈으로 읽는) 영화 드라마' 카테고리의 다른 글
[응8] 응답하라1988 줄거리 다시보기 (Reply 1988) - 2화 (당신이 나에 대해 착각하는 한 가지) (2) | 2024.06.24 |
---|---|
[응8] 응답하라1988 줄거리 다시보기 (Reply 1988) - 1화 (손에 손 잡고) (1) | 2024.06.23 |
[눈으로 보는 영화] 승리호(Space Sweepers) Review 줄거리, 2021 SF (0) | 2021.02.07 |
[눈으로 보는 영화] MINARI, 미나리 Review 줄거리, 2020 (0) | 2021.02.01 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (15~16/16화) (1) | 2019.08.29 |
C# 문법 되새김 1 -- namespace, using 키워드
A. namespace 키워드
namespace란?
프로그램은 메모리 상에 수 많은 저장 장소('변수')를 생성해서 사용하고 각 변수에 이름을 붙여 구별합니다.
프로그램 기법이 발전하면서 단순한 변수 외에도 같은 소(小)작업에 동원되는 여러 변수들을 하나로 묶어 변수들의 배열 같은 모습의 대형 변수를 만들 필요가 있었습니다. 그리고 이것을 구조체라고 불렀습니다. 그 이후 프로그램 기법은 구조체 내에 그 구조체 안에서만의 특별한 동작 기능을 추가(함수인데 특별히 '메소드'라 부름) 하고 이를 '클래스'라고 부르게 되었습니다. 이 클래스는 하나의 작은 프로그램 모듈의 붕어빵 틀같은 모양을 하고 있으며 그 자체로는 실행될 수 없고 실제 런타임 때, new 라는 키워드에 의해서 메모리에 업로드 되어 비로소 실행되는데, 이렇게 메모리에 업로드되어 하나의 독립 모듈이 된 상태를 '객체'라고 부릅니다. 즉, 프로그램 기법이 구조체를 도입하면서 급발전하여 곧바로 클래스를 만들어 객체화(캡슐화)시키자는 방향으로 발전한 것 입니다. 여기서 객체의 설계도라고 할 수 있는 것이 클래스 입니다. 객체화 프로그래밍의 장점은 코드를 편리하게 재사용 할 수 있고 수 많은 변수들을 체계적으로 분류하여 프로그래밍의 효율성을 높인다는 데에 있습니다.
다시 한 번 요약하자면, 어떤 소(小)작업을 위한 여러 변수들과 함수를 한 데 묶어 클래스라는 붕어빵 틀을 만들었습니다.
붕어빵 틀이 있으니 '이 틀로 객체를 만들어라!' 하는 명령만으로 무한대의 똑같이 생긴 붕어빵 객체들을 만들 수 있게 된 거죠. 때문에 현대의 프로그램에서 클래스는 소스 코드 작성의 핵심입니다. 그리고 그 클래스라는 붕어빵틀은 복사, 기능추가, 다형성(후에 설명함), 은폐성, 개선된 가독성등에서 강력한 잇점들을 프로그래머들에게 안겨 주었습니다.
C# 프로그램 소스에는 많은 클래스들이 포함되어 있습니다.
네임스페이스란, 대략적으로 말하자면 클래스가 나열된 공간입니다. (= 클래스들의 덩어리)
하나의 네임스페이스 안에서는 동일한 이름을 가진 클래스가 존재하면 안됩니다. 구별할 수 없으니까요.
클래스명의 중복을 불허 한다는 것은 하나의 네임스페이스 안에서의 얘기일 뿐, 다른 네임스페이스에 같은 이름의 클래스명이 있냐 없냐는 문제되지 않습니다. 네임스페이스란 이렇게 클래스들을 무리지어 놓는 하나의 구획입니다.
미리 정의되어 있는 대표적인 C#의 편리한 네임스페이스에는 System 이 있습니다.
B. using 키워드의 용도
using 키워드로 다른 네임스페이스에 정의된 클래스를 가져오거나
네임스페이스의 별칭을 정의해서 사용할 수 있습니다.
1. 다른 네임스페이스에 있는 클래스 가져오기
형식)
using 다른네임스페이스명;
using 다른네임스페이스명.클래스명; // 하위 클래스들은 .으로 구분
예문)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
2. 별칭 정의하기
C#의 네임스페이스 이름이나 클래스 이름들이 정말 긴 경우가 많습니다.
실생활 용어도 약어를 많이 사용하는데, 프로그램 코드에서도 약자를 쓰고 싶어 근질근질 합니다.
사용하는 전처리 지시자는 using (소문자임!) 입니다.
using으로 간략화 할 수 있는 대상은 네임스페이스 이름과 클래스 이름 뿐 입니다.
형식)
using 별칭 = 네임스페이스명;
using 별칭 = 네임스페이스명.클래스명;
예문)
using con = System.Console; // 별칭을 정의하고 있는 using 키워드
class Program
{
static void Main(string[] args)
{
con.WriteLine("으하하");
con.ReadLine();
}
}
별칭 사용시 고려할 점)
여러 사람들과 코드를 공유하면서 개발 하는 경우라면, 별칭 사용을 자제 하는 것이 좋을 수도 있습니다.
자신은 알아보기 쉽겠지만, 다른 사람들에겐 불편할 수 있으니까요!
'프로그래밍' 카테고리의 다른 글
[도서] 시작의 기술 -개리비숍-
엘버트 엘리스 : 현대 심리학의 아버지
신경가소성 : 생각이 뇌의 물리적 구조를 바꾸는 현상
마르쿠스 아우렐리우스 : 로마황제가 된 스토아 학파 철학자
"상처 느끼기를 거부하면 상처 자체가 사라진다"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
안녕하세요?
여러 분은 문득 '과거에 내가 그 일을 했어야 했는데...'
또는 '시도해 봤어야 했는데...' 하고 느끼신 적 없으신가요?
예나 지금이나 어떤 새로운 일을 시작하는데 있어서 많은 망설임을 갖는 분들에게 좋은 조언서가 있어 소개합니다.
소개해 드릴 책은 <<시작의 기술>> 입니다.
저자인 개리비숍은 스코틀랜드 태생으로 1997년 미국으로 건너가 자기계발 코치로서 활동해 오고 있습니다.
삶의 진정한 변화를 원하는 사람들에게 그들 마음 속에 가지고 있는 '진정한 나'를 일깨워 새로운 삶을 시작할 수 있도록 계기를 마련해주고 있죠.
그는 '자기 대화가 자신의 행동에 절대적인 영향을 준다' 라고 말합니다.
그리고 생각의 변화가 결국 행동의 변화를 이끌죠.
바꾸기 힘든 생각은 끈질긴 부단함을 통해 행동 함으로써 변하도록 이끌 수 있답니다.
그래서 평소에 7가지의 자기 암시어를 권해주면서 여러 분의 '시작'을 독려하고 있습니다.
잠깐 읽어 볼까요?
혹시 모르지요? 이 블로그에서 10분간 읽은 짧은 글이 여러분에게 어떤 계기를 만들어 줄 지...? ~~~ ^^
ππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
본 서 내용
ππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
한가지 행동이 자동적으로 나올때까지 그 행동을 반복하면 습관이 된다.
마찬가지로 강력하고 '단언적인 언어'를 오랫동안 사용하면 삶에 영구적인 변화를 일으킬 수 있다. 단순히 행복하다는 생각만 드는 정도가 아니라 뇌의 생물학적 구조까지 영향을 미칠 수 있다는 말이다.
기억하라. 당신을 둘러싼 여건이 아무리 힘들고 버거워도 결과를 가장 크게 좌우하는 것은 그 환경을 이해하고 대처하는 당신의 태도다. 해답은 항상 당신 안에 있다.
'단언 형태의 자기대화'란, 지금 당장 여기서 내가 이 순간의 주인임을 천명하는 것이다.
즉, '(미래에)~가 될거야'라는 서사보다는 '나는 ~이다', '나는 ~를 환영한다/받아들인다', '나는 ~라고 단언한다'라는 말들을 뜻한다.
7가지 단언
1. "나에게는 의지가 있어!"
운, 환경, 남 탓을 그만하라
당신의 인생 의 한계는 당신이 참고 있는 만큼까지다. 맘에 안드는 것을 과감히 변화시켜라. 그 이후는 당신이 주도하게 될 것이다.
세상을 볼 때, '내가 원하는 듯' 보이는 것과 '원하지 않는 듯' 보는 관념에서 벗어나라.
그 대신 나에게 '의지가 있는 것'과 '의지가 없는 것'의 렌즈로 보기 시작하면 갈등이나 미련없이 또렷하게 볼 수 있다.
행동했는데 효과가 없으면 어쩌냐고? --> 효과가 없으면 어떠냐?
그리고 누구나 마음속에서는 일을 실제보다 크게 키워 생각하는 경향이 있는데 겁 먹지 마라. 중요한 것은 '의지가 있냐?'이다. 일이 실제 크다면 잘게 쪼개서 격파해 나가라. 당신 인생에 개입하라.
항상 자신에게 질문해봐라. '의지가 있는가?'
2. "나는 이기게 되어 있어!"
일상에서 우리 일의 95퍼센트를 통제하는 것은 무의식이다.
지금 현재 자신의 위치는 지금까지의 행동의 결과이고 인식했던 못했지만 무의식적으로 스스로가 추구해 온 것이고 나름대로 이 방향으로 성공해 온 것이다. 당신이 무의식적으로 추구한 것들은 결국 이기게 되어 있다. 그 마음의 방향을 긍정적으로도 부정적으로도 이끌 수 있는 것은 바로 당신이다.
'정복되지 않는다는 게 마음이 가진 힘이다' -스토아 학파 세네카-
3. "나는 할 수 있어!"
의시소침해 있다면 지급까지 겪어왔던 과거를 돌이켜 봐라. 직면했던 모든 일들이 지금은 결국 극복되어 지금에 이르지 않았는가?
기억하라, 이 세상에 풀지못할 문제는 없다. 문제가 생기면 당당히 직면하라.
종종 해결챋이 보이지 않으면 잠시 물러나 생각해봐라.
'나는 할 수 있어' 라는 말은 당신이 모든 해결책을 가지고 있다는 뜻이 아니다. 다만 당신이 운전대를 잡고 있고 결정권이 당신에게 있다는 뜻이다. 언제나 아름답지만은 않을 것이다. 언제나 그래왔듯이 당신은 이번에도 극복하게 될 것이다.
과거에 봉착했던 갖가지 난관들이 어떤 방향으로든 극복되어 왔음을 상기하라.
4. "나는 불확실성을 환영해"
항상 확실하게 성공하는 것이 아니다. 열심히 해도 보장이란 없는 것이다.
당신이 편안함만을 추구한다면 결코 앞로 나아갈 수는 없다.
결정적인 순간이 왔을때,
최선은 옳은 일을 하는 것,
차선은 틀린일을 하는 것이고,
최악은 아무것도 하지않는 것이다.
- 시어도어 루즈벨트 -
루즈벨트의 말을 곱씹어 봐라. 당신이 저지를 수 있는 최악의 행동은 목표를 빗맞추는 것이 아니다. 목표를 쏘지 않는 것이다. 성공한 사람들은 당신이 갖지 못한 무언가를 가지고 있는 것처럼 보이겠지만 그렇지 않다. 그들의 성공은 불학실하다고 생각될 때 그만두지 않았기 때문이다.
5. "시작이 아니라 행동이 나를 규정해!"
행동을 해야 인생이 바뀌는 것이지, 생각하는 것만으로는 인생이 바뀌지 않는다.
아이러니컬하게도 다음 두 이유 때문에 '행동'하는 것이 생각을 바꾸는 제일 빠른 '방법'이다.
첫째,
행동의 결과가 옳은 현실을 만들어 내면 생각도 거기에 맞게 바뀐다.
뭔가에 완전 몰입을 할때 그와 관련된 모든 문제와 부정적인 생각들이 사라지는 것을 경험해 본 적이 있을 것이다.
둘째,
실행과정 중에 떠오르는 부정적인 생각은 전체 과정의 일부로 받아들이고 차분히 다음 행동에 착수하면 부정적인 생각들이 걷힌다.
행동을 바쁘게 하다보면 다른 것을 생각할 겨를이 없다.
일단 움직이기 시작하면 계속 움직이는 건 어렵지 않다.
아무것도 하지 않으면 의심과 공포가 생긴다.
행동하면 자신감과 용기가 생긴다.
두려움을 정복하고 싶다면 집에 앉아서 생각만 하지말고
밖에 나가서 바쁘게 움직여라
- 데일 카네기-
6. "나는 부단한 사람이다!"
무슨 일이 생겨도 계속 움직이고, 또 움직이고 움직이게 해주는 계기, '부단함!'.
당신 인생에서 사장 성공했던 일을 떠올려 봐라. 당신이 어떡해 했는지는 모르지만 한가지는 확실하다. 당시에 당신은 편안하지 않았을 것이다. 달리 말해 당신은 안전지대 밖에서 활동하고 있었을 것이다. 실제로 당신이 경험하는 불편과 어려움이 클수록 이후에 느끼는 개인적 성취감도 크다.
현재 위치가 어딘지 얼마를 왔고 얼마를 더 가야 하는지 모를때 당신을 계속 가게 해 줄 유일한 것은 부단함이다. 진정한 부단함은 부단함 밖에 남아 있지 않을 때 나타난다.
런닝머신에서 30분을 뛰었다고 딴 사람처럼 보이지는 않는다. 그러나 한 번 뛸 때마다 당신은 달라지고 있는 것이다. 그렇게 운동을 이어가던 어느 날, 문득 거울을 보며 이런 생각을 하게 될 것이다.
'와!!'
불가능하다고 생각하지만 않는다면 우리는 더 많은 것을 이룰 수 있다.
- 빈스 룸바르디 -
7. "나는 아무것도 기대하지 않고 모든 것을 받아들여!"
가장 최근에 크게 화가 났었거나 실망, 좌절했을 경우를 떠올려보라.
마음 속 숨은 기대감과 현실 사이의 큰 격차를 느낄때 화가 더욱 나는 것이다. 기대를 잘라내라. 매사를 일어나는 그대로 받아들이는 것이 훨씬 더 효과적이다.
현재에 살아라. 힘없이 순순히 삶에 항복하라는 의미가 아니다. 오히려 그 무엇에도 그 누구에도 지배되지 않는 사람이 되라는 의미다. 아무것도 기대하지 않으면 현재를 살게 된다. 종종 우리는 내가 남들에게 대하는 방식과 똑같은 방식으로 남들도 나를 대해주길 원한다. 말 없이 무엇인가 기대했다가 그 일이 일어나지 않아 무시당했다고 생각하지 말고 기대를 놓아줘라. 원하는 게 있으면 기대하지 말고 부탁해라. 좋은 일, 친절한 일을 할때는 답례를 기대하지 말고 정말 원해서 해라. 사람들을 있는 그대로 사랑하고 사람들이 당신을 사랑하는 방식 그대로 사랑을 받아라. 결과에 연연해 하지 마라.
- 마무리하며 -
우리는 걱정거리와 불편한 사항만 해결되면 실행할거라고 마음 먹고 기다린다. 그러나 모든 게 완벽해지는 순간은 없다.
정말 간단한 진실이 있다.
내면 세계를 개선하고 싶다면 외부세계에서 뭔가 행동을 취해야 한다는 것이다.
생각 밖으로 나와라.
삶 속으로 뛰어 들어라.
사람은 언젠가 결국 죽는다. 임종의 순간을 상상해봐라.
"여러 권의 책을 읽었지만 실천한 적은 없었어. 원하는 목표를 달성하고 이루었을때를 상상하며 하려고 마음 먹었던 수 많은 계획들이 있었지만 못했지. 인생을 바꿀 많은 시도들이 시작한 이후 얼마안가 결국 시들시들해져졌어..."
후회가 몸과 마음을 훑고 지나갈 것이다.
미래의 당신은 인생에서 무언가를 이루지 못했거나 무언가를 갖지 못했다는 것을 후회하지 않을 것이다.
당신이 후회하게 될 유일한 일은 분명, '시도하지 않았다'는 사실이 될 것이다.
만약 그 임종의 순간에 당신이 이 책을 읽던 때로 되돌아갈 수 있다면, 만약 그럴 수 있다면 무엇을 하겠는가? 깨어나라!
성공한 사람들이 가진 것 중에서 당신이 갖지 못한 것은 없다.
단지 유일한 차이점이라면, '성공한 사람들은 때가 되기를 기다리지 않는다'는 것이다!
덧붙여,
지나간 과거가 발목을 잡는다고 생각하는 사람이 있다. 그 중엔 끔찍한 것도 있다. 그래서 뭐!!!!????? 왜 과거에 더 열정인가?
"진정으로 원한다면 당신이 앞으로 전진하는 것을 막을 것은 없다!"
자! 준비가 되었다면 다음 두 가지를 해야한다.
1. 지금 하고 있는 것을 그만둬라. 당신의 변화를 붙잡고 현재에 매어있게 만드는 습관들을 타파하라.
2. 앞으로 나아갈 수 있는 행동을 하라. 습관을 그만 둔 것만으로 인생이 바뀌지는 않는다. 도려낸 그 습관들 자리에 삶을 변화시킬 새로운 일을 집어 넣어라.
당신의 생각은 당신이 아니다, 당신의 행동이 당신이다.
당신이 앞으로 할 행동에 당신의 실제 인생이 걸려 있다!
ππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
함께 책 내용을 읽어 봤는데요, 어떠신가요?
제게는 와닿는 부분이 좀 있었습니다. 그래서 소개해 드리는 거고...~
유익한 내용이 되셨기를 바랍니다~~~
'사회 경제' 카테고리의 다른 글
2024년 윤석열 내란, 쿠데타! 12.3 계엄 (1) | 2024.12.10 |
---|---|
(2019년 알리익스프레스(Aliexpress) 블랙프라이데이) 프로모션 코드 Promotional Code는? (1) | 2019.11.25 |
(경제, 장사꾼) 미중 무역전쟁의 중심, 화웨이! 4차산업혁명, 그리고 중국의 '제조2025' (0) | 2019.02.03 |
(경제, 국가부도의 날) IMF 구제금융을 받았던 그 경제적 혼란기 (0) | 2019.01.27 |
(8.2 부동산대책과 9.13 부동산 대책) 강화된 1가구 1주택자, 다주택자 양도세 비과세 요건 (0) | 2019.01.12 |
[눈으로 보는 영화] 승리호(Space Sweepers) Review 줄거리, 2021 SF
경고) 완벽 풀스포 있음!!!
긴 글인데, 이 글 다 읽으면 영화 못 봄. 시간 없어서 영화 볼 수 없는 분들만 보세요~!!
한국 최초의 우주 SF영화 '승리호'가 지난 2월 5일,
넷플릭스를 통해 전세계에 공개 됐습니다.
원래 극장 개봉 예정이었으나 코로나 여파로
넷플릭스를 통한 온라인 공개로 방향을 전환한거죠.
출연한 배우들도 쟁쟁합니다.
장르 : SF액션 (스페이스 오페라)
감독 : 조성희
촬영기간 : 2019년 7월 3일 ~ 2019년 11월 2일
개봉일(공개일) : 2021년 2월 5일
제작비 : 240억
상영시간 : 136분
<<출연진>>
김태호 (송중기)
장현숙 (김태리) - '장 선장' 이라고 불립니다
제임스 설리반 (리처드 크리스핀 아미티지)
업동이 (유해진) - 군사용으로 설계된 로봇
박경수 (진선규) - '타이거 박' 이라고 불려요
강꽃님 (박예린) - '도로시' 라고도 불러요
이 영화 보기 전에, 일단 영화 속 세계관 부터~
<<세계관>>
배경은 황폐화된 서기 2092년의 지구와 그 주변행성!
- 새로운 양극화
지구는 극도로 오염되어 인간이 살기에 부적합한 행성이 되었습니다.
다행인지 불행인지, 기술을 축적한 우주개발기업, UTS가 위성 궤도에 인공 도시를
여러 개 건설해 사람들을 이주시키지만, 전 인류를 이주 시킬 수는 없습니다.
게다가 이윤을 추구하는 기업이다보니 UTS 기준에 맞는 선택된 5% 정도 사람들만 이주가 허용됩니다.
그 과정에 UTS의 권력은 막강해졌고 그들이 정하는 기준이 사실상의 법이나 다름없죠.
대부분의 인류는 혹독한 지구 환경에서 하층민으로 힘들게 살아갈 수 밖에 없어요.
- 미래 기술 : 자동 번역기와 마그네틱 신발
이 시대에는 언어 장벽이 없습니다. 보청기 처럼 귀에 꽂기만 하면 자동 번역되는 장치가 있기 때문이에요.
마그네틱 신발을 신고 있으면 적당한 세기의 자장이 형성되어 우주에서도 선체에 들러 붙어있을 수 있습니다.
우주 청소부들에게는 필수템입니다~ 다만 비싼게 흠이랄까...
- 우주 청소부
지구 주변에는 방대한 양의 우주 쓰레기들이 흩어져 있는데 아주 골치덩이 입니다.
크기가 작은 것들은 지구 대기권에 진입하면서 모두 불타 없어지지만, 커다란 쓰레기들은
지상에까지 떨어져 큰 위험을 초래하죠.
그래서 생겨난 게 우주 청소부들입니다. 환경미화원 같은 거죠.
이 영화에서 나오는 승리호는 우주 쓰레기를 청소하는 청소선인데, 돈 되는 쓰레기를 두고
청소부들끼리 치열한 확보 경쟁을 해야 하기 때문에 불법으로 많은 곳의 성능을 개조했습니다.
UTS 회장은 UTS 시민권을 부여할 수 있는 막강한 권한을 행세하며 신격화 됩니다.
그에게서 선택받지 못한 지구의 사람들은 타락했다며 인권도 완전 무시합니다. UTS 법을 무시한 사람들은
가차없이 죽임을 당해요. UTS 회장에겐 또 다른 야심이 있는데, 바로 화성 개발이죠. 위성 도시보다 안정적이고
막대한 개발 이익을 얻을 수 있으니까요. 그러나 화성 개발의 진행 속도는 지지 부진합니다. 화성 토양이 생명을
키우는데 적합하지 않아서 입니다. 그런데 최근 도로시의 출현으로 기존의 문제점들을 모두 극복하고
성공리에 화설 개발이 거의 완료되었습니다.
이 사람이 UTS의 회장, 제임스 설리반. 152세~
수명 연장 인체 시술로 인한 부작용인지 감정이 격해질 땐,
목소리가 괴물처럼 변하고 피부에 푸르게 혈관이 돋습는다.
점차 막강한 권력을 가지게 되자 거의 신처럼 군림하려 들죠.
능력이 월등했던 태호를 처음으로 알아보고 처음이자 마지막으로
UTS 치안군 기동대장으로 키운 사람이기도 합니다.
<< 줄거리 >>
자~ 이제 승리호 탑승.
영화가 시작되자 마자 돈 되는 우주 쓰레기를 차지하려는 청소선들의 쓰레기 쟁탈전이 정말 치열합니다.
결국 승리호 낚아챘네요! (극장에서 큰 화면으로 보면 정말 대박이었을듯)
수집한 우주 쓰레기는 '공장'이라고 불리는 쓰레기 하치위성으로 가져가 '카룸'이라는 사람에게 팔아요.
카룸과 태호는 오랜 거래로 좀 아는 사이입니다. 친하지는 않아 자세히는 모르고요.
네 명의 승리호 선원들을 소개해 드릴게요~ ==============
김태호~ (우주선 조정 실력이 뛰어 납니다)
태호는 UTS 불법 이민자들을 처리하는 UTS군의 기동대장 이었습니다.
작전을 수행하다보면 지구인 무차별 살상도 많이 하는데, 그 과정에 어떤 피살된 엄마가 품고 있던
아기를 데려다 키우게 됩니다. 천사같았대요~ 그리고 처음으로 느낀 자책감에 아빠가 되어 주고
이름도 순이~~라고 지어줍니다. 어느 순간부터는 완전 딸 바보가 됩니다. 그 후로는 살상도 못하게 되고요.
그런데 순이가 UTS 시민이 아니므로 UTS 거주지에서 키우는 건 명백한 불법이었고 그 사실이
설리반에게 발각되어 결국 UTS에서 퇴출되고 지구행 함선에 오릅니다. 하루 아침에 삶이 나락으로 떨어진
태호는 술에 쩔어 살다가 불행히도 갑작스런 우주 쓰레기 충돌 사고로 순이를 잃습니다.
순이를 찾아 헤매는 것이 인생이 된 태호에게는 20만의 수색비 마련이 정말 절실해 집니다.
그래서 돈에 집착하게 됐고 이 청소선에 합류한 거죠~ 그러나 각종 과태료와 세금, 대출금을 내고 나면
이상하게 항상 적자입니다 ㅜㅡ
이 아이가 순이인데, 우주 어딘가에 시체가 되어 떠돌고 있습니다.
태호가 차고 있는 하얀 색 팔찌는 순이 시체로 부터 오는 미약한 전파를 탐지하는 기기 입니다. 멀어져 신호가
약해질수록 깜빡임이 느려집니다. 약 3년 뒤면 감지할 수 있는 궤도에서 이탈합니다. 그 전에 돈을 마련해서
시체를 찾아야 하죠.
장현숙~ (장 선장 으로 불리죠)
장 선장은 과거 오염 지역 침투 전문, 뛰어난 두뇌 소유자로 탁월한 무기 개발 실력을 가지고 있습니다.
UTS에 대한 혐오감으로 UTS에서 오히려 탈출해 자유로운 삶을 사는 사람입니다. ---+
업동이~
원래 군사용 로봇이었는데, 장 선장이 재활용 센터에서 데려와 함선에 합류하게 됐죠.
뭔가 관심이 집중될 땐 눈에 노란 불이 켜지고, 화가나면 빨강 색,
아잉 부끄러울 때는 양 볼이 발그레 해지기도 합니다~~
돈 모아서 골격과 피부 이식 수술을 해서 사람 모양이 되는 게 꿈입니다~
허구한 날, 들여다 보는 게 아래와 같은 성형외과 광고죠~~
로봇 피부이식 전문 ... ㅋㅋ
업둥이 왈, '아, 씨. 더럽게 비싸네~'
타이거 박~
한 때 갱단 두목 이었습니다. 티타늄 도끼가 주 무기인데, 자기가 잡은 적은 그걸로 손모가지를 잘랐다고
업둥이에게 떠벌립니다. 그런데 정말이었나 봅니다. 업둥이는 항상 비꼬지만요.
우주 쓰레기 해체 작업중인 평온한 일상~
조그만 창고 쓰레기 더미에서 이상한 아이를 발견합니다. (이 아이가 바로 도로시~ 한국 이름으로는 '강 꽃님'이라네요)
TV 뉴스에 나왔는데, 이 도로시는 인간형 수소 폭탄이라서 굉장히 위험하답니다. 반드시 신고하랬어요~
그래서 타이거박이 일단 UTS 통합범죄신고센터에 신고를 넣어놨죠; 그러나 UTS 공무원들 반응이 느리네요.
수거해 가는 데 한참 걸릴 것 같아요. UTS 기동대에 신고를 하면 5분만에 오는데, 태호가 기동대 출신이라서 그런지
그냥 일반 신고 센터에 신고 했습니다.
그런데 수소 폭탄이라는 도로시는 이상한 능력을 가지고 있습니다. 거의 죽어 있던 식물을 성장시키네요~ 와!
이 능력만 있으면 화성에서도 식물을 기를 수 있고 인류가 거주할 수 있게 됩니다. 그래서 설리반이 도로시에
집착하는 거군요!! 오랫동안 지지부진하던 화성 개발이 갑자기 탄력을 받고 있었는데, 그 중심엔 도로시가
있었던 겁니다. 도로시의 몸에 주입된 나노 로봇과 나무에 퍼져 있는 나노봇들간에 통신이 이뤄지고 나무
조직을 재생, 합성하는 겁니다. 전 우주에 세균 처럼 퍼져 있는 나노봇들은 도로시와 통신할 수 있습니다!
굉장한데~!
도로시는 그림도 잘 그리네요. 수소폭탄치고 너무 여느 아이들과 닮아 있어요. 응가도 한대요.
엥? 응가도? 로봇이 응가를? 뭔가 이상한데요???
네, 사실 도로시는 폭탄이 아니었습니다~ 도로시를 추적하던 UTS가 폭탄이라며 신고를 하도록 유도하고 있는 거죠.
바가지 머리 모양을 한 도로시는 태호에게 딸, 순이를 잃기 직전의 모습을 떠올리게 합니다.
태호는 도로시를 볼 때, 종종 순이를 연상하곤 합니다. 그래서 사례금에 더욱 더 목이 메이는 거죠.
태호는 도로시가 가지고 있던 가방에서 오래 전에 지구인들이 사용하던 스마트폰을 발견합니다.
폰에는 최근까지 통화를 시도했던 흔적이 있네요. '강 현우', 도로시의 아빠 입니다. 게다가 UTS 시민권자네요.
여기서 태호의 눈이 반짝입니다. 아빠에게 애들 돌려보내면 큰 사례금을 받을 수도 있으니까요~
그런데, 문제가 하나 있습니다. 승리호의 모든 선원들은 UTS 비시민권자들 입니다. UTS 시민들이 신고를 한다면
사례를 받을 수 있지만, 비시민권자는 사례금를 받을 수 없습니다.
아 그리고 잠깐, 옆에 '한글 쓰기'라는 책이 보이시죠? 태호가 UTS 거주 지역에서 순이와 행복했던 시절에
순이 역시 이 '한글 쓰기'라는 책으로 한글을 배우고 있었습니다. 도로시도 한글 공부를 했나 봅니다.
폰을 든 손에 하얀 색 팔찌는 순이로 부터 오는 신호 감지기 입니다. 벌써 팔찌 받은지 3년이 거의 다 되어가니
태호가 안절부절 못하는 겁니다. 순이 시체가 곧 궤도에서 이탈해서 영영 만나지 못하게 되니까요.
우주선에 도로시를 홀로 내버려두고 네 선원들은 바에서 도로시 몸값을 추정하고 수익 배분을 논의합니다.
100만 (목숨걸고 쓰레기 팔아 얻는 수익이 5~6백이니까, 그에 비해 2천배임. 큰 돈이죠).
이들은 아직 도로시를 폭탄으로 알고 있습니다. 그래서 도로시가 감정 동요로 폭발하지 않도록
우주선 밖으로 피신해서 몸값 논의를 하고 있는 거죠 ㅎㅎ
도로시가 가지고 있는 스마트폰 무선 주파수로 강현우와의 통화에 성공하고 만날 장소까지 정합니다.
태호는 의심이 많았기에 강현우가 제의한 곳이 아닌, 유흥단지로 약속 장소를 정합니다.
접선 장소는 '32번 상업단지 고스트 2번 비상구 앞 14시'. 돈도 200만을 주겠답니다. 대박~~
당장 32번 상업 단지로 달립니다~~~
그런데 이 때 사용한 주파수가 통신을 장악하고 있는 UTS 치안망에 도청되고 맙니다. 설리반에게까지 보고되어
설리반이 기동대를 출동시키죠;;
그러면서 말합니다. "도로시를 찾아와! 인류의 운명이 달린 일이야!"
막상 거래 하려는데, 도로시가 사라졌습니다. 가방 안에 있으라고 신신당부를 했는데, 상업단지에서의
현란한 음악등에 이끌려 도로시가 가방을 열고 거리로 나간 겁니다. 도로시는 수배령이 내려져 있는 상태인데...
이미 잠복중이던 UTS 기동대가 거래 현장을 급습해 총격전이 시작됩니다.
그런데 이 때 도로시의 또 다른 능력이 나타납니다. 태호, 타이거 박, 도로시가 함께 피신할 때 기동대의 총탄이
날아오는데 도로시가 순간적으로 나노봇들을 동원해 방어막을 만들어 낸 거죠. 덕분에 무사히 현장에서 탈출합니다.
그리고 이 모습을 승리호에 남아있던 장 선장이 목격합니다. 도로시에 대한 가치를 직감한 장 선장은 도로시에 대한 정보를 수집하기 시작합니다. 그리고 도로시가 폭탄이 아니라는 것도 깨닫게 됩니다.
참고로 도로시를 추적하는 건 UTS 뿐만이 아닙니다. '검은 여우단'이라는 환경단체도 있습니다.
이들은 도로시를 확보해서 황폐화된 지구를 복원하려 하고 있죠. 검은 여우단과 강현우도 아는 사이 같고요.
탈출한 승리호는 쓰레기 하치 위성에 숨어 기체 정비를 합니다.
얼마 후, 다시 강현우와 통화가 된 태호 일행은 MR-13 27번 블록에서 만나기로 하고 출발할 준비를 하는데,
타이거 박이 태호에게 꽃님을 보내지 말자고 말합니다. 꽃님은 도로시의 한국 이름입니다. 강 꽃님.
지금까지 많은 일들을 겪으며 승무원들과 도로시가 많이 정들면서 도로시가 한국 이름을 가르쳐 준 거죠.
태호는 단호히 거절합니다. 빨리 수색비를 모아야 되거든요.
출발 전, 꽃님이 화장실에 가다가 수상한 사람에게 납치됩니다. 다행이 타이거 박이 발견하고 꽃님을 구하는데
그 수상한 사람은 다름아닌 카룸이었네요. 카룸이 누구냐고요? 위에서 잠깐 언급했었죠.
쓰레기 하치위성에서 쓰레기 사주던 나이지리아인입니다. 이 사람이 검은 여우단을 이끌고 있었군요.
그들은 도로시에 대해 뭔가 내막을 알고 있는 것 같습니다.
장 선장이 검은 여우단과 서로의 정보를 까놓자고 합니다. 이 시점에 태호와 타이거 박도 꽃님이 사람임을
알게 되고 깜짝 놀랍니다. 업동이는 슬쩍 자기는 이미 알고 있었다며 시인 하죠.
자기도 로봇이다보니 꽃님의 감정 표현을 보며 로봇이라기엔 어떤 이질감을 느꼈던 거죠~
장 선장이 꽃님에 대해 알아낸 정보를 다음과 같이 말합니다.
"강 꽃님, 화성 테라포밍 비밀 연구소 나노봇 과학자, 강 현우의 딸!"
승리호의 선원들이 놀랍니다.
도로시에 대한 정체를 장 선장이 알고 있음을 알게된 카룸은 체념하고 도로시에 대해 털어 놓습니다.
'_____________________________________________
도로시는 뇌세포가 파괴되는 원인 모를 병을 갖고 태어났다. 강 박사가 최후의 수단으로 정교하게
프로그래밍된 자신의 나노봇을 도로시에게 주입했는데, 기적이 일어났다. 나노봇들이 뇌신경을 메꾸고
도로시는 건강해 졌다. (이 말인즉, 도로시는 나노봇에 의해 생명이 유지되고 있다는 의미임)
더 신기한 일은, 나노봇들은 서로 신호를 주고 받도록 설계됐는데 도로시가 다른 나노봇들에게
메시지를 보내기 시작했다는 것이다. 그 원인을 강 박사도 밝혀내지는 못했지만 그 능력으로 도로시는
죽어 가는 나무도 꽃을 피웠다. 이를 '테라 포밍'이라고 한다. 도로시가 지구를 되살릴 유일한 희망이란 말이지.
그런데 설리반이 도로시를 가로채 화성 개발에 이용했고 화성에 숲이 형성된 것이다.
이렇듯 화성에도 숲이 생겼지만 정작 인류의 고향인 지구는 점점 더 황폐해져만 가고 있다.
사실 설리반의 목적을 위해서는 지구가 연구원들에 의해 재생되어선 안되고,
오히려 완전히 황폐화되어야 했다. 관련된 연구원들을 모두 실종됐고 기록도 삭제됐다.
더 이상 필요없어진 도로시도 함께 없앨 생각이었데, 검은 여우단이 피신시킨 것이다.
지구엔 희망이 없고 화성만이 희망이라는 설리반의 말은 거짓인 것이다.
한편 도로시는 나노봇들의 보호로 인해 죽지 않는다. 오로지 수소 폭탄으로 부터 방출되는
2억도의 크립톤 파동으로만 무력화 할 수 있다. 그 크립톤 파동의 범위는 반경 약 5000Km 이다.
공장(쓰레기 하치위성)에 있는 반중력 엔진에 수소 폭탄이 있고 설리반은 도로시를
그 폭탄에 묶어 폭탄을 폭발시키려 하고 있다. 엔진이 폭발한 공장은 그대로 지구로 낙하하여
지구 인류의 약 30억이 사라지게 된다.
_____________________________________________'
검은 여우단의 설명이 마무리 될 때쯤 밖에서 총성이 울립니다. 쓰레기 하치장에 기동대가 들이 닥친 거예요.
태호는 카룸에게 강현우와 만나기로 한 장소를 일러주며 거기서 만나자고 한 후, 각자 피신합니다.
급하게 탈출하면서 승리호는 우주 쓰레기 정체 구간(진짜 쓸모없는 쓰레기들만 버려진 곳), '라그랑주'로
빠져들어 사방에 널리 퍼져 있는 나노봇에 의해 우주선이 모두 침식되기 직전까지 가지만
꽃님이에 의해 다시 복원되어 안전하게 빠져 나옵니다. 힘이 많이 소진됐는지 꽃님은 그 자리에서
풀썩 쓰러집니다. 하지만 다행히 시간이 지난 후, 무사히 깨어나네요.
드디어 강현우와 꽃님이 만나게 됐습니다. 강현우와 검은 여우단이 함께 등장합니다.
(역시 강현우는 검은 여우단의 보호를 받고 있었군요. 아하 승리호가 쓰레기 하치장에 있을 때, 검은 여우단에게
꽃님이 정보를 준 것도 강현우였었나 봅니다. 그래서 검은 여우단이 꽃님이를 납치했던 거였군요.
앗, 그런데 강현우와 검은 여우단이 통화했다면, UTS가 그것을 감지하지 않았을까요?!)
기쁨도 잠시, 이들이 있는 구역 전체에 EMP 지뢰가 설치되어 있었네요. 매복입니다!! EMP 지뢰가 폭발하자
우주선의 전원이 모두 나가고 업동이도 맥 없이 쓰러집니다. 곧바로 UTS 기동대가 또 들이치고 이번엔 설리반이
기동대와 함께 왔습니다. 안타깝게도 검은 여우단원들 모두와 강현우까지 기동대에 의해 모두 사살됩니다.
단, 승리호 선원들만 죽이지 않습니다.
설리반이 선체 안으로 들어오자, 장 선장이 선원들에게 미안하다고 속삭입니다.
(장 선장의 앞 치아는 반경 500m를 모두 날릴 위력의 폭탄이었는데 그 뇌관을 제거한 거였어요)
그러나 장 선장에게 다가간 설리반이 장 선장의 폭탄 치아를 손으로 뽑아냅니다. 설리반은 장 선장에 대해
잘 알고 있었거든요. 고통스러워 하는 장 선장을 팽개치고 설리반이 태호 앞에 서서 400만의 돈을
쏟아 붓습니다. 꽃님의 몸값이라면서요. 순이의 시체를 찾아야 할 것 아니냐며 돈을 주으라고 합니다.
갈등하지만 결국 돈을 줍는 태호를 보며 설리반이 빈정댑니다. 태호 자신이 좋은 인간임을 자각하라고요.
자신을 거역한 태호에게 멸망할 지구를 보여주겠다며 꽃님을 데리고 공장으로 향합니다. 승리호와 선원들은
공장이 폭발하고 지구로 추락할 때 함께 처형하기로 합니다.
설리반이 가고 난 후, 태호는 순이의 시체를 찾기 위해 돈을 UTS 실종센터로 가지고 가고,
장 선장과 타이거 박은 꽃님을 구출하기 위해 승리호를 정비합니다. 물론 UTS군에 의해 포위된채
조준 사격 설정이 다 되어있는 상황이므로 어차피 탈출할 수도 없는데에도,
혹시모를 기회가 올 것이라 믿는 것 같습니다. 그래서 정비를 시작합니다.
실종센터에서 순이의 소지품을 찾은 태호가 순이의 한글 공부 책을 펼쳐봅니다.
순이가 비뚤 비뚤 쓴 글을 읽어 내려가다가, 태호는 순이를 잃으면서 함께 잃었던,
돈 보다 중요한 인간으로서의 인류애에 다시 눈을 뜹니다. 꽃님을 구해야 한다!
승리호로 다시 돌아온 태호는 함께 꽃님을 구하러 가겠다면서 승무원들의 투지를 북돋습니다.
그리고 남아 있던 돈을 전부 버리네요. UTS군에 의해 포위된 채로 있는 승리호가 탈출을 시도 합니다.
아슬아슬하게 탈출에 성공한 승리호가 전속력으로 공장에 도착해서 꽃님을 구합니다.
꽃님이가 태호를 보더니 손을 높이 들어 올립니다. 이번에는 태호가 하이파이브를 받아 줄려나요?
그리고 문제의 수소 폭탄과 맞딱뜨립니다. 장 선장이 재빨리 폭탄 설정 상황을 보더니
폭탄의 폭발을 막을 방법은 없다고 합니다. 무조건 터지는 거죠.
장 선장 : "폭탄이 터지면, 지구 복구고 뭐고 다 물 건너가는 거야"
크립톤 파동으로 부터 5132.464Km 밖으로 꽃님이가 벗어나야 희망이 있습니다. 그러지 못하면 꽃님의
몸 속에 있는 나노봇들이 무력화되어 버리고 꽃님도 죽죠. 지구를 복구할 희망도 사라지고요.
그 때 기동대장 카밀라가 이들이 있는 곳으로 진입합니다. 힘이 너무 월등해서 제압할 수가 없네요.
긴박한 이 때 타이거 박이 나섭니다.
"샌님들은 빠져, 너희들이 상대할 수 있는 놈이 아니야!"
타이거 박은 로봇을 격리 공간에 밀어넣고 문을 잠근 후 홀로 싸웁니다. 힘으로는 상대가 안되지만,
우주 배출구 문을 열어 로봇을 우주로 날려 버립니다. 이 때 자신을 잡았던 카밀라의 손모가지를
티타늄 도끼로 잘라 자신의 말을 얕보던 업동이에게 보여줍니다~ 업동이가 탄성을 지릅니다~
공장을 탈출해야 하지만, 이미 98기의 무인 공격기가 공장으로 접근하고 있습니다. 이 때 장 선장은 공장 근처에 있는
수 많은 청소선들에게 도움을 요청합니다. 공장이 지구로 떨어지면 지구에 있는 청소부들의 가족도 모두 죽는다면서요.
이제 우주 쓰레기를 치우는 청소선들과 무인 공격기들의 일대 격전이 펼쳐집니다.
그리고 이 때 설리반이 지구 멸망을 지시하는 내용의 녹음 파일도 비상주파수를 통해 UTS 주민과 지구상 주민들에게 방송됩니다. 지구에 있는 수 많은 사람들은 수소 폭탄이 터지고 공장이 낙하할 것이라는 소리에 아연실색합니다.
다른 청소선들의 교전 덕에 승리호는 교전 지역을 탈출해 먼 우주로 빠져나갑니다.
이를 파악한 설리반이 직접 공격기를 몰고 승리호를 쫒아와 들러붙습니다.
크립톤 파동 영역을 벗어나려는 태호와 그 반경을 벗어나지 못하게 하려는 설리반 간에
치열한 접전이 이어집니다.
시간이 지나 이윽고 위험 반경에서 거의 벗어나자, 다급해진 설리반이 외칩니다.
설리반 : "(도로시는) 도대체 어디있는 거야?!!"
그러면서 승리호의 화물칸을 부숩니다. (그 때문에 실려있던 수소 폭탄이 설리반 쪽으로 튕겨져 나옵니다)
.
ㅎㅎ 설리반의 외침에, 장 전장이 응답하죠~ "여기 없어, 이 등신아!!"
어라?! 이게 무슨 말이죠? 그럼 대체 꽃님은 어디로 간거죠?
아하~, 아까 공장에서 승리호가 실은 건 수소 폭탄이었던 겁니다. 꽃님이는 다른 청소선에 태워 보냈고요.
이제보니 승리호 선원들은 모두 꽃님과 최대한 멀리 수소 폭탄을 옮기기 위해 필사적이었던 겁니다.
이들은 그렇게 인류를 구하고 죽으려던 거예요...
아까 승리호가 공장에서 빠져나올 때의 대화 장면이 이제야 회상하듯 나오네요~
업둥이 : "미친겨?, 꽃님이는 숨겨 놓고 폭탄은 우리가 들고 가자고?!"
그. 때. !!
폭탄의 카운트 다운이 완료되고 추락하게 될 공장에 온 인류의 시선이 집중되어 있던 그 때,
수소 폭탄이 굉장한 빛을 내며 폭발합니다. 순간 고요~
(설리반은 죽기 전, 순간적으로 왜 자기 눈 앞에 있는 승리호에서 수소 폭탄이 폭발하는 건지 이해를 못합니다.
원래 공장에 있어야 하는데...)
(UTS의 시민들과 지구의 주민들도 놀랍니다. 공장에서 폭발이 있을 거라고 생각했는데 공장은 평소와 다름 없으니까요...)
잠시 후, 전혀 다른 곳에서 폭발이 있었고 공장이 안전하다는 소식이 전해지자 사람들은 환호합니다.
폭발 반원 밖, 공장 주변에 몰려 있던 우주 청소선들은 일제히 승리호를 향해 침묵하고 애도를 표합니다.
한 청소선에 타고 있던 꽃님의 눈이 보입니다. 파랗게 빛나고 있습니다~
그 때 수소 폭탄이 폭발한 장소에서 붉은 덩어리 같은 뭔가가 고속으로 청소선 주변으로 이동해 옵니다.
그 붉은 색을 띠는 표면은 폭발로 부터 승리호를 보호한 나노봇들이었습니다~
비록 기체 손상은 심했지만, 승리호는 무사합니다~
UTS는 전 인류에게 그 간의 잘못에 대해 사과 성명을 발표합니다.
그리고 향후 지구 복구에 전념하겠다는 뜻도 밝힙니다.
며칠 후, 완벽히 수리되어 깔끔한 승리호의 모습이 보이네요.
그리고 오늘 꽃님이가 우주에 퍼져 있는 나노봇과 교신하며 태호와 순이를 연결해 줍답니다.
잠시 후, 마치 매트릭스의 한 장면처럼 온 사방이 하얀 빛에 둘러싸인 가상의 방에서 만납니다.
그리고 태호는 술에 쩔어 무시했던 순이의 글도 읽어보고 마지막으로 작별 인사도 합니다.
이제야 태호의 마음이 조금 가벼워진듯 하군요.
다시 밝고 평온한 음악이 흐릅니다.
어, 그런데? 못 보던 여자가 하나 있네요. 희한하게 모습은 여자인데 목소리는 남자,...
아하! 업동이 목소리 입니다. 승리호 선원들은 많은 보상을 받았고 업동이는 몸 전체에 최고급
피부 이식을 했습니다. 진짜 완벽한 사람처럼 보이네요. 근데 업동이 목소리가 영 안어울립니다.
업동이는 꽃님에게 듣기 좋은 목소리를 골라달라네요~
그런데 어쩌죠? 꽃님이는 지금의 업동이 목소리가 좋다네요~ ^^
꽃님이에 의해 이제 지구 복구도 상당한 진전이 있습니다.
위성 궤도에 있는 UTS 거주자들도 이젠 지구에 와서 사는 게 더 낫겠네요~
어쨌든 우주 쓰레기 청소는 계속해야죠.
승리호도 우주 쓰레기 청소를 다시 시작했습니다~~ 승리호 화이팅~~
----------------------------- 끝 ----------------------------------------------
우리 나라 최초의 스페이스 오페라 영화입니다.
처음 제작되는 장르라서 기대반 우려반 했던 영화였죠. 군데 군데 이제껏 헐리우드 영화에서 봐왔던
장면들을 많이 차용했지만, 꽤 좋은 퀄리티로 만들어졌다고 생각됩니다. 아주 만족이에요.
SF류를 좋아하는 관객의 눈높이를 제대로 높여놨네요.
제작비가 240억이라는데, 관계자 분들의 의견으로는 그 정도의 제작비로 이런 품질의 SF 영화를 만들수는
없다네요. 봉사 활동하는 것도 아니고... 제작에 참여한 분들이 얼마나 애착을 가지고 작업을 하셨는지 짐작됩니다.
아쉽게 코로나 사태로 온라인 공개를 했는데, 만약 극장에서 개봉했으면 얼마나 좋았을까요.
아마도 추억하기 위해 극장앞에서 사진이라도 찍지 않았을까요?
분명 천만 관객도 단숨에 넘어섰을 겁니다.
아무튼 한국 영화, 정말 화이팅입니다~!!!
이상, 긴 글 읽어주셔서 감사합니다~
'(눈으로 읽는) 영화 드라마' 카테고리의 다른 글
[응8] 응답하라1988 줄거리 다시보기 (Reply 1988) - 1화 (손에 손 잡고) (1) | 2024.06.23 |
---|---|
써로게이트 (Surrogates, 2009) - 98%가 아바타인 세상 (0) | 2024.06.19 |
[눈으로 보는 영화] MINARI, 미나리 Review 줄거리, 2020 (0) | 2021.02.01 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (15~16/16화) (1) | 2019.08.29 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (13~14/16화) (0) | 2019.08.26 |
[눈으로 보는 영화] MINARI, 미나리 Review 줄거리, 2020
스포 있습니다~
영화 '미나리'는,
한 한국 가족의 미국 시골 마을 정착기를 그린 2시간 남짓의 작은 독립 영화(?) 입니다.
한국계 영화 감독인 정이삭 감독이 만든 이 영화가 미국에서 인기를 얻고 있습니다.
한국어를 사용하고 OST는 있는 듯 없는 듯하며 한국적인 정서를 담백하게 표현하고 있는
이런 류의 영화가 외국인들에게 먹힐까 싶긴한데, 미국인들의 초기 정착 생활에 대한
향수를 불러 일으키는 걸까요? 다행히도 좋은 평가를 받고 있네요.
- 2020년 미국 선댄스 영화제 극영화 경쟁부문 심사위원 대상/관객상을 수상 -
아무데서나 잘 자라는 미나리... 는,
어쩌면 고단한 이주민의 끈질긴 개척정신을 잘 함축하고 있는 소재 같습니다.
순자라는 할머니 역을 맡은 윤여정님의 모습이 기억에 계속 남네요.
무조건적인 자식 사랑과 희생, 그렇게 보듬어지는 따뜻한 가족애가 있습니다.
<대략적인 줄거리>
(임의로 세 개의 막으로 영화를 나누어 쓰겠습니다~)
- 시작 -
영화는 캘리포니아에서 살던 한 한국인 가족이 아칸사스의 깡촌으로
이사를 오는 장면에서 시작합니다. 창 밖엔 온통 나무와 풀, 비포장 도로 뿐.
그야말로 건물 하나 없는 그런 깡촌입니다.
=========================== 1막. 이주 =============================
- 가족 소개 -
할머니 순자(윤여정) - 모니카의 어머니이기도 하죠.
아빠 야곱(스티븐 연)
엄마 모니카(한예리)
아들 데이빗(엘런 S, 김)
딸 앤 (노엘, 조)
처음 이 시골 마을로 이사를 온 가족은 할머니 외에 네 식구 입니다.
데이빗은 뛰면 안됩니다. 심장병이 있거든요.
어쩌면 오래 살지 못할 수도 있다는 선고를 받은 상태입니다.
그래서 어린 데이빗이 뛸 때마다 아빠 엄마가 외칩니다. '데이빗 뛰지마!'
(데이빗이 자꾸자꾸 깜박 하네요. 데이빗의 소원은 남들처럼 뛰어 다니는 거!!)
아래 컨테이너는 오늘부터 이 가족이 살 집입니다.
모니카는 이 컨테이너를 보자마자 불만 폭발이네요~
(모니카는 독실한 크리스찬입니다. 아빠는 무교. 자신을 믿습니다~)
이 집, 폭풍이 올 때면 정전도 되고, 피난도 해야되고...
수돗물이 없으므로 수시로 돈을 내고 물통을 채워야 하기 때문에
설겆이, 세수 등 작은 세숫대야에 물을 받아서 아껴 써야 합니다.
쓰레기는 창고 옆 드럼통에서 모두 태워서 없애야 하고요.
(낭만이 차고 넘치죠?~ 모니카가 폭발할만 합니다)
아무것도 없는 벌판~
(이웃 주민 얘기로는, 이곳에 먼저 정착했던 사람은 망해서 총으로 자살했다네요;;)
야곱은 아내에게 여기에 넓은 정원이 있다며 데려왔나 봅니다. 크큭~
모니카: '정원은 작은 거야. 이건 농장이잖아!'
야곱: '정원이나 농장이나 똑같지~'
모니카는 농장에서의 삶에 대해 막연히 불안감이 느껴지나 봅니다.
아이들은 물 만난 물고기들 처럼 신났네요.
야곱: '데이빗, 뛰지 말라니까!'
야곱의 본업은 병아리 감별사, 모니카도 함께 감별 일을 반 년째 하고 있어요.
맞벌이죠. 때문에 애들 돌봐줄 사람이 필요한데, 넉넉치 못한 살림살이라서
보모를 고용할 수도 없고 이 깡촌에 어린이 돌봄교실 같은 건 더더욱 없습니다.
그래서 모니카는 한국에 계신 어머니를 이곳으로 오시게 합니다.
아래는 야곱과 모니카가 이곳 깡촌에 있는 양계장에 출근하는 첫 날 입니다.
생활비가 빠듯한데, 일자리를 구할 수 있어서 다행입니다.
병아리들이 참 귀엽네요. 병아리 감별...
숫놈들은 맛도 없고 알도 못 낳으니까 감별 후 곧바로 폐기 처분된답니다.
양계장 바로 옆, 검은 연기를 내뿜는 커다란 굴뚝 아래에서 화장되는 거죠;;
부부는 생활고 때문에 자주 큰 소리를 내며 싸웁니다. 정착민들에게 흔한 풍경이지요.
아이들 정서 상 좋지 않은데다 애들을 많이 위축시킵니다.
야곱: '당신 미쳤어!!'
모니카: '누가 누구더라 미쳤대?!!'
농장에는 제일 첫 번째, 물이 필요합니다.
물길을 찾아 펌프를 설치하는 것도 돈 들어가는 일 입니다.
나름의 과학적인 추리로 물을 찾아낸 아빠가 아주 신났습니다.
아들 데이빗에게 큰 소리로 말하네요.
야곱: '데이빗!, 한국인은 머리를 써야 돼!!
머리를 써서 물을 찾으면 공짜로 물을 얻을 수 있어~!
큰 소리로 외쳐봐, 와우~! 와우~!!'
(참고로 아빠 야곱은 아들 데이빗이 강해지길 바랍니다.
반면 엄마 모니카는 데이빗이 조심조심 오래 살아주길 원합니다)
이 사람은 앞으로 야곱의 농장 일을 도와 줄 일꾼, '폴'입니다.
심하게 하느님을 믿습니다. 모니카 처럼요.
오늘 트랙터를 배달해 줬죠.
이렇게 넓은 농장에 펌프와 트랙터는 필수품입니다.
농장일이 힘들지만, 나름 시골 사는 맛도 있습니다.
그네를 만들고 좋아하는 모니카와 아이들.
야곱이 즐겁게 트랙터를 몹니다.
새로 시작한 농장일에 들떠, 밭을 갈면서 희망찬 미래도 그려봅니다.
야곱은 미국으로 이주해오는 수 많은 한국인들이 그 수요가 될 것이라며
한국 채소들을 재배합니다. 계획대로만 된다면 크게 성공할 수 있을 거예요.
(그런데 세상일이 어디 뜻대로 되나요...)
========================= 2막. 어머니(할머니) =========================
모니카의 어머님이 드디어 오셨네요.
한국의 어머니들이 모두 그렇듯, 자식과 손주들에게 줄 무언가를 잔뜩 가져오셨습니다.
모니카는 어머니가 가져온 가방을 풀어보다가 눈시울이 붉어 집니다.
가방 속에는 어머니의 딸 걱정이 한 가득이었습니다...
물론 손자에게 줄 한약도 한 첩 지어 오셨고요.
(어머니의 유일한 낙인 듯 보이는 화투도 한 벌 있네요)
어머니: '이거는 너 한테 주는 거야!'
어머니가 품에서 꼬깃꼬깃한 돈 봉투를 슬며시 꺼내 놓습니다.
(얼마나 아끼면서 한 푼 두 푼 모으셨을까요...)
두 아이 데이빗과 앤은 한국어와 영어를 모두 구사합니다.
데이빗은 할머니에게서 한국 냄새가 난다며 싫어 합니다.
할머니가 가져온 한약을 먹기 싫다며 몰래 세면대에 버리고 그 사발에
몰래 오줌을 넣어 할머니에게 마시라고까지 할 정도로 할머니를 싫어합니다.
어떤때에는 영어를 알아듣지 못하시는 할머니 뒤에서 영어로 험담을 합니다.
외국에서 태어난 손주와 할머니의 관계가 이렇죠, 뭐...
맞벌이 부부가 일하러 가면, 할머니는 하루 종일 아이들을 돌봅니다.
할머니 자신도 낯선 땅, 딸네 집에 와서 마음이 편치 않을텐데,
아직 할머니를 거부하는 손주들에게 다가서려고 다방면으로 애쓰시네요...
(하지만 쉽지 않아 보여요. 할머니가 다가서면 아이들은 한 발짝 피하는 식이죠)
그러던 어느 날, ~
한국인 하면 화투인가요? ^^
할머니가 아이들에게 화투를 가르치시네요~ ;;
화투장 하나를 힘껏 내리치며 할머니가 외칩니다, "비켜라, 이놈아!"
나중에 데이빗은 교회에서 만난 친구, 존 에게 이 화투를 전파합니다~ ^^
데이빗이 존을 가지고 놀죠.
데이빗이 화투장을 힘껏 내리치며 할머니가 그랬던 것 처럼 외칩니다.
"비켜라~, 이놈아~!"
그렇게 한가롭게 일상이 흘러가던 어느 날,
할머니는 아이들을 데리고 농장에서 좀 멀리 떨어진,
미나리를 심기 좋은 장소를 찾아 미나리를 심었습니다.
"여기가 좋겠다~"
주일 -----
넓고 황량하기만 한 시골마을이었기에, 다른 사람들과의 교류를 원했던
모니카는 어머니를 포함해 가족 모두와 함께 동네 교회를 방문합니다.
예배 시간~
모니카는 헌금 바구니에 지폐를 한 장 넣습니다.
그런데,... 딸 모니카가 넣은 지폐를 헌금 바구니에서 슬며시 다시 꺼내시는 어머니...
딸네 살림을 걱정을 할 때면, 비난 받을지도 모르는 그 어떤 일에도 꿋꿋(?) 하십니다^^;
할머니는 요리에 쓰일 미나리를 수시로 오셔서 따 가시는데,
데이빗을 함께 데려오곤 하셨죠.
얼마전 할머니께서 심으신 미나리가 소박하지만 풍성하게 잘 자랐네요.
할머니: '미나리는 아무데서나 막 자라니까 부자든 가난한 사람이든
누구든지 다 뽑아 먹고 건강해질 수 있어. 김치에도 찌개에도 넣어 먹고
국에도 넣어 먹고 아플 땐 약도 되고. 미나리는 원더풀~'
( 데이빗이 할머니의 미나리 얘기를 듣고 '미~나리는~ 원더풀~'하며서
노래하듯 혼자 흥얼 거리니까 할머니도 함께 노래로 흥얼거리십니다~)
============================== 3막. '가족' =============================
평온하던 어느 날, 갑작스런 불행이 찾아옵니다.
뇌졸증으로 할머니의 팔 다리가 마비되신 거예요.
그 이후로는 네 발 지팡이가 없으면 걷기도 힘겨워 하시고
식사 때 데이빗에게 물을 따라 주려다가 물을 엎지르기도 하시고
침대에 누워 잘 움직이지도 못 하십니다.
중풍이란 무서운 병이고 가족들에게도 힘든 병이죠...
언제부터였는지 데이빗과 앤은 할머니 걱정에 마음이 불안합니다.
교회 아이들이 몸이 불편한 다른 노인을 보며 비웃을 땐 마음이 편치 않습니다.
안타까운 마음에 데이빗은 퇴원해서 잠든 할머니 옆에서 이렇게 소근댑니다.
데이빗: '이건 모두가 할머니의 잘못이에요. 할머니가 미국에 왔기 때문이라고요...'
데이빗 심장병의 경과를 보기 위해 야곱 가족이 병원을 찾아왔습니다.
그런데 가족의 생계를 책임지고 있는 야곱은 데이빗에 대한 걱정보다
읍내에 나온 김에, 실어 온 농작물 샘플의 신선도를 잘 유지해서
구매처를 찾을 생각밖에 없습니다. 그래서 진료 상담 때에도
밖의 더운 날씨를 피하려고 농작물 상자를 들고 진료실 안으로 들어옵니다.
(실은 며칠 전 야곱의 농작물을 사겠다던 상인이 계약을 일방적으로
파기했기 때문에 아주 예민해져 있는 상태였죠)
가족을 2순위로 보는 듯한 이런 야곱의 행동에 모니카는 슬슬 짜증이 납니다.
다행인 것은 상담 결과, 데이빗의 병이 눈에 띄게 호전됐답니다.
아마도 공기 좋은 시골 생활과 할머니가 키운 미나리를 먹은 효과 아닐까요?
기쁜 소식이 또 하나 있습니다.
야곱이 가져온 농작물 샘플을 보고 야곱의 농작물을 사주겠다는 상인을
드디어 만났습니다. 집 창고에 쌓여있는 농작물들을 다음 주부터 납품하기로 했습니다~~~
그렇게 일들이 좀 풀리나 싶었는데, 야곱 부부는 또 싸웁니다...
야곱과 달리 모니카는 늘 농장을 벗어나야 한다고 생각하는 것 같습니다.
그도 그럴 것이 어머니는 중풍이고 남편은 농장일로 하루 종일 눈코 뜰 새 없고
맞벌이에 아이들은 돌볼 수도 없으니까요.
도시로 가자는 모니카의 제안에 농사일을 포기할 수 없었던 야곱은,
(세상에...) 서로 떨어져 살자고 합니다... 그 말에 모니카가 폭발해 버린 거예요.
때문에 집으로 돌아오는 길이 좀 많이 늦어 졌습니다.
그렇게 부부가 아이들을 데리고 집으로 돌아올 즈음엔, 날이 이미 저물었어요.
그 때 집에 혼자 계셨던 할머니는 집안일이라도 좀 거들 생각으로
그 불편한 몸을 이끌고 청소며 쓰레기들을 처리하고 계셨는데,
갑작스런 강한 바람에 불씨가 창고 쪽으로 번지고 맙니다. 아, 이런...
네 발 지팡이로 불을 끄려고 안깐힘을 쓰시는 모습이 애처롭기까지 합니다.
팔 다리가 마음처럼 움직여 주질 않습니다.
때늦게 도착한 야곱 부부는 창고에 몇 몇 농작물을 밖으로 옮기려 하지만,
강한 열기와 연기 때문에 쉽지 않습니다.
애써키운 농작물과 조금씩 마련 해놨던 농기구들이 전부 불길에 휩싸였네요.
차 안의 아이들은 놀라서 엄마, 아빠를 계속 외치고,... 그냥 아수라장입니다.
그 모습을 무력하게 보고 있을 수 밖에 없는 모니카의 어머니는
안타까움과 자책감에 한 없이 가슴이 그야말로 미어집니다... ㅠㅜ
결국 창고는 전소됐고 그렇게 모두가 정신이 없던 사이,
모니카의 어머니는 네 발 지팡이도 없이 홀연히 절뚝절뚝 어디론가 떠나십니다.
수중에 아무 가진 것도 없고, 이 낯선 타지에 대체 어디 갈 곳도 없는데,
무작정 어둠 속으로 걸어 가십니다. 중풍으로 이젠 몸 조차 제대로 가누질 못해
자식과 손주들에게 짐만 된다는 생각에 무턱대고 자식들에게서 사라져야 한다고 생각하시는 거예요.
일생을 자식 뒷바라지에 헌신하고 더 이상 도움이 안될 것 같으니 사라지는...
멍한 표정에 촛점을 잃은 눈으로, 어둠 속으로 들어가시는 모습이 너무 안타깝네요.
그 때 차안에서 무서워하고 있던 데이빗이 할머니를 찾아 주변을 두리번 거립니다.
그런데 할머니가 보이질 않습니다. 데이빗이 급하게 사방을 돌아보며 찾습니다.
어마! 다행히 저 멀리 걸어가고 계시는 할머니가 희미하게 보이네요.
아무리 '할머니!, 할머니!' 하고 외쳐보지만, 할머니는 정신적 충격에
아무 것도 듣지 못하시는 듯 계속 어둠 속을 걸어 멀어져 갑니다.
... 그런데 그 때... 다급해진 데이빗이 할머니를 향해 뛰기 시작합니다!!
( 가슴에 통증이 느껴지는지 이를 꽉 깨물고 뜁니다!
머릿 속에서 아빠 엄마가 '뛰면 안돼, 데이빗!'이라고 소리치는 것
같았지만, 데이빗은 멈출 수가 없습니다.
왠지 할머니를 잃을 지도 모른다는 불안감 때문이죠 )
( 오래 전부터 할머니께서는 데이빗의 용기를 북돋아 주기위해 종종 이렇게 말씀하셨어요.
'데이빗 너는 강해, 내가 본 사람들 중에 우리 데이빗이 제~일 강해!!')
이윽고 할머니 앞에 다다른 데이빗과 앤...,
... (가쁜 숨을 몰아쉬는 데이빗이) ...
...(숨을 삼키며 할머니에게 말합니다)...
'할머니, 그 쪽 아니에요, ... 우리 집은 저 뒷쪽이에요.
할머니, 가지 마세요!, 저희랑 함께 집에 가요!'
그렇게 힘든 어둠이 가고 새로운 아침이 밝아옵니다.
싱그러운 아침,
야곱은 데이빗의 안내를 받아 미나리를 따러 미나리 밭에 왔습니다.
밝고 따스한 햇볕이 평화롭게 내리쬐는 이 곳,
이름 없는 이 개울가에 미나리가 정말 무성하게 자랐습니다.
미나리를 보며 야곱이 데이빗에게 기분좋게 말합니다.
야곱: '데이빗!, 할머니가 좋은 자리를 찾으셨구나!', '맛있겠다^^!!'
============================== 끝 =================================
할머니가 미나리 밭에서 했던 아래 멘트가 기억에 남네요.
'미나리는 아무데서나 막 자라니까 부자든 가난한 사람이든 누구든지 다 뽑아 먹고
건강해질 수 있어. 김치에도 찌개에도 넣어 먹고 국에도 넣어 먹고 아플 땐 약도 되고.
미나리는 원더풀~'
데이빗은 실제로 할머니의 미나리가 들어간 음식과 건강한 시골 생활로
심장병이 많이 호전된 것으로 보입니다~ 반면 그 미나리를 심으셨던 할머니는
점점 노화로 쇠약해져 가고, 그렇게 시간은 기억 저편으로 흘러갑니다.
어머니이면서 할머니의 모습인 윤여정님이 여운으로 많이 남습니다...
윤여정님이 딱 '어머니' 상인 것처럼 배역도 잘 맞는 것 같습니다.
아마 그런 그 '어머니' 이미지로 영화계의 주목을 받는 거 아닐까 생각돼요.
정착 생활에 지친 자식에게 꼬깃꼬깃 모아 온 돈 봉투를 건네는 엄마.
(이 돈은 심장병을 앓는 손자가 병원에 갈 때 진료비로서 정말 피같이 사용되죠.
딸이 넣은 교회 헌금을 도로 슬쩍할 정도로 수중에 남은 게 하나도 없고
자식을 위해서라면 비난 받을 일에도 꿋꿋한 모습...ㅠ)
겉으로는 드러내지 않지만 낯선 미국땅에서 많은 이질감을 느꼈을텐데,
정서도 언어도 멀기만한 손자와의 트러블들을 모두 보듬어 주는 할머니의 모습.
뇌졸증으로 불편해진 몸을 지팡이에 의지하면서도 집안의 궂은 일을 마다않는,
또 불이 번져 점차 자식들에게 부담이 되어가는 자신을 깨닫고
갈 때도 없는 낯선 타향의 어둠 속을, 정처없이 가족 몰래 홀로 집을 떠나는 어머니.
... ... ...
그런 어머니가 계셨기에,
그 먼 타국 땅의 낯선 이름 모를 개울가에서 미나리는 탐스럽게 자라 났습니다~
낯선 땅에 정착하려 애쓰는 가족의 모습과 미나리의 억센 생명력이 서로 닮아 보입니다.
이 영화는 재밌다기 보다는 그냥 여운이 잔잔하게 남는 한 편의 풍경화같은,
가족의 의미를 한 번 되새겨보게 해주는 영화 같습니다.
... ... ...
긴 글 읽어주셔서 감사합니다~
'(눈으로 읽는) 영화 드라마' 카테고리의 다른 글
써로게이트 (Surrogates, 2009) - 98%가 아바타인 세상 (0) | 2024.06.19 |
---|---|
[눈으로 보는 영화] 승리호(Space Sweepers) Review 줄거리, 2021 SF (0) | 2021.02.07 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (15~16/16화) (1) | 2019.08.29 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (13~14/16화) (0) | 2019.08.26 |
(MBC 드라마 W (더블유)) 다시보기, 줄거리 (12/16화) (0) | 2019.08.25 |
직렬화(Serialization), 마샬링(Marshalling)의 의미?
직렬화 란? 그리고 마샬링 이란?
1. 직렬화(Serialization)
객체 데이터를 일련의 byte stream으로 변환하는 작업을 직렬화라고 합니다.
반대로 일련의 byte stream을 본래의 객체 모양으로 복원하는 작업은 Deserialization 이라고 합니다.
2. 마샬링(Marshalling)
메모리 상에 형상화된 객체 데이터를 적당한 다른 데이터 형태로 변환하는 과정을 말합니다.
컴퓨터간 데이터 전달 또는 프로그램 간 데이터 전달을 할 때 사용되죠.
전송된 데이터를 다시 원래의 객체 모양으로 복원하는 작업은 언마샬링(Unmarshalling)이라고 합니다.
3. 직렬화와 마샬링의 차이
직렬화와 마샬링은 사실 상 거의 같은 개념입니다.
직렬화는 오래 전부터 데이터를 주고 받는 모든 전자 기기에 폭 넓게 사용해오던 개념입니다.
예를 들면, 위 그림 처럼 레고 블럭으로 만든 빌딩을 먼 거리로 전송한다고 상상을 해 보죠.
전송 채널은 일반적으로 단일 채널 즉, 1차원적인 일렬로 보낼 수 밖에 없으므로 빌딩 블럭을
순서대로 해체해서 블럭 하나 하나를 채널을 통해 발송하고, 수신하는 측에서는 이렇게 받은
하나 하나의 블럭을 약속된 방법에 따라 다시 빌딩의 모습으로 복원하는 거죠.
이런 직렬화 작업을 컴퓨터 프로그래밍 데이터 처리에 적용한 개념이 마샬링입니다.
음... 이름만 특이할 뿐이지 똑같은 개념입니다~.
단지 이런 차이점이 있습니다. 컴퓨터의 데이터 처리에는 여러 가지 매개 값들이 오고 갑니다.
때문에 직렬화된 데이터에 여러 가지 매개 값들도 추가하고 그 리턴 값들도 한꺼번에 집어넣게 되었는데,
이렇게 좀 더 세분화된 정보 처리기에서의 직렬화 작업을 약간 더 특별하게 마샬링이라고 부르고 있습니다.
직렬화 작업이 프로그래밍적으로 조금 더 전문화 된 것이지요.
(그래서 프로그래밍에서 함수나 클래스를 직렬화 할 때 함수 마샬링, 클래스 마샬링 이라고 부릅니다)
즉, 직렬화라는 작업은 마샬링이라는 것이 포함된 폭 넓은 개념입니다. (직렬화 >> 마샬링)
직렬화 작업들 중에 프로그래밍 작업에서 좀 더 전문화된 세부 개념이 마샬링인 것이지요.
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (16/16) - 앱 만들기 9 (TodoList)
=== TodoList (일정표) ===
- 기능 소개
할 일 목록을 표시
할 일을 데이터베이스에 추가, 수정, 삭제
- 주된 도구
ListView (목록을 표현하는 리스트 형태의 뷰)
Realm (모바일용 데이터베이스)
1. 프로젝트 생성
두 개의 화면을 사용할 것임 (할 일 목록 표시 화면 + 편집 화면)
데이터 베이스로는 모바일에서 SQLite를 대체할 정도로 인기가 높은 Realm을 사용할 것임
(Realm은 SQL 문법을 몰라도 사용 가능함)
프로젝트명 : TodoList
minSdkVersion : 19
기본 액티비티 : Basic Activity
Anko 라이브러리 설정 할 것.
VertorDrawable 하위 호환성 설정 할 것.
* 참고)
Basic Activity는 지금까지의 액티비티와 조금 다른 면이 있습니다.
플로팅 액션 버튼(FAB)과 메뉴가 미리 작성되어 있고 activity_main.xml과 content_main.xml 두 개의 레이아웃 파일이 생성됩니다.
activity_main.xml의 컴포넌트 트리 창을 보면, 'include...'를 통해 content_main.xml 파일이 포함되어 있음을 알 수 있습니다.
(include 속성을 이용해 여러 개의 화면을 구성할 수 있음)
2. 할 일 목록을 표시하는 화면의 레이아웃 추가
content_main.xml의 기본 텍스트 뷰 삭제
Autoconnect 모드 on
팔레트 창에서 Legacy 카테고리의 ListView를 끌어다 화면 정중앙에 배치
(id: listView, layout_width: match_constraint, layout_height: match_constraint, 상하좌우 여백: 모두 0)
3. 편집 화면의 레이아웃 추가
File > New > Activity > 'Empty Activity'
(이름: EditActivity)
activity_edit.xml에 팔레트 창 Widgets 카테고리에서 CalendarView를 끌어다 화면 상단에 배치
id: calendarView
layout_width, layout_height: wrap_content
위, 좌우 여백: 0
팔레트 창 Text 카테고리에서 Plain Text를 끌어다 달력 아래에 적당히 배치 (할 일 편집용으로 쓸 것임)
Autoconnect 모드는 부모 레이아웃과의 제약을 자동으로 생성하지만, 자식 뷰들간의 제약은 생성되지 않습니다.
아마도 좌우 여백 제약은 추가되어 있을 것입니다. EditText와 CalendarView와의 제약을 추가합시다.
id: todoEditText
layout_width: match_constraint
layout_height: wrap_content
위, 좌우 여백: 8
inputType: text
hint: 할 일
text: (공백)
완료버튼용, 삭제버튼용 벡터이미지를 추가
팔레트 창 Buttons 카테고리에서 FloatingActionButton을 끌어다 우측 하단에 배치 (편집 완료 버튼으로 사용할 것임)
이미지는 프로젝트에 이미 추가한 완료버튼 이미지를 지정해서 사용하고 다음 속성을 지정.
id: doneFab
하단, 우측 여백: 16
backgroundTint: @android:color/holo_orange_light
tint: @android:color/white
팔레트 창 Buttons 카테고리에서 FloatingActionButton을 끌어다 좌측 하단에 배치 (삭제 버튼으로 사용할 것임)
이미지는 프로젝트에 이미 추가한 삭제버튼 이미지를 지정해서 사용하고 다음 속성을 지정.
id: deleteFab
하단, 좌측 여백: 16
tint: @android:color/white
Activity_main.xml에서 일정 추가버튼의 벡터 이미지를 아래 이미지로 변경
srcCompat: @drawable/ic_add_black_24dp
tint: @android:color/white
4. Realm 데이터 베이스 사용 준비
비만도 계산기를 구현할 때에는 데이터 베이스를 사용하지 않고 SharedPreferences를 이용해 데이터를 간단히 저장했었습니다.
그러나 데이터가 많고 복잡하다면 데이터 베이스를 활용하는 것이 효율적입니다.
안드로이드는 SQLite를 지원하지만 다루기가 어렵고 코드량도 많아 Realm 데이터 베이스를 활용해 보기로 합시다.
안드로이드에서는 앱 별로 격리된 데이터 베이스를 가질 수 있습니다.
앱 간에 자신의 데이터 베이스를 공개하려면 프로바이더를 이용하면 됩니다.
참고) 데이터 베이스에서의 테이블은 엑셀의 시트와 같습니다.
1) 사용 준비
Build.gradle(project)에 플러그인 정보 설정
dependencies {
...
classpath "io.realm:realm-gradle-plugin:6.0.0"
}
Build.gradle(module)에 플러그인 추가
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
2) 할 일 정보 데이터 베이스의 모양
데이터 베이스 명: todolist
테이블 명 : todo
id (Long) |
title (String) |
date (Long) |
1 |
청소 |
2020. 05. 25 |
2 |
빨래 |
2020. 05. 25 |
3 |
공부 |
2020. 05. 27 |
3) 모델 클래스 작성
① File > New > Kotlin > 'File/Class' (클래스명: Todo)
② 위 데이터 베이스를 클래스화 한 모양 --> "모델 클래스" 라고 부름
class Todo(
@PrimaryKey var id: Long = 0, // PrimaryKey: 중복되지 않는 유일키
var title: String = "",
var date: Long = 0
) {
}
이 모델 클래스를 Realm에서 테이블로 사용하려면,
클래스 명 앞에 open 키워드를 사용하고
RealmObject 클래스를 상속받으면 됩니다.
-- Todo.kt 코딩
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class Todo(
@PrimaryKey var id: Long = 0,
var title: String = "",
var date: Long = 0
) : RealmObject() {
}
4) Realm 초기화
앱 실행시 가장 먼저 Realm이 초기화 되도록해야 액티비티들이 공동으로 데이터 베이스를 사용할 수 있습니다.
이 역할을 하는 클래스를 하나 새로 만듭시다.
File > New > 'Kotlin File/Class' (클래스명: MyApplication, 종류: Class)
-- MyApplication.kt
import android.app.Application
import io.realm.Realm
class MyApplication : Application() { // Application 클래스를 상속
override fun onCreate() { // 이 메서드는 액티비티가 생성되기 전에 호출됨
super.onCreate()
Realm.init(this) // Realm 초기화
}
}
-- AndroidManifest.xml (application 엘리먼트에 name 속성을 추가 --> 앱에서 사용하는 전체 액티비티에
공동으로 사용하는 객체를 초기화할 때 이런 방법을 사용합니다)
<application
android:name = ".MyApplication" // <-- 이 행을 추가
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".EditActivity"></activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
5. EditActivity.kt 코딩
-- EditActivity.kt
package com.tistory.todolist
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.activity_edit.*
import org.jetbrains.anko.alert
import org.jetbrains.anko.yesButton
import java.util.*
class EditActivity : AppCompatActivity() {
val realm = Realm.getDefaultInstance() // Realm 인스턴스 얻기
val calendar: Calendar = Calendar.getInstance() // 캘린더 객체 생성(오늘 날짜로 초기화됨)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit)
// 인텐트로 id를 전달해서 데이터 베이스의 삽입/변경/삭제를 분기
// id=-1 (추가모드)
val id = intent.getLongExtra("id", -1L)
if (id == -1L) {
insertMode() // 추가 모드
} else {
updateMode(id) // 삭제 모드
}
// 캘린더 뷰의 날짜를 선택했을 때 캘린더 객체에 설정
calendarView.setOnDateChangeListener { view, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
}
}
// 추가 모드
private fun insertMode() {
deleteFab.visibility = View.GONE // 삭제 버튼 숨기기
// visiblity 프로퍼티: setVisibility()
// VISIBLE : 보임, INVISIBLE : 영역은 차지하지만 보이지는 않음, GONE : 숨김
doneFab.setOnClickListener { insertTodo() } // 완료 버튼 클릭시 insertTodo() 호출
}
// 삭제 모드
private fun updateMode(id: Long) {
// id에 해당하는 객체를 화면에 표시
val todo = realm.where<Todo>().equalTo("id", id).findFirst()!!
todoEditText.setText(todo.title)
calendarView.date = todo.date
// 완료 버튼 클릭시 updateTodo() 호출
doneFab.setOnClickListener { updateTodo(id) }
// 삭제 버튼 클릭시 deleteTodo() 호출
deleteFab.setOnClickListener { deleteTodo(id) }
}
override fun onDestroy() {
super.onDestroy()
realm.close() // Realm 인스턴스 해제
}
// 데이터 베이스 할 일 삽입
private fun insertTodo() {
realm.beginTransaction() // *** 트랜잭션 시작 ***
// 트랜잭션 시작 (트랜젝션: 데이터베이스의 작업단위)
// beginTransaction ~ commitTransaction 사이의 코드들은
// 전체가 하나의 작업(트랜잭션)이며 도중에 에러가 나면 일괄 취소됨
// 데이터베이스의 추가/삭제/업데이트는 항상 이 사이에 작성해야 함
val newItem = realm.createObject<Todo>(nextId())
newItem.title = todoEditText.text.toString()
newItem.date = calendar.timeInMillis
realm.commitTransaction() // *** 트랜잭션 종료 ***
alert("일정이 추가 되었습니다") {
yesButton { finish() }
}.show()
}
// 데이터 베이스 할 일 변경
private fun updateTodo(id: Long) {
realm.beginTransaction()
val updateItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
// where<Todo>() : 테이블의 모든 값을 얻어옴
// .equalTo(필드명, Long) : 해당 '필드명'의 Long형 id값의 데이터를 가져옴
// findFirst() : 첫 번째 데이터
updateItem.title = todoEditText.text.toString()
updateItem.date = calendar.timeInMillis
// timeInMillis 프로퍼티 : 날짜를 가져오는 getTimeInMilles()
realm.commitTransaction()
alert("일정이 변경 되었습니다") {
yesButton { finish() }
}.show()
}
// 데이터 베이스 할 일 삭제
private fun deleteTodo(id: Long) {
realm.beginTransaction()
val deleteItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
deleteItem.deleteFromRealm()
realm.commitTransaction()
alert("일정이 삭제 되었습니다") {
yesButton { finish() }
}.show()
}
// Realm은 자동 키 증가를 지원하지 않으므로 아래 메서드를 만들었음
private fun nextId() : Int {
val maxId = realm.where<Todo>().max("id")
// where<Todo>() : 테이블의 모든 값을 얻어옴
// .max(필드명) : 현재 '필드명'중 가장 큰 값을 얻음 (Number형)
if (maxId != null) {
return maxId.toInt() + 1
}
return 0
}
}
6. MainActivity.kt 코딩
메인 액티비티에 할 일 추가 버튼 클릭 시, EditActivity를 시작하도록 리스너 등록
-- MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
fab.setOnClickListener { // 할 일 추가 버튼 클릭 리스너
startActivity<EditActivity>()
}
}
7. 메인 액티비티에 할 일 목록 표시
타이머 예제의 랩 기록과 같이 적은 목록을 표시할 때는 스크롤 뷰로 충분했지만,
표시해야 할 목록의 양이 많아질 경우엔 리스트 뷰를 사용합니다.
스크롤 뷰 |
모든 아이템을 메모리에 로드하므로 데이터가 많아지면 많은 메모리를 차지 |
리스트 뷰 |
뷰를 재사용하고 화면에 보이는 것만 동적으로 로딩 하므로 적은 메모리를 사용 |
리스트 뷰에 데이터를 출력하려면 읽어올 데이터 소스와 리스트 뷰의 입력부간에 데이터 형식을 맞춰주기 위한
어댑터가 필요하다. 이 어댑팅 작업은 출력 속도에 큰 영향을 주므로 아주 중요합니다.
리스트 뷰에 목록을 출력할 때에는 '뷰홀더' 방식을 사용합니다.
뷰홀더 방식이란 딱 한 번 생성한 레이아웃에 내용만 수정해서 재사용하는 방식을 말합니다.
사용하는 메모리나 성능 면에서 효율적이죠.
1) 어댑터 사용 준비
-- build.gradle (Module)
dependencies {
implementation 'io.realm:android-adapters:2.1.1'
...
}
2) 목록을 표시할 레이아웃 리소스 파일 작성
res/layout 폴더에서 마우스 컨텍스트 메뉴 > File > New > Layout resource file
(파일명: item_todo , 레이아웃: ConstraintLayout)
(item_todo.xml 파일 작업)
날짜 표시용 TextView를 좌측 상단에 배치
id: text1
layout_width: match_constraint
위 여백:0, 좌우 여백: 8
text: (공백)
(붓)text: 2020/05/28 (디자인 시 보일 텍스트 아무거나)
textAppearance: AppCompat.Body1
할 일 표시용 TextView를 좌측 상단에 배치
id: text2
layout_width: match_constraint
위, 좌우 여백: 8
text: (공백)
(붓)text: 청소하기 (디자인 시 보일 텍스트 아무거나)
textAppearance: AppCompat.Body2
전체 레이아웃의 layout_height 속성: wrap_content
3) 어댑터 클래스 작성
File > New > 'Kotlin File/Class' (이름: TodoListAdapter, 종류: Class)
-- TodoListAdapter.kt
package com.tistory.todolist
import android.text.format.DateFormat // 자동으로 임포트가 잘 안되면 직접 코딩할 것
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.realm.OrderedRealmCollection
import io.realm.RealmBaseAdapter
class TodoListAdapter (realmResult: OrderedRealmCollection<Todo>)
: RealmBaseAdapter<Todo>(realmResult){ // RealmBaseAdapter 를 상속
// RealmBaseAdapter의 추상 메서드 구현
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
// 이 메서드는 매 아이템이 화면에 보일 때마다 호출됨
// 이 메서드에 리스트 뷰의 각 아이템에 표시할 뷰를 구성하면 됨
// position (출력할 리스트 뷰 내의 아이템 위치)
// convertView (재활용되는 아이템의 뷰. 처음 아이템이 작성되기 전엔 null, 이후에는 그 전에 작성했던 뷰를 전달)
// parent (부모 뷰. 여기서는 리스트 뷰의 참조를 가리킴)
val vh: ViewHolder
val view: View
if (convertView == null) { // null이면 레이아웃을 작성
view = LayoutInflater.from(parent?.context).inflate(R.layout.item_todo, parent, false)
// LayoutInflater : XML 레이아웃 파일을 불러옴
// inflate() : XML 레이아웃 파일을 뷰로 전환
// false : XML 파일을 불러왔을 경우
vh = ViewHolder(view)
view.tag = vh // tag 프로퍼티에는 모든 데이터형의 객체를 저장할 수 있음
} else { // null이 아니면 이전에 작성된 convertView를 재사용
view = convertView
vh = view.tag as ViewHolder // view.tag는 Any형이므로 ViewHolder 타입으로 형변환
}
// adapterData : RealmBaseAdapter가 제공하는 프로퍼티로서 이를 통해 데이터에 접근할 수 있음
if (adapterData != null) { // 데이터가 있으면.
val item = adapterData!![position]
vh.textTextView.text = item.title
vh.dateTextView.text = DateFormat.format("yyyy/MM/dd", item.date)
}
return view
}
override fun getItemId(position: Int): Long {
// 리스트뷰 클릭 이벤트 처리시 인자로 position, id등이 넘어옴. 이 때의 id 값을 결정
if (adapterData!= null) {
return adapterData!![position].id // adapterView가 Realm 데이터를 가지고 있으므로
// 해당 위치의 id를 반환해줘야 함
}
return super.getItemId(position)
}
}
class ViewHolder(view: View) {
val dateTextView: TextView = view.findViewById(R.id.text1)
val textTextView: TextView = view.findViewById(R.id.text2)
}
4) 할 일 목록 표시
-- MainActivity.kt
package com.tistory.todolist
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.listView
import org.jetbrains.anko.startActivity
class MainActivity : AppCompatActivity() {
val realm = Realm.getDefaultInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
fab.setOnClickListener { // 할 일 추가 버튼 클릭 리스너
startActivity<EditActivity>()
}
val realmResult = realm.where<Todo>().findAll().sort("date", Sort.DESCENDING)
// 할 일 목록을 날짜순으로 모두 가져옴
val adapter = TodoListAdapter(realmResult) // 할 일 목록이 담긴 어댑터 생성
listView().adapter = adapter // 어댑터 지정 (이 때 목록이 출력됨)
realmResult.addChangeListener { _ -> adapter.notifyDataSetChanged() } // 데이터가 변경될 경우 어댑터에 적용됨
// notifyDataSetChanged() : 데이터 변경을 통지하여 목록을 다시 출력함
listView().setOnItemClickListener { parent, view, position, id -> // 리스트 뷰 아이템 클릭시 처리
startActivity<EditActivity>("id" to id) // 기존 id 존재 여부에 따라 새 할 일 추가 또는 수정
}
}
override fun onDestroy() {
super.onDestroy()
realm.close()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}
실행 결과)
여기까지 9개의 코틀린 예제를 통해서 안드로이드 주요 기능들을 골고루 훑어봤습니다.
안드로이드 기기가 발전되어 오면서 추가된 여러 센서 기능들과 기기 자체 성능이 다양한 제조업체를 통해 이뤄지다 보니 마치 웹 브라우저에 플러그인들이 더덕더덕 붙어 있는 듯한 느낌입니다.
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (15/16) - 앱 만들기 8 (실로폰)
== 실로폰 ===
- 기능 소개
음판을 누르면 소리가 재생
- 주된 도구
SoundPool (음원을 관리하고 재생하는 클래스. 안드로이드 5.0 이전과 이후 버전의 동작이 다르므로
모든 기기에서 잘 동작하도록 버전 분기를 적용할 것임)
1. 프로젝트 생성
프로젝트명 : Xylophone
miniSdkVersion : 19
기본 액티비티 : Empty
레이아웃 에디터에서 미리보기 모드를 가로모드 작업 환경으로 설정합시다.
(실로폰이 가로 모양이니까요)
2. 텍스트 뷰로 음판 만들기
음판을 적당하게 배치한 후 한 번에 제약을 추가해 봅시다. (Autoconnect 모드 off)
디폴트 텍스트 뷰 삭제 > 팔레트 창 Common > TextView를 끌어다 건반 모양으로 배치
id : do1
layout_width : 50 dp
layout_height : match_constraint
위, 아래 여백 : 16 (좌, 우 여백은 모든 건반 배치후 한 번에 설정할 것임)
text : 도
textAppearance : AppCompat.Large
textColor : @android.color/white
background : holo_red_dark
gravity : center에 체크 (컨텐츠를 가운데 배치하는 역할)
위와 같은 형태로 7개의 테스트 뷰를 추가로 배치.
id |
위, 아래 여백 |
text |
background |
re |
24 |
레 |
@android:color/holo_orange_dark |
mi |
32 |
미 |
@android:color/holo_orange_light |
fa |
40 |
파 |
@android:color/holo_green_light |
sol |
48 |
솔 |
@android:color/holo_blue_light |
la |
56 |
라 |
@android:color/holo_blue_dark |
si |
64 |
시 |
@android:color/holo_purple |
do2 |
72 |
도 |
@android:color/holo_red_dark |
모든 건반 배치가 끝나면 컨트롤 키를 누른 상태에서 모든 건반을 클릭하여 전체 건반 선택 >
마우스 우측 버튼 > 컨텍스트 메뉴에서 Chains > Create Horizontal Chain (뷰들이 체인으로 연결됨)
체인으로 연결된 뷰 중 아무 뷰나 선택 후 컨텍스트 메뉴 > Cycle chain mode 클릭
(클릭할 때마다 세 가지 모드가 전환됨)
(아래와 같이 건판이 균일한 간격이 되는 모드를 선택)
이렇게 텍스트 뷰들로 실로폰 음판을 만들었습니다.
3. 재생할 소리 리소스 준비
wav, mp3 와 같은 사운드 파일은 .raw 리소스 디렉터리를 만들어 그 안에 저장해 놓고 사용합니다.
1) .res 디렉토리 생성
프로젝트 창의 .res 디렉터리에서 마우스 우측 버튼 클릭 > 컨텍스트 메뉴 중 'New > Android Resource Directory'
Resource type : raw
2) http://bit.ly/2K9dQjo 에서 실로폰 음계의 wav 파일 다운로드 > .res에 저장
※ 안드로이드 기기에서 소리 재생
방법1) MediaPlayer 클래스 (음악 및 비디오 파일 재생. 한 번만 재생하고 플레이어를 끝낼 때 유용)
사용 예)
val mediaPlayer = MediaPlayer.create(this, R.raw.do1)
button.setOnClickListener{ mediaPlayer.start() }
...
mediaPlayer.release() // 사용이 끝난 후 해제해 줘야 함
방법2) SoundPool 클래스 (실로폰과 같이 연속으로 소리를 때마다 계속 내야 할 때 유용)
사용 예)
val soundPool = SoundPool.Builder().build()
val soundId = soundPool.load(this, R.raw.do1, 1) // load(컨텍스트, 소리파일, 우선순위)
button.setOnClickListener { soundPool.play(soundId, 1.0f, 1.0f, 0, 0, 1.0f) }
// play(음원id), 왼쪽볼륨 0.0~1.0, 오른쪽 볼륨, 우선순위(0은 최하순위),
// 반복여부(0:반복안함, -1:반복), 재생속도(배속)
4. SoundPool 초기화 버전 분기
-- MainActivity
class MainActivity : AppCompatActivity() {
...
private val soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SoundPool.Builder().setMaxStreams(8).build() // 한꺼번에 재생하는 음원 개수 (8개 동시 재생) *1
} else {
SoundPool(8, AudioManager.STREAM_MUSIC, 0) // 최대 재생 스트림 개수, 음원 종류, 음질 (default: 0)
}
}
*1 에서 오류가 표시될 때 Alt+Enter를 눌러 표시되는 제안 중 Sorround with... 를 선택해 주세요.
(버전에 따른 if 분기를 하는 코드가 생성됨)
5. 건반에 동적으로 클릭 이벤트 정의
-- MainActivity (최종 소스 코드)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sounds.forEach { tune(it)} // sounds 리스트를 각각 tune()에 전달
}
private val soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SoundPool.Builder().setMaxStreams(8).build() // 한꺼번에 재생하는 음원 개수 (8개 동시 재생) *1
} else {
SoundPool(8, AudioManager.STREAM_MUSIC, 0) // 최대 재생 스트림 개수, 음원 종류, 음질 (default: 0)
}
private val sounds = listOf( // 리스트 객체
Pair(R.id.do1, R.raw.do1),
Pair(R.id.re, R.raw.re),
Pair(R.id.mi, R.raw.mi),
Pair(R.id.fa, R.raw.fa),
Pair(R.id.sol, R.raw.sol),
Pair(R.id.la, R.raw.la),
Pair(R.id.si, R.raw.si),
Pair(R.id.do2, R.raw.do2)
)
private fun tune(pitch: Pair<Int, Int>) {
val soundId = soundPool.load(this, pitch.second, 1) // 음원 id 얻기
findViewById<TextView>(pitch.first).setOnClickListener { // 텍스트 뷰의 id에 해당하는 뷰 얻기
soundPool.play(soundId, 1.0f, 1.0f, 0, 0, 1.0f)
}
}
override fun onDestroy() {
super.onDestroy()
soundPool.release()
}
}
}
실행한 후 각 건반을 터치 해 봅시다.
(건반을 누르면 저장된 음원을 재생하며 소리가 납니다)
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (14/16) - 앱 만들기 7 (손전등)
=== 손전등 ===
- 기능 소개
앱에서 플래시를 켜고 끌 수 있음
위젯을 제공함으로써 앱을 실행하지 않고도 플래시를 켜고 끌 수 있음
- 주된 도구
Camera Manager (플래시 동작 제어)
Service (보이는 화면 없이 백그라운드에서 실행되는 컴포넌트)
App Widget (런처에 배치하여 앱의 기능을 빠르게 사용)
1. 프로젝트 생성
프로젝트 명 : Flashlight
minSdkVersion : 23 (Android 6.0 Marshmallow) - 6.0 이상에서 지원되는 방법이 제일 쉽기 때문임
(5.0 미만 버전들에서는 공식적으로 기능이 제공되지 않으며 제조사별 방법도 다르고 매우 복잡함)
기본 액티비티 : Empty Activity
Anko 라이브러리 설정
2. 손전등 기능 구현
1) 별도의 클래스 파일로 Torch 클래스 작성
File > New > 'Kotlin File/Class' (파일명: Torch, 유형: class)
-- Torch.kt
package com.tistory.flashlight
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
class Torch(context: Context) { // CameraManager 객체를 얻어야 하므로 Context를 생성자로 받았음
private var cameraId: String? = null
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE)
as CameraManager // getSystemService()의 리턴값이 object형이므로 as로 형변환했음
init { // 클래스가 초기화 될 때 실행됨
cameraId = getCameraId()
}
fun flashOn() {
if (cameraId != null) cameraManager.setTorchMode(cameraId!!, true)
}
fun flashOff() {
if (cameraId != null) cameraManager.setTorchMode(cameraId!!, false)
}
private fun getCameraId():String? { // 카메라 ID는 각각의 내장 카메라에 부여된 고유의 ID이다
// 카메라가 없다면 null을 반환해야 하므로 리턴형을 String?로 지정
val cameraIds = cameraManager.cameraIdList // 기기가 가진 모든 카메라 목록
for (id in cameraIds) {
val info = cameraManager.getCameraCharacteristics(id)
val flashAvailable = info.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) // 플래시 가능 여부
val lensFacing = info.get(CameraCharacteristics.LENS_FACING) // 카메라 랜즈의 방향
if (flashAvailable != null && flashAvailable && (lensFacing != null) && (lensFacing > 0)
&& lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
// 플래시가 가능하고 카메라 방향이 뒷방향
return id
}
}
return null
}
}
3. 액티비티에 스위치 제작
액티비티 정중앙에 switch 를 배치 (id: flashSwitch, text: '플래시 On/Off')
※ Switch는 두 가지 상태 값을 가지는 버튼 객체임
-- MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val torch = Torch(this)
flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
torch.flashOn()
} else {
torch.flashOff()
}
}
}
}
일단 동작을 한 번 확인해 봅시다!
(가운데 버튼을 켜면 플래시가 켜질 거예요)
4. 손전등 제어에 '서비스'를 활용
액티비티에서 했던 손전등 제어를 이제 서비스에서 구현해 봅시다. ('위젯'을 사용)
(서비스는 이미 전술했듯 4대 컴포넌트의 하나이며 백그라운드에서 동작)
액티비티는 제어 기능은 그대로 살리되, 직접 제어 대신 서비스를 호출만 하도록 바꿔보죠.
(즉 액티비티는 서비스 호출함으로써, 앱 위젯에서는 직접 서비스를 제어함으로써 Torch클래스를 동작시킴)
※ 안드로이드의 서비스
서비스 역시 액티비티처럼 생명주기용 콜백메서드들을 가짐
onStartCommand() : 일반적으로 실행할 작업을 여기에 작성함
onDestroy() : 서비스가 중지될 때 호출됨
stopSelf() 서비스 내부에서 서비스 중지
stopService() 서비스 외부에서 서비스 중지
위젯 / 서비스 사용
1) 서비스 생성
File > New > Service > Service (클래스명 : TorchService)
2) TorchService 클래스 코딩
class TorchService : Service() { // 자동으로 Service 클래스를 상속받는 군!
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
// 본 TorchService 클래스에서 Torch 클래스를 사용할 것임
// Torch 클래스의 인스턴스를 얻는 방법으로 onCreate() 와 by lazy 중 하나를 이용할 수 있음
// onCreate() 콜백서비스를 이용하면 코드가 길어지므로 by lazy를 사용했음
// by lazy는 torch 객체를 처음 사용할 때 아래 코드가 초기화됨
private val torch: Torch by lazy {
Torch(this)
}
// 외부에서 startService()로 본 TorchService 서비스를 호출하면
// onStartCommand()가 호출됨
// 보통 인텐트에 action 값을 저장하여 호출함
// (참고: 서비스는 메모리 부족 등의 이유로 강제 종료될 수 있음)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
// 앱에서 실행할 경우
"on" -> {
torch.flashOn()
}
"off" -> {
torch.flashOff()
}
}
return super.onStartCommand(intent, flags, startId) // *1
}
}
/* *1
onStartCommand()의 반환값
이 반환값들에 따라 시스템이 강제 종료된 후, 다시 서비스 복원을 어떻게 할지를 결정함
START_STICKY (null 인텐트로 재시작. 무기한 실행 대기하는 미디어 플레이어에 적합)
START_NOT_STICKY (재시작 안함)
START_REDELIVER_INTENT (마지막 인텐트로 재시작. 능동적으로 수행 중인 파일 다운로드 서비스등에 적합)
*/
3) MainActivity.kt에서 torch 객체에 직접 접근해 스위치를 켜고 끄게 되어 있는 코드를
인텐트에 "on" 또는 "off" 액션을 보내 TorchService으로 서비스를 시작하도록 수정.
flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
// torch.flashOn()
startService(intentFor<TorchService>().setAction("on")) // *1 Anko 코드
} else {
// torch.flashOff()
startService(intentFor<TorchService>().setAction("off"))
}
}
/* *1
Anko를 사용하지 않는다면,
val intent = Intent(this, TorchService::class.jave)
intent.action = "on"
startService(intent)
*/
5. 앱 위젯 작성
웹 위젯이란 런처에 배치하여 빠르게 앱 기능을 사용할 수 있게 해 주는 컴포넌트입니다.
간단히 '위젯' 이라고도 부릅니다.
1) 위젯 추가
File > New > Widget > App Widget (클래스명 : TorchAppWidget)
※ 위젯 작성 마법사 화면의 옵션 항목들
Placement 위젯 배치 위치
Home-screen only (홈 화면에만 배치)
Home-screen and Keyguard (홈 화면과 잠금 화면에 배치)
Keyguard only (API 17+) (잠금 화면에만 배치)
Resizable (API 12+) 위젯 크기 변경
Horizontally and vertically (가로 세로 크기 변경 가능)
Only Horizontally
Only vertically
Not resizable
Minimum Width (cells) 가로 크기를 1~4 중 선택
Minimum Height (cells) 세로 크기를 1~4 중 선택
Configuration Screen 위젯의 환경설정 액티비티를 생성
Source Language 자바와 코틀린 중 선택
2) 생성된 위젯용 파일들
TorchAppWidget.kt 위젯 동작 작성용 파일
TorchAppWidget.xlm 위젯의 레이아웃 정의 파일
dimens.xml과 dimens.xml (v14) 위젯의 여백 값이 API 14부터 바뀌었음. v14 버전 이하와 이상 분기 파일
Torch_app_widget_info.xml 각종 설정용 파일
3) 위젯 레이아웃 수정 - layout/torch_app_widget.xml
텍스트 뷰 속성 > text 속성 수정 버튼 클릭 > App_name 선택 > "Edit Translations"
appwidget_text 문자열 값에 "손전등"
위젯 레이아웃 전체에 클릭 이벤트를 연결해야 하므로 전체 레이아웃 id를 정합시다.
트리 창에서 RelativeLayout을 선택 > id 속성 : appwidget_layout
4) TorchAppWidget.kt 코딩
class TorchAppWidget : AppWidgetProvider() {
// AppWidgetProvider라는 일종의 브로드캐스트 리시버 클래스를 상속
override fun onUpdate( // 위젯이 업데이트 돼야 할 때 호출됨됨
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) { // 위젯이 여러 개 라면 모든 위젯을 업데이트
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) { // 위젯이 처음 생성될 때 호출됨
}
override fun onDisabled(context: Context) { // 위젯이 여러개일 때 마지막 위젯이 제거될 때 호출됨
}
companion object {
internal fun updateAppWidget( // 위젯을 업데이트 할 때 호출됨
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val widgetText = context.getString(R.string.appwidget_text)
// 위젯은 액티비티에서 레이아웃을 다루는 것과 다름
// 위젯에 배치하는 뷰는 따로 있고 RemoteViews 객체로 가져옴
val views = RemoteViews(context.packageName, R.layout.torch_app_widget) // 위젯 전체 레이아웃 정보
// 텍스트 값을 변경
views.setTextViewText(R.id.appwidget_text, widgetText)
// 위젯 클릭 시 처리할 작업
val intent = Intent(context, TorchService::class.java)
val pendingIntent = PendingIntent.getService(context, 0, intent, 0)
// *1 (각 0 : 사용하지 않을 때 0 값을 전달)
// 클릭 이벤트 연력 (위젯 클릭시 위에서 정의한 인텐트 실행)
views.setOnClickPendingIntent(R.id.appwidget_layout, pendingIntent) // *1
// 레이아웃 수정이 완료되면 appWidgetManager을 사용해서 위젯을 업데이트 함
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
/* *1
PendingIntent 객체는 실행할 인텐트 정보를 가지고 있다가 실행함
어떤 인텐트를 실행힐 지에 따라서 다음의 적당한 메서드를 사용해야 함
PendingIntent.getActivity() 액티비티 실행
PendingIntent.getService() 서비스 실행
PendingIntent.getBroadcast() 브로드캐스트 실행
*/
5) TorchService.kt 수정
TorchService는 인텐트에 on/off 액션을 지정해서 켜거나 껐으나 위젯은 어떤 경우가
on인지 off인지 알 수 없으므로 액션을 지정할 수 없습니다. 때문에 액션이 지정되지 않아도
플래시가 동작하도록 TorchService.kt를 수정해야 합니다.
class TorchService : Service() {
...
private var isRunning = false
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
// 앱에서 실행할 경우
"on" -> {
torch.flashOn()
isRunning = true
}
"off" -> {
torch.flashOff()
isRunning = false
}
// 서비스에서 실행할 경우 (이때는 액션 값이 설정되지 않음)
else -> {
isRunning = !isRunning
if (isRunning) {
torch.flashOn()
} else {
torch.flashOff()
}
}
}
...
}
}
앱을 실행하면 위젯 코드가 반영됩니다.
앱을 실행했다가 바로 종료합시다.
휴대폰의 위젯 모음에 들어가보면 제작한 위젯이 추가되어 있을 것이다. 홈 화면에 끌어다 놓고 위젯으로
손전등을 동작 시켜 봅시다.
※ 참고) 위젯 모양 바꾸기
xml/torch_app_widget_info.xml 파일에 위젯 모양에 대한 정보가 있습니다.
drawable/example_appwidget_preview.png 아이콘이 지정되어 있음을 확인할 수 있네요.
※ 참고) 앱 위젯에 배치하는 뷰
앱 위젯에 배치하는 뷰는 정해져 있습니다.
1) 레이아웃으로 가능
FrameLayout
LinearLayout
RelativeLayout
GridLayout
2) 레이아웃에 배치 가능
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (13/16) - 앱 만들기 6 (지도와 GPS)
=== 지도와 GPS ===
- 기능 소개
구글 지도에 현재 위치를 표시하고 이동중인 자취를 실선으로 표시.
- 주로 사용하는 도구
Google Maps Activity (지도를 표시하는 기본 템플릿)
FusedLocationProviderClient (현재 위치 정보 클래스)
play-services-maps (구글 지도 라이브러리)
play-services-location (위치 정보 라이브러리)
1. 프로젝트 생성
(구글 지도를 사용하는 가장 쉬운 방법이 이렇게 기본 액티비티로 맵을 선택하는 것임)
프로젝트 명 : GPSMAP
기본 액티비티 선택 : Google Maps Activity
관련 라이브러리 추가
play-services-maps (구글 지도 라이브러리) - 자동으로 추가됨
play-services-location (위치 정보 라이브러리)
(이전 예제에서 처럼 File 메뉴의 Project Structure... 로 검색하여 추가하면 더 편리)
Anko 라이브러리도 설정할 것
2. 구글지도 표시
1단계 코딩) 구글 지도 API 키 발급받기와 구글 지도 표시
자동으로 열려있는 google_maps_api.xml에 표시된 웹 링크로 들어가면 구글 API 콘솔화면에서
키를 발급받을 수 있습니다. 이전에 프로젝트를 만든 적이 있다면 기존 프로젝트를 선택하면 됩니다.
발급받은 키를 google_maps_api.xml의 "YOUR_KEY_HERE" 부분에 복사해 넣읍시다.
(발급받은 키는 분실하지 않도록 잘 보관할 것. 사용 횟수 제한도 있고 향후 구글지도는 유료화될 예정임)
※ 자동으로 생성되어 있는 MapsActivity.kt 코드(커맨트 번역)
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
// (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
/** (자동 생성된 코드)
* 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)
* 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역 변경 가능 (디폴트로 호주 시드니를 표시됨)
* Google Play 서비스가 설치되어 있지 않으면
* SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨
* 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
// 표시 지역을 시드니로 마킹하고 카메라를 이동시킴
val sydney = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
}
※ 자동으로 생성되어 있는 activity_maps.xml 디자인
트리를 보면, name이 com.google.android.gms....인 특별한 프래그먼트가 하나 배치되어 있는데,
play-services-maps 라이브러리에서 제공됩니다.
2단계 코딩) 주기적으로 현재 위치 정보 업데이트 하기
AndroidManifest.xml 에는 다음과 같은 자동으로 위치 권한이 추가 되어 있을 것입니다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
위치 권한은 위험 권한이므로 실행 중에 권한 요청을 해야 합니다.
(참고로 위치 서비스는 구글 플레이 서비스를 취신 버전으로 업데이트해야 연결됨)
// 주기적인 위치 정보 요청
requestLocationUpdates(locationRequest: LocationRequest, locationCallback: LocationCallback, looper: Looper)
* locationRequiest (위치요청 객체)
* locationCallback (위치가 갱신되면 호출되는 콜백)
* looper (특정 루퍼 스레드 지정. 특별한 경우가 아니라면 null)
! 주의) 위치 정보를 주기적으로 요청하는 코드는 액티비티가 화면에 보일 때에만 수행되도록 할 것!
즉, 아래 코드의 onResume()에서 정보를 요청하고, onPause()에서 요청을 삭제.
3단계 코딩) 소스 코드 참고
4단계 코딩) 소스 코드 참고
- 소스 코드
package com.tistory.gpsmap
import android.Manifest
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.gms.maps.model.PolylineOptions
import org.jetbrains.anko.alert
import org.jetbrains.anko.noButton
import org.jetbrains.anko.toast
import org.jetbrains.anko.yesButton
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
/* (1단계 코딩)
*
* ===== 구글 지도 표시 =====
* (기본 코드는 자동 생성됨)
*
*/
// ① 위치정보를 주기적으로 받는데 필요한 객체들 선언
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var locationRequest: LocationRequest
private lateinit var locationCallback: LocationCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 화면 꺼지지 않게.
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // 세로 모드 고정.
setContentView(R.layout.activity_maps)
// (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
// ② ①에서 선언한 객체들을 onCreate() 마지막에 초기화
locationInit()
}
// ②
private fun locationInit() {
fusedLocationProviderClient = FusedLocationProviderClient(this)
locationCallback = MyLocationCallBack()
locationRequest = LocationRequest() // LocationRequest객체로 위치 정보 요청 세부 설정을 함
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY // GPS 우선
locationRequest.interval = 10000 // 10초. 상황에 따라 다른 앱에서 더 빨리 위치 정보를 요청하면
// 자동으로 더 짧아질 수도 있음
locationRequest.fastestInterval = 5000 // 이보다 더 빈번히 업데이트 하지 않음 (고정된 최소 인터벌)
}
/** (자동 생성된 코드)
* 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)
* 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역을 변경할 수 있음 (디폴트로 호주 시드니를 표시됨)
* Google Play 서비스가 설치되어 있지 않으면 SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨
* 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
// 표시 지역을 시드니로 마킹하고 카메라를 이동시킴
val sydney = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
/* (2단계 코딩)
*
* ===== 현재 위치 요청 하기 =====
*
*/
// ③
private fun addLocationListener() {
fusedLocationProviderClient.requestLocationUpdates(locationRequest,
locationCallback,
null) // 혹시 안드로이드 스튜디오에서 비정상적으로 권한 요청 오류를 표시할 경우, 'Alt+Enter'로
// 표시되는 제안 중, Suppress: Add @SuppressLint("MissingPermission") annotation
// 을 클릭할 것
// (에디터가 원래 권한 요청이 필요한 코드 주변에서만 권한 요청 코딩을 허용했었기 때문임.
// 현재 우리 코딩처럼 이렇게 별도의 메소드에 권한 요청 코드를 작성하지 못하게 했었음)
}
// ④ MyActivity 클래스의 내부 클래스로 생성
inner class MyLocationCallBack: LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
super.onLocationResult(locationResult)
val location = locationResult?.lastLocation // GPS가 꺼져 있을 경우 Location 객체가
// null이 될 수도 있음
location?.run {
val latLng = LatLng(latitude, longitude) // 위도, 경도
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17f)) // 카메라 이동
Log.d("MapsActivity", "위도: $latitude, 경도: $longitude") // 로그 확인 용
/* (4단계 코딩)
*
* 이동 경로 그리기 (여기에서는 구글맵에서 이동 자취 그리기용으로 지원해주는
* 편리한 메서드를 이용)
*
*/
mMap.addPolyline(polylineOptions) // 선 그리기 (위치 정보가 갱신되면
// polyLineOptions 객체에 추가되고
// 지도에 polylineOptions 객체를 추가 함
}
}
}
/* (3단계 코딩)
*
* ===== 실행 중 권한요청 =====
*
* */
private val REQUEST_ACCESS_FINE_LOCATION = 1000
private fun permissionCheck(cancel: () -> Unit, ok: () -> Unit) { // 전달인자도, 리턴값도 없는
// 두 개의 함수를 받음
if (ContextCompat.checkSelfPermission(this, // 권한이 없는 경우
android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_FINE_LOCATION)) { // 권한 거부 이력이 있는 경우
cancel()
} else {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_ACCESS_FINE_LOCATION)
}
} else { // 권한이 있는 경우
ok()
}
}
private fun showPermissionInfoDialog() {
alert("위치 정보를 얻으려면 위치 권한이 필요합니다", "권한이 필요한 이유") {
yesButton {
ActivityCompat.requestPermissions(this@MapsActivity, // 첫 전달인자: Context 또는 Activity
// this: DialogInterface 객체
// this@MapsActivity는 액티비티를 명시적으로 가리킨 것임
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_ACCESS_FINE_LOCATION)
}
noButton { }
}.show()
}
// 권한 요청 결과 처리
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_ACCESS_FINE_LOCATION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
addLocationListener()
} else {
toast("권한이 거부 됨")
}
return
}
}
}
override fun onResume() {
super.onResume()
// 권한 요청
permissionCheck(
cancel = { showPermissionInfoDialog() }, // 권한 필요 안내창
ok = { addLocationListener()} // ③ 주기적으로 현재 위치를 요청
)
}
override fun onPause() {
super.onPause()
removeLocationListener() // 앱이 동작하지 않을 때에는 위치 정보 요청 제거
}
private fun removeLocationListener() {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
}
/* (4단계 코딩)
*
* ===== 이동 경로 그리기 =====
* MyLocationCallBack 클래스
*/
private val polylineOptions = PolylineOptions().width(5f).color(Color.RED)
}
※ 구글 맵에서 이동 자취 그리기를 위해 지원해주는 편리한 메서드들
(만약 이들이 지원하지 않는 그래픽 기능이 필요하다면,
수평 측정기 제작 때처럼 뷰에 그래픽 API를 이용해서 그려야 함)
addPolyLine() - 여러 개의 경로 선을 그림
addCircle() - 원 그리기
addPolygon() - 영역 그리기
구글 지도를 활용하는 프로그램 소스에 특별한 내용은 없습니다.
이 예제는 지도를 그리고 현재 위치를 요청하고 그에 관한 권한을 요청하는 게 전부입니다.
지도를 표시하는 것과 이동 자취를 그리는 기능들을 전부 구글 맵용 라이브러리에서
지원하고 있으니 참 편합니다. 좀 더 세부적인 기능들을 구현하는 게 아니라면 제공되는
라이브러리를 쓰면 될 것 같습니다.
그럼, 이만~
'프로그래밍' 카테고리의 다른 글
[잠깐 휴식] 람다식, 익명함수, 고차함수, 콜백함수란?
코딩의 간결성과 직관적 가독성을 위해 프로그램 작성법이 조금씩 변천되어 왔는데,
특히 함수 표현에 있어 획기적이라고 할 수 있는 표현법이 익명함수와 람다식 입니다.
그리고 프로그래밍 기법에 있어서도 특히 비동기 프로그래밍을 위해
고차함수, 콜백함수들이 도입되었습니다.
여기에서 개선됐다는 기준은 80, 90년대의 프로그램 기법에서 개선됐다는 말입니다. ;;
그러니까 이 기술들에 대한 지원이 시작된 것은 2000년대 부터 였던 것 같네요.
그 이후 지금까지 커다란 변화는 없는 것 같습니다.
각 용어와 표현 방법에 대해 간략하고 쉽게 정리해봤습니다.
람다식
함수를 간결화한 형태.
익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리함.
한편 코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있음.
아래 세 가지 표현은 모두 같은 의미임.
① 일반 함수
fun add(x: Int, y: Int) : Int { // 함수 선언부 (2개의 int형 인자를 받아 int형 값을 리턴)
return x + y // 함수의 내용 본체
}
val sum: Int = add(3, 4)
val sum = add(3, 4) // 이렇게 줄임 표현도 가능
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y // 함수 선언부에 인자 목록은 명시. 리턴 타입은 생략
// 함수 본체의 제일 마직막 표현식에 따라 자동으로 리턴 타입이 추론됨
val sum = add(3, 4)
③ 람다식
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y } // '{ 인수들 -> 함수본체 }' 부분이 람다식임
val sum: (Int, Int) -> Int = {x, y -> x + y } // 이렇게 줄여 표현 하든
var sum = {x: Int, y: Int -> x + y } // 이렇게 줄여 표현하든 모두 가능
// (전달인자든 람다식 안에서든 데이터형 명확히 지정됐으므로)
// 그리고 람다식이 저장된 변수 sum은 일반 함수처럼 호출이 가능함
sum(1,2) // 결과: 3
고차 함수 (High-order Function)
함수의 전달인자나 반환 값의 데이터형에 함수가 포함된 형태.
fun printResult( s: String, r: Float, ff: (num: Float) -> Float) { // *1
val result = ff(r)
println("$s 는 $result")
}
printResult("원의 넓이", 3.0f, {x -> 3.14f * x*x }) // *2
printResult("구의 체적", 3.0f, {x -> 4.0f/3.0f * 3.14f * x*x*x }) // *3
실행 결과)
원의 넓이 는 28.26
구의 체적 는 113.04001
*1 함수의 전달인자를 표시하는 방식은,
문자 : 전후에 함수내에서 사용할 전달인자 이름, 그 전달인자의 데이터형을 명기한 형태입니다.
결국 전달인자로 사용된 함수 부분은,
함수명은 ff, 데이터형은 (num: Float) -> Float) 라고 해석할 수 있습니다.
(num: Float) -> Float)
이 부분이 람다식으로 표현되어 있네요.
Float 형 인자 하나를 받고 Float 형으로 리턴하는 형태의 함수가
전달인자로 사용될 수 있음을 의미합니다.
*2, *3
그림으로 좀 더 쉽게 설명을 해 보자면,...
①,② 각각의 함수가 *1의 전달인자 선언을 통해 printResult()내에서 result 값을 계산하는데 사용되었습니다.
참고로 ①,②는 호출한 함수 printResult에 의해 거꾸로 호출되는데, 이와 같은 함수를 '콜백함수'라고 합니다.
한편, 이번 예에서는 이 함수들을 이름 없이 (이름이 없으므로 '익명(Anonymous) 함수'라고 부릅니다)
함수 본체만을 람다식 형태로 전달했지만, 여러 부분에서 이 함수들을 반복해서 참조한다면
이름을 갖는 함수를 정식으로 정의하고 그 함수명을 전달해줘도 됩니다.
참고) 람다식을 사용할 때 몇몇 표현의 융통성
*2, *3의 표현은 (눈치를 챘는지 모르겠지만) 아래의 표현에서 전달인자 데이터형을 생략한 것 입니다.
printResult("원의 넓이", 3.0f, {x: Int -> 3.14f * x*x })
// (데이터형이 printResult()선언부에 이미 작성되어 있으므로 생략 가능)
*2, *3처럼 함수를 호출할 때 제일 마지막 전달인자가 람다식인 경우,
람다식을 ()밖에 적어도 됩니다.
printResult("원의 넓이", 3.0f) {x -> 3.14f * x*x } // 람다식만 밖으로 꺼내 작성했음
*2, *3처럼 심지어 람다식의 전달인자가 단 하나 뿐이라면, 람다식의 전달인자를 아예 생략해도 됩니다.
그리고 그 전달인자는 키워드 it으로 참조할 수 있습니다.
printResult("원의 넓이", 3.0f) {3.14f * it*it }
fun sum( x: Int, y: Int, prt: (num: Int) -> Unit) { // Unit는 리턴형이 없다는 의미임(void와 같음)
val total: Int
total = x + y
prt( total )
}
sum(3,4) {println(it)} // 결과: 7 (람다식의 표현이 거의 깡패 수준이죠?...)
뿐만아니라, 고차함수의 전달인자가 람다식 하나 뿐이라면, 고차함수를 호출할 때 ()를 생략할 수도 있습니다.
fun sum(prt: (num: Int) -> Unit) { // 전달인자가 람다식 하나 뿐이면,
prt(100)
}
sum() {print(it)}
sum {print(it)} // 호출 때 빈 ()를 생략 가능함
※ 콜백함수의 활용
비동기 프로그래밍에서 작업시간이 오래 걸리는 함수에게 콜백함수를 전달해주고,
메인 프로그램은 다음 동작을 계속 진행.
예)
fun longTimeJob( ..., callback() ) {
(오래걸리는 작업 - DB, 통신, 사용자이벤트, ...)
callback() // 오래걸리는 작업 완료
}
메인프로그램작업1
longTimeJob( ..., cBackFun() ) // 콜백함수를 넣어 longTimeJob() 호출 후 다음 작업 계속
메인프로그램작업2
...
...
참고로, 이런 활용이 UI등에서 사용자 응답 이벤트를 다루는 경우에 이뤄진다면,
언제 발생할 지 모르는 응답에 대해 대기를 해야 하는 '이벤트 리스너' 함수에게
그 응답을 처리할 작업을 담고 있는 '이벤트 핸들러(= 이벤트 처리기)' 함수(콜백함수)를 전달하게 됩니다.
말하자면 다음과 같은 모양이 됩니다.
fun 리스너(핸들러) {
(센서등 응답 대기...)
핸들러()
}
메인프로그램작업1
리스너(핸들러)
메인프로그램작업2
...
...
읽을 거리) 그럼 함수의 전달인자로 함수를 받을 수 없었던 예전에는 버튼 이벤트를 어떻게 처리했나?
(함수가 객체는 받을 수 있었다. 자바에서 변수는 곧 객체)
이벤트 핸들러 구현용 인터페이스를 개발팀 공용으로 만들어 놓고,
각 팀원들이 그 인터페이스를 구현한 자신들만의 클래스를 만들고 객체화하여 리스너 함수에 전달함으로써
콜백함수를 구현했습니다.
아래 버튼 이벤트 처리의 경우, 인터페이스의 추상메서드 onClick()을 구현한 이벤트 핸들러 객체
'new onClickListener()'를 전달하고 있습니다.
Button button = findViewID(R.id.button)
button.setOnClickListener( new onClickListener() {
@override
public void onClick(View view) {
// 이벤트 처리
}
}
간략화된 표현식에 대해서는 오히려 혼동을 주는 경우도 있을 수 있습니다.
각자 편한대로 코딩하면 됩니다. 그래도 내용은 알고 있는 게 좋겠죠. 다른 프로그램을 해독하려면요.
그럼, 이만~
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (12/16) - 앱 만들기 5 (전자 액자)
=== 전자 액자 ===
기능 소개
안드로이드 기기에 있는 사진 파일들을 보여줍니다.
3초마다 자동으로 사진이 슬라이드 됩니다.
핵심이 되는 주제
프로바이더를 이용해 사진 데이터를 조회
외부 메모리 데이터로의 접근 권한 요청
프래그먼트 생성
Glide 라이브러리 사용
뷰 페이저, 페이저 어댑터의 사용
1) 프로젝트 생성
프로젝트 명 : MyGallery
Anko 라이브러리 설정할 것
2) 프로바이더 사용
컨텐츠 프로바이더는 데이터베이스, 파일, 네트워크등의 데이터를 앱들에게 공유해 주는 컴포넌트다.
사진을 촬영하면 사진 정보가 안드로이드 미디어 데이터베이스에 저장되는데,
이 사진 정보를 '콘텐츠 프로바이더(Contents Provider)'를 통해 얻어올 수 있다.
(단, 사진 정보는 외부저장소에 저장되므로 반드시 외부저장소 읽기 권한 요청 필수!)
콘텐츠 프로바이더를 포함해 안드로이드 기기에는 중요한 4가지 컴포넌트들이 있다.
※ 안드로이드 기기의 4대 컴포넌트
액티비티 (화면 구성)
콘텐츠 프로바이더 (데이터베이스, 파일, 네트워크 데이터 제공)
브로드캐스트 리시버 (앱이나 기기에서 발송하는 데이터를 수신)
서비스 (화면에 표시되지는 않고 백그라운드에서 동작)
※ 안드로이드 기기의 2가지 저장소
내부 저장소 (OS가 설치되어 있으며 유저가 접근할 수 없는 시스템 영역)
외부 저장소 (유저 영역)
※ 안드로이드 기기의 권한의 종류 2가지
보통(normal) 권한 - 매니페스트에 권한 추가 (예: 인터넷 사용 권한)
위험(dangerous) 권한 - 매니페스트에 권한 추가 + 앱 실행시 사용자의 승인 필수 (예: 외부 저장소 읽기 권한)
주의) 위험 권한을 사용하는 앱의 경우, 이미 사용자의 승인을 얻었다고 해도 권한과 관련된 기능 사용시
매번 사용자 승인완료 상태를 체크해야 한다. 왜냐하면 사용자가 언제든 권한을 취소할 수 있기 때문.
자주 사용되는 위험 권한들의 예)
STORAGE |
READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
LOCATION |
ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION |
SMS |
SEND_SMS RECEIVE_SMS |
CAMERA |
CAMERA |
※ 사진 정보 가져오기 구현 (콘텐츠 프로바이더 사용법임)
① 아래 getAllPhotos()의 *1처럼 contentResolver 객체로 Cursor라는 객체를 얻어오면 그 안에
사진 데이터가 담겨져 옴
class MainActivity : AppCompatActivity() {
private val REQUEST_READ_EXTERNAL_STORAGE = 1000
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 권한 승인이 안되어 있는 경우
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_EXTERNAL_STORAGE)) { // true: 거부한 적이 있음
// 이전에 이미 권한 거부가 있었을 경우 설명 (Anko 라이브러리를 쓰면 편하다)
alert("사진을 표시하려면 외부 저장소 권한이 필요합니다!", "권한이 필요한 이유") {
yesButton {
ActivityCompat.requestPermissions( // 권한 요청
this@MainActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_READ_EXTERNAL_STORAGE) // 권한 요청에 대한 분기 처리를 위해
// 만든 적당한 정수 값임
}
noButton { }
}.show()
} else {
// 이전에 권한 거부가 없었을 경우 권한 요청
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_READ_EXTERNAL_STORAGE)
}
} else {
// 권한이 이미 승인되어 있는 상태
getAllPhotos()
}
}
private fun getAllPhotos() {
val photosCursor = contentResolver.query( // *1
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // 가져올 데이터의 URI
// EXTERNAL_CONTENT_URI 는 외부 저장소를 의미함
null, // 가져올 항목들을 문자열 배열로 지정 (null : 모든 항목을 가져옴)
null, // 조건1 (null : 전체 데이터)
null, // 조건2 (조건1과 조합하여 조건 지정)
MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC") // 정렬방법 (촬영날짜 내림차순)
// 이 코드는 사진이 제대로 읽어지는지 로그로 확인해 보려고 작성
if (photosCursor != null) { // photosCursor == null 이면 사진이 없는 것임
while(photosCursor.moveToNext()) { // PhotosCursor 객체 내부에 데이터 이동용 포인터가 있음
val uri = photosCursor.getString(
photosCursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
// 사진의 경로가 저장된 데이터베이스의 컬럼명은 DATA 상수에 정의되어 있음
Log.d("MainActivity", uri) // 안드로이드 스튜디오의 Logcat에서
// MainActivity로 필터링 했을 때 사진의 URI가 표시됨
}
photosCursor.close() // 이 객체를 더 이상 사용하지 않으므로 닫아줘야 함 (메모리 누수 방지)
}
}
}
② 매니페스트에 외부 저장소 읽기 권한 설정
-- AndroidManifest.xml --
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
일단 이 단계에서 실행해보자. 아직 사진이 표시되지는 않지만 Logcat에 사진 경로가 표시될 것이다.
(사진이 많으면 시간이 좀 걸릴 수도 있음)
3) 전자 액자 구현
사진들을 좌우로 돌려볼 수 있도록 '프래그먼트UI' 요소와 'ViewPage'로 구현.
메모리 관리와 성능 향상을 위해서 'Glide' 라이브러리를 활용.
※ 프래그먼트 (Fragment; 파편)
웹 페이지 프레임처럼 화면에 구획한 프레임을 말하며 한 번 프레임을 짜놓으면 재사용도 가능하다.
액티비티처럼 생명주기를 가지고 있는데 액티비티보다 더욱 세분화되어 있다.
※ 본 예제에서 사용할 세 가지 메서드
onCreate()
프래그먼트 생성시 호출 (레이아웃 완성 전)
onCreateView()
프래그먼트에 표시할 뷰를 레이아웃 파일에서 읽어 옴
(레이아웃 완성 전)
onViewCreated()
생명주기에는 포함되지 않는 특별 메서드
완성된 레이아웃 뷰를 받아 이벤트 처리
※ 참고
onAttach() : 액티비티에 붙을 때 호출
onActivityCreate() : 액티비티의 onCreate() 실행 직후 호출
onStart() : 프래그먼트가 사용자에게 보여질 때 호출
onResume() : 사용자와 상호작용 시작
onPause() : 프래그먼트 일시중지 (사용자와 상호작용 안함)
onStop() : 프래그먼트 중지
onDestroyView() : 프래그먼트 자원 해제
onDestroy() : 프래그먼트 제거
onDetach() : 프래그먼트가 액티비티에서 완전히 제거
① 프래그먼트 생성
File > New > Fragment > Fragment(Blank)
(이름: PhotoFragment --> 레이아웃 이름은 자동으로 fragment_photo가 될 것임)
(체크박스해제: 'Include interface callbacks?' ∵ 본 예제에서 사용하지 않음)
아래의 두 개 파일이 생성될 것임 (액티비티 처럼 두 개의 파일로 구성됨)
app/java/팩키지명/PhotoFragment.kt
app/res/layout/fragment_photo.xml ( <-- 디자인에는 텍스트 뷰가 하나 포함되어 생성됨)
※ 옵션 설명
Create layout XML? (본 예제에서는 XML을 사용하므로 체크)
Include fragment factory methods? (팩토리 메서드를 이용해 생성. 생성시 인자도 넘겨줌. 체크)
Include interface callbacks? (액티비티와 상호 작용하는 콜백 인터페이스를 가져옴.
프래그먼트에서 발생한 이벤트를 액티비티로 전달할 경우 체크)
② 트리창에서 루트 레이아웃인 FrameLayout에서 컨텍스트 메뉴를 이용해 ConstraintLayout으로 변경(옵션:디폴트)
(변환이 되면 레이아웃 아이콘 모양이 바뀐다)
(트리창의 루트 레이아웃 이름은 id가 표시된 것임 --> 본 예제에서는 ID를 사용하지 않으므로 id를 삭제할 것)
③ 트리창에서 자동으로 생성된 텍스트뷰를 삭제하고,
Autoconnect 모드를 켜고 여백은 0dp로 설정한 후 팔레트창에서 ImageView를 정 중앙에 배치
이미지 리소스 선택창이 나오면 샘플 이미지를 아무거나 선택 (디자인 하는 동안에만 임시로 표시됨)
이미지 뷰 속성 >> id: imageView1, layout_width와 layout_height: match_constraint, scaleType: centerCrop
④ Glide 라이브러리 사용 준비
다음과 같이 setImageURI()를 이용해서 이미지 뷰에 사진을 표시할 수도 있지만,
imageView.setImageURI(Uri.parse("/storage/emulated/0/DCIM/Camera/aaaaa.jpg"))
효율적인 리소스 관리를 자동으로 해주고 이미지 비동기 로딩으로 UI의 끊김이 없는
Glide 라이브러리를 이용하자.
-- build.gradle 에 의존성 추가후 Sync Now.
(버전은 https://github.com/bumptech/glide 에서 최신 버전을 확인해서 적을 것)
dependencies {
...
implementation 'com.github.bumptech.glide:glide:4.11.0' // 2020.05.13 버전
}
※ build.gradle 파일을 수정하지 않고 직접 안드로이드 스튜디오의 메뉴를 활용하는 방법도 있다.
File > Project Structure 클릭 > Dependencies > '+'눌러 추가 > 검색창에 glide 입력후 검색
자동으로 검색된 라이브러리들 중에 필요한 항목을 선택하고 (자동으로 최신 버전도 표시됨) OK 버튼 클릭.
⑤ PhotoFragment.kt 파일 코딩
자동으로 작성되어 있는 코드를 아래와 같이 수정하면 된다.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
private const val ARG_URI = "uri" // 클래스 밖에서 상수를 정의하면 컴파일 시 상수가 초기화됨
// 컴파일 시 상수 초기화는 프리미티브형(Int, Long, Double등 기본형)만 가능
class PhotoFragment : Fragment() {
private var uri: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
uri = it.getString(ARG_URI) // 프래그먼트가 생성되고 onCreate() 호출되면
// ARG_URI키에 저장된 uri값을 얻어서 변수에 저장
}
}
// 프래그먼트에 표시될 뷰를 생성함
// 액티비티가 아닌 곳에서 레이아웃 리소스를 가져오려면 LayoutInflater객체의 inflate()를 사용함
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_photo, container, false)
}
// newInstance()를 이용해서 프래그먼트를 생성할 수 있고 인자로 uri 값을 전달
// 이 값은 Bundle 객체에 ARG_URI 키로 저장되고 arguments 프로퍼티에 저장됨
companion object {
@JvmStatic
fun newInstance(uri: String) =
PhotoFragment().apply {
arguments = Bundle().apply {
putString(ARG_URI, uri)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Glide.with(this).load(uri).into(imageView1) // 사진을 이미지뷰에 표시
// with(): 사용준비, load(): 이미지 로드, into(): 이미지 표시
}
}
⑥ 액티비티에 ViewPager 추가
activity_main.xml 디자인 창 > Containers > ViewPager를 화면 정중앙에 배치.
(id: viewPager1, layout_width, layout_height: match_constraint)
(디자인창을 이용하는 경우에는 안내와 함께 필요한 라이브러리가 자동으로 의존성으로 추가됨)
※ 뷰페이저란?
여러 프래그먼트들을 좌우로 슬라이드하는 뷰. (별도의 라이브러리에 있는 뷰이므로 의존성 추가 필요)
프래그먼트 목록용 컨테이너('페이저 어댑터'라고 부름)는 다음의 두 가지가 있는데, 이를 뷰페이저에
연결하면 사진이 표시된다. (본 예제에서는 FragmentStatePagerAdapter를 사용함)
FragmentPagerAdapter (페이지들 로딩후 계속 메모리 상주. 속도는 빠르나 메모리 많이 사용)
FragmentStatePagerAdapter (보이지 않는 페이지는 메모리에서 제거. 페이지가 많을 때 적합)
⑦ 페이저 어댑터 작성
File > New > 'Kotlin File/Class' (이름: MyPagerAdapter1, 종류: Class)
(자동으로 생성된 코드를 아래와 같이 조금 수정하면 됨)
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
class MyPagerAdapter1(fm: FragmentManager?) : FragmentStatePagerAdapter(fm) {
private val items = ArrayList() // 뷰페이저가 표시할 프래그먼트 목록
override fun getItem(position: Int): Fragment {
return items[position]
}
override fun getCount(): Int {
return items.size // 아이템의 개수
}
fun updateFragments(items: List) {
this.items.addAll(items) // 외부에서 추가
}
}
⑧ MainActivity.kt의 getAllPhotos() 코드 추가
private fun getAllPhotos() {
val photosCursor = ...
val fragmentArray = ArrayList()
if (photosCursor != null) {
while (photosCursor.moveToNext()) {
val uri = photosCursor.getString(photosCursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
Log.d("MainActivity", uri)
fragmentArray.add(PhotoFragment.newInstance(uri)) // 사진을 photosCursor 객체에서 가져올 때마다
// 새 프래그먼트를 생성하여 추가
}
photosCursor.close()
}
val adapter1 = MyPagerAdapter1(supportFragmentManager)
// 프래그먼트 관리자는 getSupportFragmentManager()로 가져올 수 있는데,
// 코틀린에서는 supportFragmentManager 프로퍼티로 접근할 수 있음
adapter1.updateFragments(fragmentArray)
viewPager1.adapter = adapter1
...
}
이 단계에서 실행해 보고 사진이 좌우로 잘 슬라이드되며 표시되는 지 확인해 보자.
(이전 단계에서 사진이 제대로 읽어지는지 로그로 확인해 보려고 작성했던 코드는 리마크할 것
- 그냥 놔두면 photosCursor.close()에서 오류가 날 수 있다)
4) 슬라이드 쇼 구현
getAllPhotos()에 간단한 코드를 추가하여 3초마다 자동으로 사진이 슬라이드 되도록 만들어 보자.
private fun getAllPhotos() {
...
timer(period = 3000) {
runOnUiThread {
if (viewPager1.currentItem < adapter1.count - 1) {
viewPager1.currentItem = viewPager1.currentItem + 1
} else {
viewPager1.currentItem = 0
}
}
}
}
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (11/16) - 앱 만들기 4 (수평 측정기)
수평 측정기
기능 소개
기기의 수평 상태를 보여줍니다.
핵심이 되는 주제
가속도 센서 활용법
액티비티의 생명 주기에 대한 학습
커스텀 뷰를 만드는 방법
기본적인 그래픽 기능 실습 (원, 선 그리기)
사전 지식
1) 액티비티의 생명 주기
액티비티는 아래의 순서로 생성되고 소멸합니다.
우리가 구현하려는 기능을 적절한 메소드에 코딩을 해줘야 우리가 원하는 시점에 맞춰 실행이 됩니다.
모든 앱은 백그라운드 실행중에 메모리 확보등을 이유로 언제든지 강제 종료될 수 있습니다.
그리고 그 후 다시 실행하면 onCreate()부터 다시 호출되는 거죠.
센서를 사용하는 모바일 기기용 프로그램을 제작할 때에는 배터리 소모를 항상 고려해야 합니다.
배터리를 절약하려면 꼭 필요한 때에만 센서를 동작시키는 게 좋겠죠?
2) 안드로이드가 지원하는 센서의 종류
( * 우리는 가속도 센서를 사용할 예정입니다)
종류 |
용도 |
|
중력 센서 |
흔들림, 기울임등 동작 감지 |
TYPE_GRAVITY |
가속도 센서 |
흔들림, 기울임등 동작 감지 |
TYPE_ACCELEROMETER |
선형 가속도 센서 |
단일 축을 따라 가속 모니터링 |
TYPE_LINEAR_ACCELERATION |
자기장 센서 |
나침반 |
TYPE_MAGNETIC_FIELD |
방향 센서 |
장치 위치 결정 |
TYPE_ORIENTATION |
자이로 센서 |
회전 감지 |
TYPE_GYROSCOPE |
회전 센서 |
회전 감지, 모션 감지 |
TYPE_ROTATION_VECTOR |
주변 온도 센서 |
대기 온도 모니터링 |
TYPE_AMBIENT_TEMPERATURE |
근접 센서 |
통화중인지 검사 |
TYPE_PROXIMITY |
조도 센서 |
화면 밝기 제어 |
TYPE_LIGHT |
기압 센서 |
공기압 모니터링 변화 |
TYPE_PRESSURE |
온도 센서 |
온도 감지 |
TYPE_TEMPERATURE |
상대 습도 센서 |
이슬점, 상대 습도 모니터링 |
TYPE_RELATIVE_HUMIDITY |
3) 센서 값을 받는 빈도
빈도가 빈번할 수록 배터리를 많이 사용합니다.
SENSOR_DELAY_FASTEST |
빈번하게 |
SENSOR_DELAY_GAME |
게임에 적합한 정도로 |
SENSOR_DELAY_NORMAL |
화면 방향이 전환될 때에 적합한 정도 |
SENSOR_DELAY_UI |
UI 표시에 적합한 정도 |
4) 안드로이드 좌표축
※ 가속도 센서의 동작
가속도 = '중력가속도'를 의미합니다.
중력가속도를 9.8 이라고 할 때,
중력이 x, y, z 각 방향으로 작용하는 정도에 비례해서 값이 분배됩니다.
(x, y, z 각 방향의 값의 총합은 항상 9.8이 됨)
음... 그러니까, 쉽게 예를 들어 볼게요.
휴대폰 화면을 하늘로 향한 상태로 바닥에 놓으면 z축으로만 중력이 작용하므로 --> x=y=0, z=9.8
휴대폰 좌측 변을 기준으로 화면을 왼쪽 방향으로 90도 세우면 x축으로만 중력이 작용하므로 --> x=9.8, y=z=0
(우측 변을 기준으로 화면을 오른쪽 방향으로 90도 세우면 x=-9.8, y=z=0)
휴대폰 하측 변을 기준으로 화면을 정면을 향해 90도 세우면 y축으로만 중력이 작용하므로 --> x=z=0, y=9.8
(상측 변을 기준으로 화면을 뒤로 향하도록 90도 세우면 x=z=0, y=-9.8)
※ 센서 사용법 (예: 가속도 센서)
아래는 가속도 센서를 사용하기 위한 코딩입니다. 설명을 덧붙였으니까 천천히 살펴보세요~
(코드가 눈에 익지 않다면 생소해 보일 수도 있지만, 딱히 어려운 내용은 없습니다)
class MainActivity : AppCompatActivity(), SensorEventListener { // ① 센서 이벤트 리스너 상속
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// ② 센서관리자 객체 얻기
private val sensorManager1 by lazy { // 지연된 초기화는 딱 한 번 실행됨
getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
// ③ 리스너 등록
override fun onResume() { // 앱이 사용될 때에만 동작 (배터리 절약)
super.onResume()
sensorManager1.registerListener(
this, // 센서 이벤트 값을 받을 리스너 (현재의 액티비티에서 받음)
sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), // 센서 종류
SensorManager.SENSOR_DELAY_NORMAL // 수신 빈도
)
}
// ④ 필요한 메서드들 재정의
// 안드로이드 스튜디오에서 추상메서드를 구현하라는 오류 안내를 이용하면 코딩이 편리. (아래 그림 참조)
// (구현할 메서드들을 ctrl+A로 전체선택하자)
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { // 센서 정밀도 변경시
}
override fun onSensorChanged(event: SensorEvent?) { // 센서 값 변경시
event?.let {
Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")
// [0] x축값, [1] y축값, [2] z축값
}
}
// ⑤ 리스너 해제
override fun onPause() {
super.onPause()
sensorManager1.unregisterListener(this)
}
}
참고) 위에서 Log 클래스의 메소드는 다음과 같은 형태로 사용합니다.
Log.d([태그], [출력할 메시지]) // 태그 : 로그캣에는 많은 내용이 표시되므로 필터링할 때 사용함
기타, ...
Log.e() 에러메시지를 출력할 때 자주 사용함
Log.w() 경고를 표시할 때 자주 사용함
Log.i() 정보성 로그를 표시할 때 자주 사용함
Log.v() 모든 로그를 표시할 때 자주 사용함
실습
일단 센서만 동작시켜 봅시다.
1) 빈 액티비티를 생성 (앱 이름 : TiltSensor)
잘 모르면 이전의 앱 제작 과정을 참고하세요~
2) 레이아웃 작업과 코딩
여기서는 커스텀 뷰를 제작할 때, 레이아웃 작업을 디자인 창에서 하지 않고 그냥 코딩으로 했습니다.
코딩으로 이해하는 게 훨씬 쉽기 때문입니다.
class MainActivity : AppCompatActivity(), SensorEventListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private val sensorManager1 by lazy { // 지연된 초기화는 딱 한 번 실행됨
getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
override fun onResume() {
super.onResume()
sensorManager1.registerListener(
this,
sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL
)
}
override fun onPause() {
super.onPause()
sensorManager1.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")
// [0] x축값, [1] y축값, [2] z축값
Log.d(TAG, "onSensorChanged: " +
"x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
tiltView.onSensorEvent(event)
}
}
}
3) 실행 테스트
여기까지 작업했다면 일단 센서가 올바르게 동작하는지 테스트 해 볼 수 있습니다.
앱을 실행하고 안드로이드 스튜디오 하단의 'Logcat'탭을 클릭한 후,
① 연결된 기기, ② 실행중인 프로세스명 , ③ 필터링 태그(MainActivity)를 설정하면
우리가 작성한 로그만 볼 수 있습니다. x, y, z 축의 가속도 값들이 표시되고 있네요.
※ 커스텀 뷰 제작 및 사용법
일단 우리가 만들려는 수평 측정기는 시중에서 판매되는 아래 상품을 모델로 하고 있습니다.
위 수평 측정기들 중 원 모양의 측정기를 화면에 그려 사용합시다.
3) 커스텀 뷰 제작을 위한 코딩
이제 사용자 정의 화면(커스텀 뷰)에 수평 측정기의 원 모양을 화면에 그려서 메인 액티비티에 표시해 보죠.
그리고 그 화면에 센서 값을 전달하고 값에 따라 원의 위치가 새로 그려지도록 해야 합니다.
일단 커스텀 뷰를 위한 클래스 파일을 추가로 생성해야 합니다.
우리가 만들 커스텀 뷰의 이름은 TiltView 라고 합시다.
① File > New > Kotlin File / Class 를 이용하여 빈 클래스 파일을 추가 하세요.
Name: TiltView
종류: Class
② View 상속받기
'class TileView'에 이어 ': View'를 입력하고 나타나는 자동완성기능에서 View(android.view)를 선택
빨간 줄이 표시되는 View에 커서를 놓고 'Alt+Enter'를 눌러 제안 목록 중, Context를 인자로 받는 생성자 선택
③ 추가된 빈 클래스 파일을 열어 아래와 같이 코딩 하세요.
class TiltView(context: Context?) : View(Context?) { // View 상속 (View라고 입력하면 여러 생성자가 나옴)
// 그 중에 Context를 인자로 하는 생성자를 선택
private val paintGreen: Paint = Paint() // (페인트는 붓에 비유됨)
private val paintBlack: Paint = Paint()
private var cX: Float = 0f // 센터좌표 x
private var cY: Float = 0f
init {
paintGreen.color = Color.GREEN
paintBlack.style = Paint.Style.STROKE // STROKE (외곽선만 그림)
// FILL | FILL_AND_STROKE | STROKE
} // 디폴트: FILL
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { // 뷰의 크기를 얻기 위함
// 안드로이드 좌표계는 좌측 상단이 (0, 0)
// w(변경된폭), h, oldw, oldh(변경전높이)
super.onSizeChanged(w, h, oldw, oldh)
cX = w / 2f // 센터좌표 x 산출
cY = h / 2f
}
override fun onDraw(Canvas: Canvas?) { // (캔버스는 도화지에 비유됨)
super.onDraw(canvas)
canvas?.drawCircle(cX-xx, cY-yy, 100f, paintGreen) // ⓑ원내부. (테두리와 같은 크기의 꽉찬 원)
canvas?.drawCircle(cX, cY, 100f, paintBlack) // ⓐ테두리. x(float), y(float), 반지름(float), 색(Paint!)
canvas?.drawLine(cX-20, cY, cX+20, cY, paintBlack) // ⓒ중앙의 십자 수평선 x1,y1,x2,y2,색
canvas?.drawLine(cX, cY-20, cX, cY+20, paintBlack) // ⓒ중앙의 십자 수직선
}
// 센서 값은 SensorEvent로 전달됨. 이 값을 받아오는 onSensorEvent()를 정의하자.
private var xx: Float = 0f
private var yy: Float = 0f
fun onSensorEvent(event: SensorEvent) {
// x값: values[0], y값: values[1] 이지만, 화면을 가로방향으로 바꿔 사용하므로 서로 뒤바꿔서 사용
// 눈에 띄일 정도의 화면 좌표로 사용하기 위해 스케일을 약 20배
yy = event.values[0] * 20
xx = event.values[1] * 20
invalidate() // 뷰를 다시 그리도록 onDraw()를 다시 호출해 주는 역할
}
}
③ MainActivity의 코드를 아래와 같이 수정하여,
TiltView를 MainActivity에 등록하고 화면 제어 설정을 하세요.
import android.content.ContentValues.TAG
class MainActivity : AppCompatActivity(), SensorEventListener {
...
private lateinit var tiltView: TiltView // 늦은 초기화
// onCreate() 기존 코딩을 아래와 같이 수정
override fun onCreate(...) {
...
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE // 화면 방향: 가로
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) // 화면 꺼짐 방지
tiltView = TiltView(this) // 커스텀 뷰 배치
setContentView(tiltView) // R.layout.activity_main 을 tiltView 로 바꿀 것
// (이로써 tiltView가 전체 레이아웃이 됨)
}
override fun onSensorChanged(event: SensorEvent?) {
// values[0]:x, values[1]:y, values[2]:z z값은 불필요
event?.let {
Log.d(TAG, "onSensorChanged: " +
"x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
tiltView.onSensorEvent(event) // TiltView에 센서 값을 전달
}
}
}
실행한 후, 모바일 기기를 이리저리 기울여 보십시오. 그러면 가운데 동그라미가 움직이며 수평 상태를 표시해 줍니다.
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (10/16) - 앱 만들기 3 (나만의 웹 브라우저)
나 만의 웹 브라우저
기능 소개
웹 서핑용으로 사용할 수 있고 나만의 몇 몇 메뉴와 컨텍스트 메뉴를 가지고 있습니다.
핵심이 되는 주제
웹 뷰 (인터넷 사용 권한 필요)
메뉴 구성
컨텍스트 메뉴 구성
암시적 인텐트 사용
실습
1) 빈 액티비티를 생성 (앱 이름 : MyWeb) & Anko 라이브러리 추가
잘 모르면 이전의 앱 제작 과정을 참고하세요~
2) 레이아웃 작업
① Plain Text 뷰 배치 (id: URLEditText , inputType: textUri, hint: http://, imeOptions: actionSearch)
(입력 자료형을 textUri로 선택하면 입력할때 소프트키보드도 URL 입력에 편리한 자판 배열로 표시됩니다)
(또 hint를 설정하면 사용자에게 웹주소를 입력해야 함을 알려줄 수 있죠)
actionSearch : 소프트키보드의 서치아이콘(돋보기 처럼 생긴...) 활성화 시켜 줍니다.
② webView 뷰 배치 (id: webView)
인터넷 권한 추가 (앱 설치시 사용자에게 권한 허용을 요청하게 됨)
-- AndroidManifest.xml
<manifest xmlns:...>
...
<uses-permission android:name="android.permission.INTERNET" />
...
</manifestxmlns>
③ 코딩 (웹뷰를 위한)
-- MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView.apply {
settings.javaScriptEnabled = true // 자바스크립트 사용 설정
webViewClient = WebViewClient() // webViewClient 객체를 생성하여 전달
// 이 행이 없으면 폰 자체 웹 브라우저가 동작함
}
webView.loadUrl("http://www.google.com") // 구글페이지 로드
URLEditText.setOnEditorActionListener { _, actionId, _ -> // 자동완성 기능으로 보면 v, actionId, event
// (반응한 뷰, 액션ID, 이벤트)
// 세 개의 인수를 사용하지만 여기선 actionId만 사용
// (사용하지 않는 인자는 _로 작성)
// 이 리스너는 editText에 글자가 입력될 때마다 호출됨
if (actionId == EditorInfo.IME_ACTION_SEARCH) { // 검색버튼이 눌렸는가?
webView.loadUrl(URLEditText.text.toString())
true // true를 반환하며 이벤트 종료
} else {
false
}
}
override fun onBackPressed() { // 액티비티에서 뒤로가기 키 이벤트 onBackPressed() 재정의(오버라이드)
if (webView.canGoBack()) {
webView.goBack() // 이전페이지로 갈 수 있으면 이전페이지로 이동하고,
} else {
super.onBackPressed() // 그렇지 않다면 본래의 동작을 수행(즉, 종료)함
}
}
}
일단 이쯤에서 실행하면 기본적인 웹 서핑이 가능합니다.
이제 옵션 메뉴와 컨텍스트 메뉴를 만들어 볼까요? 일단 메뉴는 별도의 디렉토리로 관리합니다.
File > New > Android Resource Directory 로 리소스 디렉토리 생성 (Resource type : menu)
④ 옵션 메뉴 (앱 우측 상단에 …으로 표시되는 메뉴)
i) 메뉴용 리소스 준비
생성된 Android > app > res > menu 마우스 우클릭 > Menu resource file (File name: main)
이렇게 하면 main.xml이 생성됩니다.
메뉴에 사용할 벡터 이미지 준비 (여기서는 홈 버튼에 사용할 이미지를 준비해야 하는데,
Vector Asset의 Clip Art의 검색 창에서 home으로 검색하면 곧바로 찾아줍니다~)
ii) 메뉴 구성하기
팔레트 창에서 'Menu Item'뷰를 끌어다 컴포넌트트리 창 menu 하위에 배치 (title: 검색사이트)
검색사이트 메뉴 아이템 하위에 'Menu'뷰를 끌어다 배치
Menu 뷰 하위에 세 개의 'Menu Itemp'뷰를 끌어다 배치
(id: action_daum, title: 다음)
(id: action_google, title: 구글)
(id: action_naver, title: 네이버)
같은 방법으로 검색사이트 레벨의 메뉴 아이템과 그 하위에 세 개의 메뉴 아이템들을 갖는 메뉴를
하나 더 생성 (메뉴아이템title: 개발자 정보) 하세요.
(id: action_call, title: 전화하기)
(id: action_send_text: 문자보내기)
(id: action_email: 이메일보내기)
검색사이트 레벨의 메뉴 아이템을 하나 더 추가 하세요.
(id: action_home, title: Home, icon: 위에서 만든 집 모양 벡터이미지)
툴바 밖으로 이 메뉴 아이템을 노출시키기 위해 showAsAction을 ifRoom으로 설정해 줍니다.
(아래 그림에서 보면 집 모양 아이콘이 메뉴에서 빠져나와 앱의 툴바 영역에 나타났음)
※ 참고
never
ifRoom(툴바에 여유가 있을때만)
withText(글자와 아이콘을 함께 표시)
collapseActionView (액션 뷰와 결합하면 축소되는 메뉴 생성 가능)
iii) 코딩 (메뉴를 위한)
-- MainActivity.kt
class MainActivity : AppCompatActivity() {
...
// 액티비티에서 onCreateOptionsMenu()를 오버라이드하여 메뉴 리소스 파일을 지정하면 메뉴가 표시됨
override fun onCreateOptionsMenu() : Boolean {
menuInflater.inflate(R.menu.main, menu) // MainActivity의 메뉴로 등록 (* inflate 바람넣다. 부풀리다(과장하다))
return true // 반드시 true를 반환하여 액티비티가 있음을 인식시켜줘야 함
}
// 각 옵션 메뉴 클릭 이벤트 처리기(이벤트 핸들러)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item?.itemId) {
R.id.action_daum -> {
webView.loadUrl("http://www.daum.net")
return true // 처리를 끝낸후엔 정상종료 됐음을 알리기 위해 true를 반환
// (그리고 안드로이드에서는 자신의 작업을 하는 경우를 제외한 모든 경우에
// super 메소드를 호출하는 것이 기본규칙임
}
R.id.action_google, R.id.action_home -> { // 구글 메뉴 또는 홈버튼
webView.loadUrl("http://www.google.com")
return true
}
R.id.action_naver -> {
webView.loadUrl("http://www.naver.com")
return true
}
R.id.action_call -> {
// 전화 걸기 코드 - '암시적 인텐트'를 사용 (예제 끝 부분에 별도로 설명)
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:02-1234-5678")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
return true
}
R.id.action_send_text -> {
// 메시지 발송 코드 (*k 마지막에 별도 정리)
return true
}
R.id.action_email -> {
// 이메일 발송 코드 (*k 마지막에 별도 정리)
return true
}
}
return super.onOptionsItemSelected(item)
}
}
⑤ 컨텍스트 메뉴 (특정 뷰를 길게 누르고 있을 때 나타나는 메뉴)
이번에는 컨텍스트 메뉴를 추가해 봅시다~
i) 컨텍스트 메뉴용 리소스 준비
Android > app > res > menu 마우스 우클릭 > Menu resource file (File name: context) --> context.xml이 생성됩니다.
ii) 메뉴 구성하기
팔레트 창에서 'Menu Item'뷰 2개를 끌어다 컴포넌트트리 창 menu 하위에 배치 (title: 검색사이트, 기본 브라우저에서 열기)
iii) 코딩 (컨텍스트 메뉴를 위한)
-- MainActivity.kt
// *1, *2 순으로 작업
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
registerForContextMenu(webView) // *2
}
override fun onCreateContextMenu( // *1
menu: ContextMenu?,
v: View?,
menuInfo: ContextMenu.ContextMenuInfo?
) {
super.onCreateContextMenu(menu, v, menuInfo)
menuInflater.inflate(R.menu.context, menu) // MainActivity의 컨텍스트 메뉴로 등록
}
// 컨텍스트 메뉴 클릭 이벤트 처리
override fun onContextItemSelected(item: MenuItem): Boolean {
when (item?.itemId) {
R.id.action_share -> {
// 페이지 공유 코드 (*k 마지막에 별도 정리)
return true
}
R.id.action_browser -> {
// 기본 웹 브라우저에서 열기 코드 (*k 마지막에 별도 정리)
return true
}
}
return super.onContextItemSelected(item)
}
}
※ 암시적 인텐트
미리 정의된 인텐트들을 말합니다.
사용 예들) 출처: https://developer.android.com/guide/components/intents-common 에서 더 많은 예를 볼 수 있어요.
그런데, Anko 라이브러리를 사용하면 아래의 사용 예들 보다 훨씬 간편하게 코딩할 수 있죠 (거의 한 줄로 처리 가능!)
(일단 암시적 인텐트 사용 예들을 볼까요)
// 전화 걸기
val intent = Intent(Intent.ACTION_DIAL) // Intent 클래스에 정의된 액션 중 전화거는 액션을 선택한 것임
intent.data = Uri.parse("tel:02-1234-5678") // "tel:"로 시작하는 Uri는 전화번호를 나타내는 국제표준임
if (intent.resolveActivity(packageManager) != null) { // intent.resolveActivity()는 인텐트를 수행하는 액티비티가 있는지를 검사
// 전화 앱이 없는 태블릿 같은 기기에서는 null값을 반환함
startActivity(intent)
}
// 문자열 보내기
val intent = Intent(Intent.ACTION_SEND)
intent.apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "보낼 문자열")
var chooser = Intent.createChooser(intent, null)
if (intent.resolveActivity(packageManager) != null) {
startActivity(chooser)
}
}
// 웹 브라우저 띄우기
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("http://www.google.com")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
...
...
(이번에는 Anko 라이브러리를 사용한 예 입니다)
Anko라이브러리를 사용한 예)
makeCall(전화번호) // 전화 걸기
sendSms(전화번호, [문자열]) // 문자 보내기
browse(url) // 웹 브라우저에서 열기
share(문자열, [제목]) // 문자열 공유
email(받는메일주소, [제목], [내용]) // 이메일 보내기
와! 놀랍도록 간단하군요!!
※ 상기 본 프로그램 코드에서 생략했던 코드들을 아래에 정리 했습니다. 채워 넣으세요!
주의) 특별한 경우가 아니라면 전화걸기에 암시적 인텐트를 사용하지 않을 것을 권장합니다.
별도의 권한을 필요로 하고 전화번호 입력까지만 제공하면 사용자 의지대로 전화를 걸면 되기 때문이에요.
// 메시지 발송 코드
sendSMS("02-1234-5678", webView.url + "에 들어가봐!")
// 이메일 발송 코드
email("test@daum.net", "이 쇼핑몰이 젤 좋아!", webView.url)
// 페이지 공유 코드
share(webView.url)
// 기본 웹 브라우저에서 열기 코드
browse(webView.url)
실행결과) '메뉴>검색사이트>다음'을 눌러본 상태입니다.
잘 동작하는 군요! 수고 하셨습니다~!
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (9/16) - 앱 만들기 2 (스톱워치 StopWatch)
스톱워치(초시계)
기능 소개
타이머 시작, 일시정지, 초기화, 랩타임 표시등의 기능을 갖는 초시계를 제작해 봅니다.
핵심이 되는 주제
timer
백그라운드 스레드
runOnUiThread
ScrollView
FloatingActionButton
실습
1) 빈 액티비티를 생성 (앱 이름 : StopWatch)
벡터 이미지를 사용할 것이므로 vectorDrawables.useSupportLibrary = true 설정
(모듈수준 bundle.gradle의 defaultConfig{} 에 작성하고, 'New Sync' 클릭해 주는 거 있지 않으셨죠?^^ )
2) 레이아웃 작업
① 두 개의 TextView (초, 백분의 1초 시간 표시용)를 배치합니다.
(id : 각각 secTextView, milliTextView)
참고) 뷰의 컨텍스트 메뉴에서 기준라인을 보이게 함으로써 두 뷰의 글자 하단을 정렬할 수 있어요.
② 세 개의 벡터이미지 준비해 주세요.
play arrow(시작), pause(일시정지), refresh(초기화)
③ 2개의 FloatingActionButton(FAB)을 배치해야 하는데요.
- FAB는 벡터 이미지로 깔끔한 버튼을 만들기에 적합한 이미지합성 버튼입니다.
- 구글 머티리얼 디자인에 자주 사용됩니다.
- 컴포넌트 팔레트 창에서 처음 이 컴포넌트를 추가하려고 하면 네트워크 상에서 다운로드 받도록 되어 있습니다.
(다운로드 받으세요)
- 다운로드가 끝났으면 FAB를 드래그앤드롭으로 가져다 놓고,
벡터 이미지(시작, 초기화 이미지)를 선택하고 id와 색상을 적당히 설정하세요.
총 2개의 버튼을 만들면 됩니다.
참고) 시작 버튼 id: playFab, 초기화 버튼 id: resetFab
④ 1개의 Button (랩 타임용)을 배치하세요 - id: labButton
⑤ 중앙에 ScrollView (랩 타임 표시용)를 배치해 주세요.
수직으로 차곡차곡 쌓이는 레이아웃을 LinearLayout이라고 합니다.
(컴포넌트 트리창을 보면 ScrollView 하위에 LinearLayout이 들어있을 거예요.
ScrollView 뷰는 자식 뷰룰 하나만 갖는 특수한 뷰이고,
LinearLayout 뷰는 여러 개의 자식 뷰를 갖습니다)
LinearLayout 뷰 안에 동적으로 타임랩 값들을 새로운 뷰 형태로 쌓이도록 만들것입니다.
컴포넌트 트리 창에서 LinearLayout 뷰를 선택하고 id를 입력해 주세요. ( 예: lapLayout )
3) 코딩
※ timer (타이머)
안드로이드의 두 가지 스레드 모드가 있습니다.
① 메인스레드 (일반적인 UI 가능)
② 워커스레드 (작업 시간이 오래 걸리고 화면에 표시되지 않음. 당연히 UI 조작을 불가)
타이머 코딩 형식
timer (period = 1000) { // 1000 = 1초
// 워커스레드 (UI조작 불가)
runOnUiThread {
// 메인스레드 (UI조작 가능)
}
}
- MainActivity.kt
package com.tistory.stopwatch
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import kotlin.concurrent.timer
class MainActivity : AppCompatActivity() {
private var time = 0
private var timerTask: Timer? = null // null을 허용
private var isRunning = false
private var lap = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
playFab.setOnClickListener {
isRunning = !isRunning
if (isRunning) { start() } else { pause() }
}
labButton.setOnClickListener {
recordLapTime()
}
resetFab.setOnClickListener {
reset()
}
}
private fun start() {
playFab.setImageResource(R.drawable.ic_pause_black_24dp) // 일시정지 이미지
timerTask = timer(period=10) { // 타이머 인터벌 10 ms
time++
val sec = time / 100
val milli = time % 100
runOnUiThread { // UI 조작이 가능한 블럭
secTextView.text = "$sec"
milliTextView.text = "$milli"
}
}
}
private fun pause() {
playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp) // 시작 이미지
timerTask?.cancel() // 실행중인 타이머 취소
}
private fun recordLapTime() {
if (!isRunning) return // 타이머가 실행 중이 아니라면 리턴
val lapTime = this.time
val textView = TextView(this) // 동적으로 TextView 생성
textView.text = "$lap LAB : ${lapTime / 100}. ${lapTime % 100}"
lapLayout.addView(textView, 0) // 0 : 맨 위쪽에 추가
lap++
}
private fun reset() {
timerTask?.cancel() // 실행중인 타이머 취소
// 모든 변수 초기화
time = 0
isRunning = false
playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp)
secTextView.text = "0"
milliTextView.text = "0"
// 모든 랩타임 기록 삭제
lapLayout.removeAllViews()
lap = 1
}
}
간단하지만 실행해보면 훌륭하게 동작하는 타이머가 완성됐습니다!
이번 프로그램은 비교적 간단했네요.
다음에는 자신만의 웹 브라우저를 제작해 봅시다~
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (8/16) - 앱 만들기 1 (비만도 계산기)
비만도 계산기
기능 소개
키와 몸무게를 입력하고 결과보기 버튼을 누르면 새 화면에서 비만도를 문자와 그림으로 표시합니다.
부수 기능으로 마지막으로 입력한 키와 몸무게 자료를 저장합니다.
핵심이 되는 주제
입력 및 출력 두 개 화면('액티비티')을 사용합니다.
두 화면 사이에 데이터 전달은 인텐트라는 화면 전환을 사용합니다.
SharedPreference로 자료를 저장합니다.
기타 : Anko 라이브러리 사용 방법 ( 라이브러리 의존성 추가)
벡터 이미지 사용 방법
실습
1) 빈 액티비티를 갖는 프로젝트 생성 (앱 이름 : BMICalculator)
2) 코드 작성을 쉽게 해주는 Anko 라이브러리 추가
참고: Anko 라이브러리의 구성
① Anko Commons (인텐트, 다이얼로그, 로그) <-- 우리가 사용할 모듈
② Anko Layouts (레이아웃)
③ Anko SQLite (SQLite)
④ Anko Coroutines (코루틴)
프로젝트 탐색기 창에서 모듈 수준의 그레이들 파일인 build.gradle 파일을 더블 클릭하여 편집.
(파일명 뒤에 Module 수준과 Project 수준의 구분이 표시되어 있음)
dependencies 항목에 다음의 Anko 라이브러리를 추가.
('라이브러리 의존성 추가'라고 하며 안드로이드 스튜디오에서 자동 다운로드 설치를 하게 됨)
implementation "org.jetbrains.anko:anko:$anko_version"
프로젝트 탐색기 창에서 프로젝트 수준의 그레이들 파일인 build.project 파일을 더블 클릭하여
Anko 라이브러리 버전을 변수로 지정.
buildscript {
ext.kotlin_version = '1.3.50'
ext.anko_version='0.10.5'
repositories {
google()
jcenter()
}
dependencies {
....
}
}
추가한 그레이들 파일들을 적용하기 위해 에디트창 탭 이름 바로 아래에 'Sync Now'를 클릭해서
프로젝트를 재 빌드.
3) 레이아웃 작업 1 - 키, 몸무게 입력 화면
① 'Plain Text' 뷰 배치
ID : heightEditText
input Type : (숫자만 입력할 수 있도록) number
hint : (입력 전에 표시할 문자열) 키(cm) --> 이 문자열이 보이도록 text 란의 Name을 삭제
② Plain Text 뷰도 되지만, 숫자만 입력할 거니까 이번에는 'Number' 뷰를 배치
ID : weightEditText
③ 'Button' 뷰 배치
ID : resultButton
4) 레이아웃 작업 2 - 결과 화면
에디터 창을 디자인이 아닌 텍스트 모드로 전환한 후, 결과를 출력할 새로운 액티비티 추가.
File > New > Activity > Empty Activity
(Activity Name : ResultActivity)
① activity_result.xml 창에 TextView 뷰 배치
ID : resultTextView
textAppearance : appCompat.Large
② 이미지 뷰 배치
참고) 안드로이드 스튜디오에서 제공하는 이미지 파일들
비트맵 이미지: PNG, JPG
벡터 이미지 : SVG, EPS
벡터 이미지를 사용해 보자. (에셋 스튜디오에서 생성할 수 있음)
프로젝트 창 > res 폴더 우클릭 > New > Vector Asset에서 벡터 이미지 생성
Clip Art: 에서 원하는 이미지 선택
같은 방법으로 비만도 정상, 비만, 저체중 세 가지 이미지를 추가.
activity_result.xml 화면에서 ImageView 뷰를 배치
project 항목에서 '정상'용 이미지를 선택하고 OK (예: 스마일 아이콘)
ImageView 속성 : 사이즈 조정 (예: 가로(layout_width), 세로(layout_height : 각각 100 dp),
색상(tint) 조정
※ 벡터 이미지는 vectorDrawable 리소스로 분류하는데,
백터드로어블은 Android 5.0부터 동작함.
우리는 4.4버전으로 생성했으므로 모듈 수준의 build.gradle 파일에
다음을 추가해 이를 지원케 해야 함. (Sync Now 잊지말고!)
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
5) 뒤로가기 기능 추가
결과 화면에서 다시 입력화면으로 돌아가기 위한, '뒤로가기' 기능을 상단에 추가해 보죠.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tistory.bmicalcurator">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ResultActivity" android:parentActivityName=".MainActivity"></activity>
</application>
</manifest>
앱을 실행해 보면, 결과화면 상단에 뒤로가기 링크가 표시되고, 터치하면 입력 화면으로 되돌아 갑니다.
6) 코딩
간단한 데이터와 함께 또 다른 화면(액티비티)를 구동해 주는 인텐트를 이용합니다.
<< 자료 입력 화면 MainActivity >>
-- MainActivity.kt (코틀린 일반 코딩)
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.content.Intent // 자동으로 임포트됨
import kotlinx.android.synthetic.main.activity_main.* // 레이아웃 정보가 자동으로 임포트되어 있음
// 이 덕택에 앞에서 추가했던 텍스트뷰나 버튼 사용이 가능한 것임
// 자동 임포트는 'kotlin-android-extensions' 플러그인에 의한 기능임
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadData()
resultButton.setOnClickListener {
saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())
val intt = Intent(this, ResultActivity::class.java)
startActivity(intt)
}
}
// SharedPreference로 자료 저장
private fun saveData(h: Int, w: Int) {
val pref = PreferenceManager.getDefaultSharedPreferences(this) // 프리퍼런스 객체 생성
val editor = pref.edit() // 에디터 객체 얻어오기
// 에디터 객체는 프리퍼런스 객체에 데이터를 넣어주는 역할을 함
editor.putInt("HEIGHT", h) // 키-값 쌍으로 자료 저장
.putInt("WEIGHT", w)
.apply() // 적용
}
// 자료 읽어 오기
private fun loadData() {
val pref = PreferenceManager.getDefaultSharedPreferences(this) // 프리퍼런스 객체 생성
val h = pref.getInt("HEIGHT", 0) // 0은 값이 없을 때의 디폴트 설정 값
val w = pref.getInt("WEIGHT", 0)
if (h !=0 && w!=0) {
heightEditText.setText(h.toString())
weightEditText.setText(w.toString())
}
}
}
-- MainActivity.kt (Anko 코딩)
우리는 Anko 라이브러리를 추가해서 빌드하고 있는데, Anko 라이브러리를 쓸 경우 위 버튼 리스너
를 다음과 같이 간결하게 적어도 됩니다. (단, 이 때 anko 라이브러리를 임포트해 줘야 함)
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.startActivity // 추가할 것
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
resultButton.setOnClickListener {
saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())
// Anko 코딩된 부분
startActivity<ResultActivity>(
"weight" to weightEditText.text.toString(),
"height" to heightEditText.text.toString()
)
}
}
...
}
<< 결과 화면 ResultActivity >>
-- ResultActivity.kt 의 onCreate()
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_result.*
class ResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_result)
val h = intent.getStringExtra("height").toInt() // 인텐트에서 자료 빼오기
val w = intent.getStringExtra("weight").toInt()
val bmi = w / Math.pow(h / 100.0, 2.0)
when {
bmi >= 35 -> resultTextView.text = "고도 비만"
bmi >= 30 -> resultTextView.text = "2단계 비만"
bmi >= 25 -> resultTextView.text = "1단계 비만"
bmi >= 23 -> resultTextView.text = "과체중"
bmi >= 18.5 -> resultTextView.text = "정상"
else -> resultTextView.text = "저체중"
}
when {
bmi >= 23 -> imageView.setImageResource(R.drawable.ic_sentiment_very_dissatisfied_black_24dp)
bmi >= 18.5 -> imageView.setImageResource(R.drawable.ic_sentiment_satisfied_black_24dp)
else -> imageView.setImageResource(R.drawable.ic_sentiment_dissatisfied_black_24dp)
}
Toast.makeText(this, "BMI : $bmi", Toast.LENGTH_SHORT).show() // Toast 메시지로 BMI 값 출력
// Anko로는 toast("BMI : $bmi")
}
실행 결과
Anko 라이브러리를 추가하며 실습해 봤던, 깃허브를 이용한 '라이브러리 의존성 추가'는 참 편리하고 효과적인 것 같습니다.
이 번 실습을 두 세 차례 차근 차근 따라해 보면, 흐름이 파악되실 거예요. 이렇게해서 첫 번째 안드로이드 앱을 만들어 봤습니다. 수고 하셨습니다~
'프로그래밍' 카테고리의 다른 글
[코틀린의 고급 문법 2]
고급 문법 마지막 시간입니다, 힘내자구요~~ 화이팅!
람다식
람다식이란 함수를 간결하게 표시한 형태입니다. 익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리합니다.
코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있습니다.
아래 세 가지 표현은 모두 같은 의미입니다. (코딩 간결화의 변천 과정이라고 생각해도 됨)
① 일반 함수
fun add(x: Int, y: Int) : Int { return x + y }
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y
③ 람다식
var add = {x: Int, y: Int -> x + y } // { } 부분이 람다식임. '{ 인수들 -> 함수본문 }' 형태임
사용 예)
println(add(3+4)) // 결과: 7
SAM 변환
JAVA에서 메소드가 하나뿐인 인터페이스는 인터페이스를 구현하는 대신 함수로 구현할 수 있는데,
이것을 'SAM변환'이라고 합니다. SAM (Simple Abstract Method ; 간단한 추상메소드)
코틀린 함수는 매개변수로 한 개의 추상메소드를 받는 형태를 취할 수 있는데,
이 때는 자바에서 작성된 인터페이스인 경우에 한해,
자바의 SAM변환과 유사하게 함수로 구현된 매개변수를 전달할 수 있습니다.
예를 들어 버튼의 클릭 이벤트를 구현할 때, View.OnClickListener 인터페이스를 구현하는데,
(이 View.OnClickListener는 onClick() 이라는 추상메소드 한 개만을 가지고 있음)
이 인터페이스를 함수로, 더 나아가 익명메소드 형태로 구현해서 전달해 봅시다.
button.setOnClickListener(object : View.OnClickListener { //OnClickListener는 OnClick메소드 하나만 포함
override fun onClick(v: View?) { // 익명메소드 형태
// 클릭시 처리 코드
}
}
)
그리고 위 익명메소드는 람다식으로 표현할 수 있으므로,
button.setOnClickListener({ v: View? -> // OnClickListener가 람다식화된 형태
// 클릭시 처리 코드(람다 블럭)
})
코틀린에서 메소드 호출 시 제일 뒤의 매개변수가 람다식인 경우에는,
가독성을 위해 람다식을 ( ) 밖으로 빼서 작성할 수 있습니다.
button.setOnClickListener( ) { v: View? ->
// 클릭시 처리 코드(람다 블럭)
}
그리고 람다식이 어떤 메소드의 유일한 매개변수인 경우에는 메소드의 ( ) 를 생략할 수 있습니다. 게다가 컴파일러가 자료형을 추론할 수 있다면 자료형을 생략할 수 있는 것이므로, 최종적으로 다음과 같이 되겠네요.
button.setOnClickListener{ v ->
// 클릭시 처리 코드(람다 블럭)
}
※ 만약에 '클릭시 처리 코드'에서 v라는 매개변수를 사용하지 않는다면,
v를 _ 기호로 대치할 수도 있습니다.
button.setOnClickListener{ _ ->
// 클릭시 처리 코드(람다 블럭)
}
※ 또 만약에 그리고 람다식에서 매개변수가 하나 뿐인 경우라면,
매개변수를 아예 생략할 수도 있습니다.
button.setOnClickListener{
// 클릭시 처리코드(람다 블럭)
}
그리고 이 때 람다블럭내에서는 매개변수를 it로 접근할 수 있습니다.
예를들면,
button.setOnClickListener{
it.visiblity = view.GONE // 여기서 it는 View? 형 v를 의미함 ----- (K)
}
위에서 SAM변환을 이용한 익명메소드에서 람다식화, 그리고 코틀린의 편리한 생략 표현 기법들을 총동원해 코드를
간결화시켜봤고 코드들 모두 같은 의미입니다. 어쨌거나 가장 가독성이 뛰어난 형태는 (K)의 코드죠. 앞으로 종종 등장합니다.
주의해야 할 점은 코틀린에서의 SAM변환은 자바에서 작성한 인터페이스일 때에만 동작한다는 것~!.
확장 함수
코틀린에서는 .연산자를 이용해 이미 정의된 클래스에 메소드를 쉽게 추가할 수 있습니다.
(보통 Java나 C#에서는 final로 상속이 봉인되어 있어서 메소드를 추가하지 못하는 경우가 많은데 편리한 기능인 것 같네요)
(확장 함수 내부에서 해당 객체로의 접근은 this를 사용)
예) Int 클래스에 isEven()을 추가해 보자.
fun Int.isEven( ) = this % 2 == 0 // 'this % 2 == 0'의 결과인 부울 값이 함수의 결과 값이 됨
println(5.isEven()) // 결과: false
형 변환
val a = 10L
b = a.toInt()
c = a.toDouble()
d = a.toString()
e = Integer.parseInt(d) // 문자열 형을 숫자로 변환
※ 일반 클래스 간 형 변환 (as 키워드)
open class Human()
class Man: Human()
val man = Man()
val human = man as Human // Human 형으로 형 변환
형 체크
val st = "hello"
if (st is String) { println(st.toUpperCase() }
고차 함수
함수를 매개 변수로 전달하거나 함수 형으로 반환할 수 있습니다. 이렇게 사용되는 함수를 '고차 함수'하고 합니다.
fun add( x: Int, y: Int, callback: (sum: Int) -> Unit) { // 두 개의 Int 매개변수와 한 개의 익명함수 매개변수
// 매개 변수로 쓰인 익명함수는 한 개의 Int 매개변수를 받고 리턴 값은 없음
callback( x + y )
}
add(3,4, {println(it)}) // 결과: 7 함수를 { }로 감싸고, 이 함수 내부에서는 반환 값을 it로 접근한 예임
동반 객체
'팩토리 메소드'
코틀린에서는 클래스를 객체화 하는 것과는 별개로 메소드를 이용해 객체를 생성하는 코딩 패턴을 지원하는데, 이를 '팩토리 메소드'라고 합니다.
(나중에 다루게 될 프래그먼트 컴포넌트는 특수한 제약 때문에 팩토리 메소드로만 객체를 생성할 수 있음)
코틀린에서는 타 언어에서 정적인 메소드를 만들 때 사용하는 static 키워드 같은 게 없습니다. 그 대신 '동반 객체 (companion object)'라는 것을 통해 이를 구현합니다. (companio : 동반자, 동료, 친구)
다음은 newInstance() 정적 메소드를 사용해서 Fragment 객체를 생성하는 팩토리 패턴을 구현한 것입니다.
여기서 동반 객체 내부의 메소드는 Fragment 클래스와 아무 관계가 없는 정적인 존재입니다.
class Fragment {
companion object {
fun newInstance(): Fragment { // shs: 함수의 반환 형이 Fragment 형이라...
println("생성됨")
}
}
}
val fragment = Fragment.newInstance() // shs: 이거 정적 Fragment 안의 newInstance 멤버메소드를 액세스 하는 것과 비슷한데요...
<<< 코틀린 기본 라이브러리에서 유용한 함수들 >>>
let()
블럭에 자기 자신을 인수로 전달(it로 참조)하고 실행 결과를 반환합니다.
'안전한 호출' 즉, str이 null이 아닐때만 호출 되도록 ?.연산자를 이용하면 더 좋습니다.
val result = str?.let { // 결과는 Int형
Integer.parseInt(it)
}
// fun <T, R> T.let(block: (T) -> R): R
with()
객체를 매개변수로 받고 블럭에 리시버 객체형으로 전달(this로 참조)해 준 후, 실행 결과를 반환합니다.
단, '안전한 호출'이 불가능하기 때문에 반드시 str이 null아닐 때에만 호출해야 합니다.
with(str) {
println(toUpperCase()) // this.toUpperCase()에서 this.를 생략할 수도 있다.
}
// fun <T, R> with(receiver: T, block T.() -> R): R
apply()
블럭에 객체 자신이 리시버 객체형으로 전달되고 그 객체형으로 반환됩니다.
(주로 객체의 상태를 변경해서 반환할 때 사용함)
val result = car?.apply {
car.setColor(Color.BLACK)
car.setPrice(1000000)
}
// fun T.apply(block: T.() -> Unit): T
run()
run() 함수는 익명 함수처럼 쓰는 법과 객체에서 호출하는 법 모두를 제공합니다.
i) 익명 함수처럼 쓸 때
블럭의 결과를 반환합니다.
블럭 안에 선언된 변수들은 모두 임시로 잠깐 사용하는 변수들인데, 임시 변수를 많이 사용하는 복잡한 계산에 유용하겠네요.
val avg = run{
val kor = 90
val eng = 80
val math = 90
(kor + eng + math) / 3.0f
}
// fun run(block: () -> R): R
ii) 객체에서 호출 할 때
객체를 블럭의 리시버 객체로 전달하고 결과를 반환합니다.
안전한 호출이 가능하므로 with()보다 더 유용합니다~
str?.run {
println(toUpperCase())
}
// fun <T, R> T.run(block: T.() -> R): R
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (6/16) - 코틀린 고급 문법 1/2
[코틀린의 고급 문법 1]
특별한 상태 값 null
코틀린에서는, 모든 객체는 생성과 동시에 값을 갖도록 초기화 하는 것을 원칙으로 하며
null 사용을 허용하지 않습니다.
즉,
val a: String // 불허
val a: String = null // 불허
하지만, null을 꼭 사용하겠다면, ?를 사용해서 null을 허용하겠다고 명백히 해줘야 합니다.
val a: String? = null
주의) 위 변수 a 사용시 고려할 점
val b: String? = a // 허용 (a나 b나 동류이므로)
val c: String = a // Error (a는 부정확한 상태므로 불허)
val c: String = a!! // a값이 null이 아님을 보증한다는 의미로 !!를 붙여쓰면 허용
늦은 초기화
가끔 초기화를 일부러 늦춰야 하는 경우에 사용합니다. 코틀린에서는 두 개의 키워드로 이를 지원합니다.
(앱이 시작될 때 일부 변수들을 늦게 초기화함으로써 연산을 분산시켜 실행 속도를 빠르게 하기도 함)
lateinit
var 타입의 늦은 초기화 (int, long, float, double과 같은 프리미티브 자료형에는 사용 불가)
lateinit var a : String // 허용
by lazy
val 타입의 늦은 초기화 (프리미티브 자료형에도 사용 가능)
val a : String by lazy {
"Hello"
}
실습)
val str : String by lazy {
println("늦은 초기화!")
"Hello"
}
println(str) // 늦은 초기화!, Hello (str 첫 사용시에만 '늦은 초기화!'가 출력됨)
// (즉, lazy { } 블럭 내부 코드가 한 번만 실행됨)
println(str) // Hello
안전한 호출
.연산자 대신 ?. 연산자를 사용하면 변수 값이 null이 아닌 경우에만 메소드가 호출됩니다 (편리한 기능이네요!)
var nameUpperCase = if (str != null) str else null // 이런 문장을
var nameUpperCase = str?.toUpperCase // 로 간략하게 쓸 수 있다.
또 한 가지 편리한 기능이 있는데, 위에서 str이 null 일 때 nameUpperCase도 null 이 됩니다.
그런데 이 때 엘비스 연산자 ?: 를 사용하면 null 을 대체할 문자열을 대입할 수 있습니다.
var nameUpperCase = str?.toUpperCase ?: "초기화가 안됐어요" // str이 null인 경우 문자열이 대입됨
컬렉션
데이터 집합을 만드는 자료 구조를 말합니다. (예: 리스트, 맵, ...)
원소들의 내용을 수정할 수 없는 타입과 수정할 수 있는 가변형(mutable) 타입이 있습니다.
<리스트>
원소 변경 불가
val colors1: List = listOf("초록", "주황", "빨강", "파랑", "하양")
val colors1 = listOf("초록", "주황", "빨강", "파랑", "하양") // 물론 데이터 형을 생략할 수도 있습니다
원소 변경 가능
val colors2: MutableList = mustableListOf("초록", "주황", "빨강", "파랑", "하양")
val colors2 = mustableListOf("초록", "주황", "빨강", "파랑", "하양")
<colors2.add("검정") // 원소 추가
colors2.removeAt(4) // 원소 제거
color2[0] = "연두" // 원소 변경
<맵>
키-값 쌍의 데이터 집합 (중복불가)
원소 변경 불가
val colors3: Map<String, Int> = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
println(colors3["B"])
원소 키 값 변경 가능
val colors3: MutableMap<String, Int> = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
사용 예)
for ((k, v) in colors3) {
println("$k --- $v")
}
<집합>
원소 변경 불가
val cities: Set = setOf("Seoul", "Incheon", "CheongJu")
val cities = setOf("Seoul", "Incheon", "CheongJu")
원소 변경 가능
val cities: MutableSet = mutableSetOf("Seoul", "Incheon", "CheongJu")
val cities = mutableSetOf("Seoul", "Incheon", "CheongJu")
사용 예)
cities.add("Sokcho")
cities.remove("Incheon")
(계속...)
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (5/16) - 코틀린 기본 문법 2/2
[코틀린의 기본 문법 2]
class (클래스)
class Person { } // 빈 클래스 형태
class Person { // 생성자를 정의한 클래스 (*k1)
constructor(name: String) {
println(name)
}
}
희한하게 클래스를 아래 처럼도 정의하는군요...
class Person (var name: String) { } // (생성자를 갖는) 빈 클래스 (참고: var 생략 가능)
// 굳이 클래스명 옆에 이런 문법을 만들것 까지야...
// 메소드 오버로드는 없나보군요...
class Person (name: String) {
init { // init 블럭은 생성자와 함께 제일 먼저 실행됨 (위 *k1과 동일 결과)
println(name)
}
}
var person1 = Person("원빈") // 개체 생성
person1.name = "한효주" // 프로퍼티 쓰기
println(person1.name) // 프로퍼티 읽기
접근 제한자
public : (생략가능) 전체 공개
private : 현재 파일 내에서만 공개
internal : 같은 모듈 내에서만 공개 (모듈? 예를 들면, 한 프로젝트 내에 스마트폰용 모듈, 시계용 모듈, TV용 모듈,
태블릿용 모듈 등등 여러 모듈들을 제작하는 경우가 있음. 모듈은 여러 개의 파일 조각으로 이뤄져 있음)
protected : 부모로 부터 상속받은 클래스에서만 공개
클래스 상속
※ 코틀린에서는 기본적으로 상속을 금지합니다. (추상 클래스 제외)
그러나 꼭 사용하고자 한다면 open 키워드를 사용할 수 있습니다.
빈 클래스 상속
open class Animal { }
class Dog : Animal( ) { } // SHS: 반드시 상위 클래스의 생성자를 호출하는 형식임
생성자를 갖는 클래스의 상속
open class Animal(val name: String) { }
class Dog(name: String) : Animal(name) { }
중첩 클래스
안쪽 클래스는 바깥 클래스A와 거의 독립적입니다.
안쪽과 바깥쪽 변수명이 같아도 됩니다. 단지 선언된 위치가 클래스A 안쪽일 뿐입니다.
class A {
class B { // 중첩 클래스
}
}
var bb = A.B( ) // 클래스B의 위치를 명시하고 클래스B를 객체화 할 수 있음.
(중첩된) 내부 클래스
안쪽 클래스에 inner 키워드를 붙여주면 바깥 클래스 멤버에 접근할 수 있고,
이 때의 내부 클래스B는 클래스A의 멤버로 소속이 바뀝니다.
class A {
var a=10
inner class B { // (중첩된) 내부 클래스 - 멤버가 된 클래스
fun result( ) {
a = 20 // 바깥 클래스 멤버에 접근 가능
}
}
}
var bb = A( ).B( ) // 클래스B는 클래스A의 멤버이므로 객체화된 바깥 클래스를 통해서만 객체화가 가능
추상 클래스
미구현 메소드가 포함된 클래스를 의미하죠. 키워드: abstract
클래스명과 미구현 메소드명 앞 모두에 키워드를 작성해야 합니다.
미구현 메소드가 단 한 개라도 포함되어 있다면 추상 클래스 입니다.
추상 클래스는 반드시 상속을 목적으로 제작하는 클래스입니다.
반드시 상속한 후, 미구현 메소드를 구현해야 합니다.
이것은 반드시 지켜야 하는 개발 팀원들 간의 약속입니다.
abstract class A {
abstract fun func1( ) { }
fun func2( ) { }
}
class B: A( ) {
override fun func3( ) {
println("Hello")
}
}
인터페이스
추상 클래스와 거의 같지만, 일단 2가지 큰 차이점이 있습니다.
1. 추상 클래스는 하나만 상속 가능
인터페이스는 복수 개를 상속 받을 수 있음 ('다중 상속')
2. 추상 클래스 내부에는 추상 메소드만 선언 가능 (abstract 키워드 필수)
인터페이스 내부에서는 추상 메소드 + 일반 메소드 모두 사용가능 (abstract 키워드 불필요)
interface Runable {
fun run( ) // { } 코드가 없는 추상 메소드
fun walk( ) = println("걷는다") // 일반 메소드 포함 가능
}
class Human : Runable {
override fun run( ) { println("달린다") }
}
※ 상속과 인터페이스를 함께 사용해본 예)
class Animal { ... }
interface Runable { ... }
interface Eatable { ... }
class Dog : Animal( ), Runable( ), Eatable( ) { ... } // 클래스 상속과 인터페이스 다중 상속
(계속...)