24년 4월 기준으로 작성되었습니다.
1. 팀 프로젝트를 맛보자
스토리보드 : 앱 기획서, 화면 설계서 등으로 불리기도 한다. 말 그대로 앱 개발을 위한 설계도라고 할 수 있다.
기본적으로 화면의 구성과 기능에 대한 설명을 담고 있고, 정책이나 오류 메시지 등 개발에 필요한 다양한 정보를 포함하기도 한다.
2. 안드로이드 개발을 준비하자
구글이 제공하는 웹 · 안드로이드 · iOS 용 아이콘을 받을 수 있다.
https://fonts.google.com/icons
※ 안드로이드 스튜디오에서는 File > New > Vector Asset 에서 프로젝트에 바로 추가할 수 있다.
mipmap도 이미지를 담을 수 있지만, 오직 앱 아이콘을 위한 폴더이다.
mipmap은 drawable과 달리, 실제 화면에 보이는 크기를 고려해서 결정된다.
EX) 런처에서 한 줄에 5개의 아이콘이 표시될 때와 3개의 큰 아이콘이 표시될 때,
화면 해상도가 아닌 실제 보이는 크기에 맞는 이미지를 선택해 사용한다.
적응형 아이콘 : 안드로이드 8(API 26) 부터 통일성을 위해,
아이콘의 전경(foreground)와 배경(background)를 분리해서 아이콘을 구성하도록 지정했다.
예제 코드에서는 mipmap/ic_launcher.xml 에 드러나 있다.
※ Material 컴포넌트를 사용하려면, build.gradle에 의존성을 추가해야 한다.
EX) implementation("com.google.android.material:material:1.6.1")
2-1. BottomNavigationView 의 속성
1) app:backgroundTint : 내비게이션의 색을 설정한다.
2) app:itemIconSize : 아이콘의 크기를 설정한다.
3) app:itemIconTint : 아이콘의 색을 설정한다.
4) app:itemTextColor : 텍스트의 색을 설정한다.
5) app:labelVisibilityMode : label의 표시 방법을 결정한다.
- auto : 기본값, 아이템 개수가 3개 이하일 때는, 모든 label을 표시
- labeled : 모든 아이템의 label을 표시
- selected : 선택된 아이템의 label 만 표시
- unlabeled : 모든 label을 표시하지 않는다.
그러나 직접 변경하는 것보다는, 테마를 통해 변경하여 다른 컴포넌트들과 스타일을 통일하는 것이 좋다.
3. 서버와 함께 Hello, World!
3-1. HTTP 메서드
GET, POST, PUT, DELETE 와 같은 HTTP 메서드를 설명할 때 "멱등성" 과 "안전성" 이라는 키워드는 자주 등장한다.
멱등성 (Idempotent) : 연산을 여러 번 적용하더라도, 결과값이 달라지지 않는 성질
EX) 특정 리소스에 대한 DELETE 요청을 한 번 한 것과 열 번 한 것은 서버 입장에서는 같다. -> 멱등성이 성립한다.
안전성 (Safe) : 리소스의 변경 여부로 판단한다.
EX) GET 요청은 열 번을 해도 서버의 리소스가 변경되지 않는다,
그러나 DELETE는 한 번만 호출해도 리소스가 변경(삭제)되므로 안전한 메서드가 아닌 것이다.
3-2. 자주 사용하는 HTTP 헤더
HTTP 헤더는 요청 헤더, 응답 헤더, 일반 헤더, Entity 헤더로 나뉜다.
1) 요청 헤더
Referer : 요청을 보낸 페이지의 주소이다.
A 페이지에서 링크를 클릭해서 B 페이지로 이동했다면, B 페이지를 요청할 때 A 페이지가 Referer 헤더로 전달된다.
If-Modified-Since : 서버의 리소스가 특정 시간 이후로 변경된 경우에만, Entity를 보내달라고 요청한다.
Entity 가 생략됐을 때, 클라이언트의 캐시를 사용한다.
If-None-Match : 서버 리소스의 ETag 가 다른 경우에 요청을 처리한다. 요청에 포함되는 ETag와 서버의 ETag가 다른 경우에 PUT 요청은 리소스를 변경하고, GET 요청은 Entity를 포함한 응답을 내려준다.
2) 응답 헤더
Etag : 리소스의 특정 버전을 식별할 수 있게 하는 Entity 태그이다.
클라이언트는 응답으로 받은 ETag 값을 조건부 요청 헤더로 전달해 사용한다.
Last-Modified : 리소스의 마지막 수정 시각이다. Etag와 마찬가지로 조건부 요청 헤더로 전달해 사용한다.
3) 일반 헤더
Cache-control : 서버, 클라이언트, 프록시 사이의 캐시 정책을 제어한다.
전송된 리소스가 유효하다고 판단되는 시간을 "max-age" 지시자로 전달해 캐시하도록 할 수 있다.
public, private 지시자는 응답이 공유될 수 있는 지 여부를 나타낸다.
EX) 구글의 로고 이미지를 요청했다면, public 이겠지만 <=> 내 구글 드라이브 파일 목록을 요청했다면 private 일 것이다.
3-2. HTTP 상태 코드
200 OK : 요청 성공
201 Created : 요청에 성공해서, 새로운 리소스가 생성됨
202 Accepted : 요청이 접수되었으나, 처리가 완료되지 않았음. 배치(Batch) 처리 같은 곳에서 사용한다.
EX) 요청 접수 후, 1시간 뒤에 배치 프로세스가 요청을 처리함
204 No Content : 서버가 요청을 성공적으로 수행했지만, 응답 페이로드 본문에 보낼 데이터는 없음
EX) 웹 문서 편집기에서 Save 버튼 -> save 버튼을 눌러도 같은 화면을 유지해야 한다.
그 대신 결과 내용이 없어도 204 메시지(2xx) 만으로도 성공을 인식할 수 있다.
리다이렉션(Redirection) 이란?
웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location이 지정한 위치로 자동 이동(리다이렉트)
1) 영구 리다이렉션 : 특정 리소스의 URI가 영구적으로 이동, 원래의 URL을 사용 X, 검색 엔진 등에서도 변경 인지
301 Moved Permanently : 리다이렉트시 요청 메서드가 GET으로 변하고, 요청 본문이 제거될 수 있다.
308 Permanent Redirect : 301과 기능은 같지만, 리다이렉트시 요청 메서드와 요청 본문은 유지된다.
2) 일시적인 리다이렉션 : 리소스의 URI가 일시적으로 변경, 따라서 검색 엔진 등에서 URL을 변경하면 안됨
302 Found & 303 See other : 리다이렉트시 요청 메서드가 GET으로 변하고, 요청 본문이 제거될 수 있다.
307 Temporary Redirect : 302와 기능은 같지만, 리다이렉트시 요청 메서드와 요청 본문은 유지된다.
PRG : Post / Redirect / Get
EX) POST로 주문 후에 웹 브라우저를 새로고침하면, 중복 주문이 되지 않도록 GET 메서드로 리다이렉트하는 것이다.
현실적으로 이미 많은 애플리케이션 라이브러리들이 302를 기본값으로 사용하고 있다.
300 Multiple Choices : 쓰지 않음
304 Not Modified : 캐시를 목적으로 사용,
클라이언트에게 리소스가 수정되지 않았음을 알려준다. -> 그러면 클라이언트는 로컬 PC에 저장된 캐시를 재사용한다.
400 Bad Request : 클라이언트가 잘못된 요청을 해서, 서버가 요청을 처리할 수 없음
EX) 요청 파라미터가 잘못되거나, API 스펙에 맞지 않을 때
401 Unauthorized : 클라이언트가 해당 리소스에 대한 인증이 필요함
즉, 인증(Authentication, 본인이 누구인지 확인)되지 않음. 응답에 WWW-Authenticate 헤더와 함께 인증 방법을 설명
※ 인가 (Authorization) : 권한 부여 (ADMIN 권한같이, 특정 리소스에 접근할 수 있는 권한, 인증이 있어야 인가가 있다!)
그래서 401번은 상태 코드 이름과 내용이 달라서 헷갈릴수도 있다 ㅠㅠ
403 Forbidden : 서버가 요청을 이해했지만 승인을 거부함
EX) ADMIN 등급이 아닌 사용자가 로그인은 했지만, ADMIN 등급의 리소스에 접근하는 경우
404 Not Found : 요청하는 리소스가 서버에 없음.
또는 클라이언트가 권한이 부족한 리소스에 접근할 때, 해당 리소스를 숨기고 싶을 때
500 Internal Server Error : 서버 내부 문제로 오류 발생! 애매하면 500 오류를 띄운다.
503 Service Unavailable : 서비스 이용 불가. 서버가 일시적인 과부하 또는 예정된 작업으로 잠시 요청을 처리할 수 없음
Retry-After 헤더 필드로 얼마 뒤에 복구되는지 보낼 수도 있다.
3-3. 3장의 새로운 레이아웃
1) Material 로 시작하는 뷰
MaterialCardView, MaterialTextView, MaterialButton 들은 모두 Material 디자인을 위한 기능들이 추가된 뷰이다.
호환성을 위해서 androidx 패키지의 CardView, AppCompatTextView, AppCompatButton 을 상속받았다.
2) LinearLayout의 showDividers
질문과 답변 영역을 나누는 선을 만들기 위해서, 뷰를 추가하는 방법도 있다.
하지만 속성 중에 showDividers를 사용하면, LinearLayout의 Child 개수에 따라 자동으로 표시된다.
3) TextAppearance
TextView는 글씨 크기, 스타일, 줄 높이 등 여러가지 속성을 갖는다. 이런 속성들을 매번 따로 지정하는 것은 번거롭고 비효율적이기 때문에, 이런 속성들을 묶어 스타일을 만들어 사용한다.
TextView의 경우 모든 뷰에서 사용할 수 있는 style 속성에서는 "위젯의 컬러, 크기" 등을 설정하고,
textAppearance 속성에서는 "텍스트의 크기, 폰트 패밀리" 등을 지정하도록 나눠서 사용할 수 있다.
※ 프래그먼트와 프래그먼트의 뷰는 다른 생애주기를 가지기 때문에, onCreateView에서 생성하고,
onDestroyView에서 직접 해제하는 것이 액티비티에서 뷰 바인딩을 사용할 때와 다른점이다.
3-4. HttpURLConnection으로 API 호출하기
안드로이드에서 REST API와 연동할 때 사용하는 가장 인기있는 라이브러리는 Retrofit 이다.
Retrofit 라이브러리를 사용하면, 비동기 요청, 캐싱, 응답 처리, 인증 등 다양한 기능을 쉽게 구현할 수 있다.
우선은 자바의 기본 패키지인 HttpURLConnection을 사용해보자.
[코드 3-7] TodayFragment.kt
1) 메인 스레드(UI 스레드)에서 API 호출같이 오래 걸리는 IO 작업을 하는 경우, 사용자는 앱이 정지되었다고 느낀다.
이를 피하기 위해 안드로이드의 메인 스레드에서 API를 호출하면 "android.os.NetworkOnMainThreadException" 이 발생한다. 따라서 네트워크 작업은 항상 백그라운드 스레드에서 진행해야 한다!
4) 메인 스레드에서 네트워크 작업을 할 수 없는 것과 마찬가지로, 백그라운드 스레드에서는 UI를 변경할 수 없다.
따라서 "activity.runOnUiThread" 메서드를 이용하여 메인 스레드에서 결과를 카드에 표시했다.
4. Gson 으로 JSON을 다뤄보자
4-1. Gson 사용
Date는 시간을 담기 위한 객체이다. Date를 원하는 형식의 문자열로 만들기 위해 DateFormat을 사용한다.
DateFormat의 getDateInstance() 메서드는 스타일과 로케일을 인자로 받아 그에 맞는 DateFormat 객체를 생성한다.
로케일이 "Locale.KOREA" 일 때, 스타일에 따라 다음의 형식을 반환한다.
FULL : 2024년 4월 1일 수요일
LONG : 2024년 4월 1일 (수)
MEDIUM : 2024.4.1.
SHORT : 24.4.1.
val gson = Gson()
val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.KOREA)
val helloWorld = gson.fromJson(body, HelloWorld::class.java)
4-2. JSON 속성 네이밍 정책
Gson 객체는 생성 후 변경할 수 없으므로, 생성하기 전에 "GsonBuilder" 를 사용해 원하는 설정을 해야한다.
기본적으로 JSON 속성의 이름과 클래스의 속성 이름이 같은 것으로 매핑한다.
그런데 일반적으로 코틀린이나 자바에서는 카멜 케이스(Camel case)를 사용하고, JSON에서는 스네이크 케이스(Snake case)를 사용한다.
GsonBuilder를 생성하고, setFieldNamingPolicy() 메서드로 -> 원하는 네이밍 정책을 전달한다.
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create()
IDENTITIY : 모델과 JSON이 정확히 같은 이름을 사용한다.
LOWER_CASE_WITH_UNDERSCORES : JSON 필드를 소문자로 표기하고, 단어의 연결에 언더바(_)를 사용한다.
UPPER_CAMEL_CASE : JSON 필드의 첫 글자를 대문자로 표기한다. 단어의 연결은 변경하지 않는다.
※ 만약 JSON 속성의 이름과 클래스 멤버의 이름이 일정한 규칙이 없다면,
@SerializedName 어노테이션을 사용해, 명시적으로 지정할 수 있다.
4-3. 커스텀 직렬화와 역직렬화
Gson에서 JSON을 -> 사용자가 직접 만든 "클래스" 나 "열거형(enum)"으로 변환하려면, 변환 작업을 할 어댑터를 GsonBuilder에 등록해야 한다.
직렬화는 JsonSerializer 인터페이스를, 역직렬화는 JsonDeserializer 인터페이스를 구현한다.
null 값 직렬화
객체의 멤버 변수의 값이 null일 때, JSON 문자열로 변환하면 그 필드는 생략된 것을 볼 수 있다.
그런데 특별한 이유로 null인 멤버도 JSON 문자열로 직렬화해야 한다면, GsonBuilder의 serializeNulls() 메서드로 설정을 변경해야 한다.
※ 보기 좋게 출력하기
Gson은 효율성을 위해서, JSON의 개행이나 공백을 생략한다.
사람이 읽기에는 불편하므로, 읽기 좋게 만드는 "setPrettyPrinting()" 메서드를 제공한다.
5. REST API
RESTful (REpresentational State Transfer) 이란?
HTTP의 저자 중 한 명인 로이 필딩의 2000년 논문에 다음의 6가지 제약 조건을 잘 지키는 것이 RESTful 하다고 나온다.
1) 클라이언트-서버 : 클라이언트와 서버를 분리하고, 인터페이스를 이용하여 커뮤니케이션하며, 서로의 구현/작동은 신경 쓰지 않는다.
2) 무상태성 (stateless) : 클라이언트가 서버로 보내는 요청을 이해하기 위해 필요한 정보는 모두 요청에 포함된다.
3) 캐시 (Cache) : 서버의 응답에 캐시 가능 여부가 포함되고, 클라이언트에서 캐시할 수 있다.
4) 계층화된 시스템 (Layered System) : 여러 계층으로 구성될 수 있어야 하고, 각 계층에 속한 컴포넌트는 상호작용하는 계층 너머를 볼 수 없다.
5) 주문형 코드 (Code-On-Demand) : 서버가 클라이언트에 실행 가능한 코드를 보내 기능을 확장할 수 있다.
※ 이 조건은 필수는 아님!
6) 인터페이스 일관성 (Uniform Interface) : REST의 핵심. 아래 4가지 조건을 통해 얻을 수 있다.
6-1) 리소스 식별 (Identification of resources) : 모든 리소스는 고유한 식별자를 가진다.
6-2) 표현을 통한 리소스 조작 : 리소스 자체를 전송하는 것이 아니라, 리소스의 표현을 이용해 조작한다.
6-3) 자기 기술적 메시지 (Self-descriptive messages) : 메시지를 이해하기 위해 필요한 정보가 메시지 안에 전부 있어야 한다.
6-4) 애플리케이션 상태 엔진으로서 하이퍼미디어 : 리소스에서 무엇을 할 수 있는지, 상태 전이를 위한 정보를 제공해야 한다.
이 제약 조건들은 분산 하이퍼미디어 시스템을 위한 것으로, 이 조건들을 만족하도록 구현된 대표적인 예가 HTTP+HTML이다.
RESTful API 와 REST API는 같은 의미로 사용되기도 하고, 다른 의미로 사용되기도 하는 비슷한 용어이다.
※ 결국 REST API 스타일의 핵심은 "URI를 리소스의 식별자로 사용" 하는 것,
"리소스에 대한 행위(CRUD)는 HTTP 메서드를 사용" 해야 한다는 것이다. REST API를 더 많이 사용한다!
5-1. Retrofit 사용법
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
lifecycle-runtime-ktx 는 Activity와 Fragment 의 수명 주기를 따르는 "코루틴 스코프" 를 정의한다.
이 스코프에서 실행된 코루틴은 Lifecycle이 DESTOYED 상태가 되면 자동으로 제거된다.
interface ApiService {
companion object {
fun create(context: Context): ApiService {
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create()
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("http://10.0.2.2:5000")
.build()
.create(ApiService::class.java)
}
}
@GET("/v1/questions/{qid}")
suspend fun getQuestion(@Path("qid") qid: String) : Question
}
@Path : 파라미터가 경로에서 사용된다는 것을 나타낸다. 그 외 자주 쓰는 어노테이션은 다음과 같다.
@Header : 요청의 헤더에 값을 추가한다. EX) @Header("Accept") accept: String
@Query : 쿼리에 값을 추가한다. GET과 DELETE 요청은 URI의 쿼리로 값을 전달한다.
@Field : @FormUrlEncoded와 함께 사용되며 필드로 추가된다.
@Part : @Multipart와 함께 사용되며 파트로 추가된다.
@Body : 요청의 본문으로 사용한다.
suspend 문법 : 코루틴에서 사용하기 위해 중단 함수로 선언했다.
[코드 5-5] TodayFragment.kt
1) 스레드를 코루틴 스코프로 변경했다. 프래그먼트에는, 프래그먼트의 수명 주기에서 사용할 수 있는 lifecycle 스코프와, 프래그먼트 뷰의 수명 주기에서 사용할 수 있는 viewLifecycleOwner.lifecycleScope 가 있다.
lifecycleScope는 메인 스레드에 바인딩되어 있기 때문에, runOnUiThread를 제거하고, 직접 UI를 변경했다.
5-2. 컨버터 팩토리
Retrofit에는 JSON을 변환하는 "GsonConverterFactory" 외에도 다양한 컨버터가 있다.
1) simplexml : XML을 변환한다.
2) protobuf : Protocol Buffer의 바이너리를 변환한다.
3) scalars : string, int 등 기본 자료형을 변환한다.
5-3. HTTP 로그 출력하기
Retrofit에서는 OkHttp를 HTTP 클라이언트로 사용하고 있어서, 코드 몇 줄이면 훌륭한 로깅 기능을 사용할 수 있다.
// 123페이지 추가
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
[ApiService.kt]
companion object {
private var INSTANCE : ApiService? = null
// HTTP 로그 출력하기
private fun okHttpClient() : OkHttpClient {
val builder = OkHttpClient.Builder()
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
return builder
.addInterceptor(logging)
.build()
}
private fun create(context: Context): ApiService {
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(LocalDate::class.java, LocalDateAdapter)
.create()
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.addConverterFactory(LocalDateConverterFactory())
.baseUrl("http://10.0.2.2:5000")
.client(okHttpClient()) // 클라이언트 추가
.build()
.create(ApiService::class.java)
}
HttpLoggingInterceptor는 level에 따라 출력하는 로그 내용이 다르다.
NONE : 로그를 출력하지 않는다.
BASIC : 요청 라인과 응답 라인만 풀력한다.
HEADERS : 요청 라인과 요청 헤더, 응답 라인과 응답 헤더를 출력한다.
BODY : 요청 라인, 요청 헤더, 응답 라인, 응답 헤더, 본문을 출력한다.
5-4. 타임아웃(Timeout) 설정하기
모바일 환경은 다른 환경보다 네트워크가 불안정한 상태가 빈번하다.
이런 상태에서는 앱이 정상적으로 작동하지 않고, 이런 경험은 앱에 대한 사용자의 신뢰를 떨어뜨리는 원인이 된다.
가장 간단한 해결 방법은 시간을 정해두고, 이를 넘으면 앱에 따라 예외를 발생시켜 알맞은 처리를 하는 것이다.
OkHttp에서 타임아웃을 설정하는 예제를 보자.
// HTTP 로그 출력하기
private fun okHttpClient() : OkHttpClient {
val builder = OkHttpClient.Builder()
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
// 로깅 인터셉터 추가 + 타임아웃도 설정한다
return builder
.connectTimeout(3, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.addInterceptor(logging)
.build()
}
Connect Timeout : 정해진 시간 동안 서버와 TCP Handshake 과정을 완료하지 못하면 발생한다.
Write Timeout : Read Timeout 과 반대로, 서버로 데이터를 보낼 때 정해진 시간을 초과하면 발생한다.
Read Timeout : 서버와 연결된 후, 데이터를 수신할 때 정해진 시간을 초과하면 발생한다.
전체 데이터를 수신하는 시간이 아니라, 데이터를 읽어오는 각 작동의 간격이 초과하면 안된다는 것이다.
5-5. 질문에 답하는 로직 추가
기존의 질문만 가져왔던 getQuestion() 메서드에서, 답변을 조회, 등록, 수정, 삭제 할 수 있도록 메서드를 추가한다.
먼저 API의 응답으로 사용할, DTO(Date Trasfer Object)인 Answer 데이터 클래스를 만든다.
그리고 새로운 메서드의 반환값을 제네릭 클래스인 Response<T>로 선언하고, 그 안에 Answer를 타입 파라미터로 사용했다.
왜냐하면 이전과 같이 Question(또는 Answer)를 직접 반환값으로 받으면, HTTP 응답 코드나 헤더와 같은 정보를 사용할 수 없기 때문이다.
HTTP의 POST와 PUT 요청을 본문을 갖는데, Retrofit에서 본문을 구성하는 방법은 3가지가 있다.
1) @FormUrlEncoded 어노테이션은 요청의 Content-Type을 application/x-www-form-urlencoded로 만든다.
그리고 메서드의 파라미터 중에 @Field 어노테이션이 붙은 것은 본문으로 만든다.
2) 파일이나, 여러 타입의 데이터를 하나의 요청으로 보내기 위해서는 multipart/form-data 로 요청해야 한다.
Retrofit에서는 메서드에 @Multipart 어노테이션을 붙여 만들 수 있다.
3) JSON으로 요청을 보낼 때 주로 사용하는 방법이다. 요청에 전달할 파라미터를 객체로 만들고, 이 파라미터에 @Body 어노테이션을 붙이면 컨버터가 객체를 직렬화하여, 요청의 본문(body)로 사용한다.
5-6. 답변 작성, 수정, 삭제 기능
[코드 5-29] 까지 마치고 나면, 답을 작성할 수는 있지만 바로 작성한 답을 볼 수 없고,
다른 탭으로 이동했다가 돌아와서야 볼 수 있는 현상이 발생한다.
왜냐하면 기존에는 TodayFragment의 onViewCreated()에서 답을 가져와서 표시하게 만들었고,
WriteActivity에서 답을 쓰고 돌아오는 과정에서는 onViewCreated()가 다시 호출되지 않기 때문이다.
이런 문제는 개발할 때 자주 만나는데, 가장 쉬운 해결 방법은 -> API를 호출해 UI를 갱신하는 코드를 "onResume()" 으로 옮기는 것이다.
하지만 onResume()은 답이 변경되지 않았을 때에도 자주 호출 되기 때문에, 복잡한 데이터나 사용자가 많은 서비스의 경우에는 서버에 많은 부하를 줄 수 있다.
이번에는 답을 쓰거나 변경하면, TodayFragment에 알려줘서 UI를 갱신하도록 해보자.
1) AndroidX의 Activity와 Fragment에서는 "startActivityForResult()" 사용을 권장하지 않는다.
"regisiterForActivityResult" 로 콜백을 등록하면, "ActivityResultLauncher" 를 반환한다.
ActivityResultLauncher 의 launch() 메서드로 Intent를 전달해 WriteActivity를 시작하도록 하자.
-> startActivity() 를 "startForResult.launch()" 로 교체한다.
2) WriteActivity에서 완료 버튼을 터치하면 -> "글쓰기 API" 를 호출 ->
-> 성공 응답을 받으면, MainAcitivity로 결과를 전달하기 위해 "setResult()" 호출 -> finish() 메서드로 종료한다.
3) 그러면 WriteActivity의 결과가, TodayFragment의 "ActivityResultCallback" 으로 전달된다.
4) 답을 가져와 레이아웃에 구성하는 코드가 중복되므로, setupAnswer() 메서드로 추출한다.
val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
setupAnswer()
}
}
}
...
binding.writeButton.setOnClickListener {
startForResult.launch(Intent(requireContext(),
WriteActivity::class.java).apply {
putExtra(WriteActivity.EXTRA_QID, question!!.id)
putExtra(WriteActivity.EXTRA_MODE, WriteActivity.Mode.WRITE)
})
}
6. JWT로 사용자를 인증하자.
OAuth는 여러 환경에서 사용할 수 있도록 다양한 권한 부여 타입이 정의되어 있다.
먼저 4가지 역할을 알아보자 :
1) 자원 소유자 (Resouce Owner)
자원에 대한 접근 권한을 부여할 수 있는 사람. EX) 구글 포토에 올린 사진에 대해서 권한을 부여할 수 있는 사용자
2) 자원 서버 (Resource Server)
자원을 호스팅하는 서버. 권한을 확인하고 자원을 제공해준다. EX) 구글 포토 서버
3) 클라이언트 (Client)
자원에 대해 접근을 요청하는 애플리케이션. EX) 전자 액자 앱
4) 권한 부여 서버, 인가 서버 (Authorization Server)
클라이언트를 자원 소유자에게 확인한 후, 자원 서버에 권한을 확인할 수 있는 토큰을 발급한다. EX) 구글의 인가 서버
※ 이미 구글 포토 서버가 있는데, 별도의 인가 서버를 사용하는 이유는 구글의 다른 서비스에 대한 인가도 함께 관리하기 때문이다.
OAuth 2.0에는 4가지 권한 부여 방법이 있다. 그 중 2가지만 알아보자.
1) Authorization Code Grant
Authorization Code Grant는 클라이언트(앱)에 직접 사용자 정보를 입력하는 경우에는 별로 맞지 않다.
직접 로컬 앱에서 로컬 계정으로 로그인을 하는데, 브라우저를 경유해야 한다면 사용자가 느끼기에도 어색할 것이다.
이렇게 직접 패스워드를 입력해 권한을 얻으려는 경우에는,
유저의 패스워드를 권한 증서로 사용하는 "Resource Owner Password Credentials Grant" 를 사용한다.
2) Resource Owner Password Credentials Grant
흐름은 단순하다. 자원 소유자가 클라이언트에게 패스워드를 제공하고, 클라이언트는 받은 패스워드를 사용해 직접 액세스 토큰을 받는다.
이 방법은 패스워드를 제삼자인 클라이언트(앱)에 제공하기 때문에, 완전히 신뢰할 수 있는 경우에만 사용해야 한다.
액세스 토큰 (Access token) : 자원 서버에 자원을 요청할 때 사용한다. 유효 시간이 짧다.
리프레시 토큰 (Refresh token) : 인가 서버에서 새로운 액세스 토큰을 발급받을 수 있다.
그러나 자원에 접근할 때 사용할 수 없다. 유효 시간이 상대적으로 길다.
6-1. 소셜 로그인과 OpenID Connect
소셜 로그인은 보통 OpenID Connect 프로토콜을 사용한다.
OpenID Connect : OAuth 2.0 를 확장해 구현된 인증 프로토콜로, OAuth의 프로토콜에 따라 권한을 요청할 때 스코프에 openid를 함께 보내면 액세스 토큰과 함께 JWT(JSON Web Token)으로 만들어진 ID 토큰을 받을 수 있다.
실제로 OAuth 2.0이 어떤 값들을 주고 받는지 보고 싶다면, 구글의 OAuth 2.0 Playground가 있다.
https://developers.google.com/oauthplayground/
6-2. JWT 란?
JSON 문자열을 토큰으로 사용할 수 있도록, Base64로 인코딩하고, 위조와 변조를 막기 위해 서명을 한 것이다.
JWT는 "토큰에 대한 설명을 담고 있는 헤더" + "전달할 데이터를 담은 페이로드" + "토큰의 검증을 위한 정보를 담은 서명" 으로 구성된다.
페이로드에는 JWT를 통해 전달하려는 클레임(Claim)들이 포함된다.
클레임 : 주제에 대한 하나의 정보. "이름-값" 형태의 쌍으로 이루어진다.
클레임을 사용하는 것이 필수는 아니지만, 토큰 관리에 유용하므로 대충 의미를 알아 둘 필요가 있다.
iss (Issuer) : JWT 토큰 발급자
sub (Subject) : 토큰의 제목
aud (Audience) : 토큰 수신자
exp (Expiration time) : 토큰 만료 시간
nbf (Not before) : 토큰의 활성화 시간
iat (Issued At) : 토큰이 발급된 시간
jti (JWT ID) : 토큰 식별자
6-3. Base64 란?
바이너리 데이터를 64개의 문자만 사용해서, 문자열로 변환하는 인코딩 방식이다.
데이터를 안전하게 텍스트로 전달하기 위해서, Base64로 인코딩을 한다.
EX) 이미지 같은 바이너리 데이터를 -> CSS나 이메일 같은 텍스트 문서에 포함하려는 경우
안드로이드에서는 android.util.Base64에서 Base64 인코딩/디코딩을 지원한다.
encode() / decode() : 인코딩/디코딩 결과를 "바이트" 로 반환한다.
Base64.encodeToString() : 인코딩 결과를 "문자열" 로 반환한다.
이 메서드들은 플래그에 따라 다른 결과를 만들기 때문에, 어떤 플래그가 있는지 알아보자.
1) DEFAULT : 기본 플래그. 인코더와 디코더에서 모두 사용할 수 있다. 모든 다른 플래그들이 적용되지 않은 상태인 것이다.
2) NO_WRAP : DEFAULT로 인코딩하면, 76자마다 나뉘고, 모든 라인에 개행 문자가 추가된다.
NO_WRAP은 DEFAULT의 이 기능을 사용하지 않고 -> 개행 없이 한 줄로 연결된 문자열을 만들 때 사용한다.
3) CRLF : 인코딩 시 개행 문자로 LF(Line Feed, \n, 0x0A) 대신
CRLF(Carriage Return + Line Feed, \r\n, 0x0D0A)를 사용한다.
4) NO_PADDING : 바이트의 길이가 3의 배수가 아니어도, '='로 채우지 않는다.
5) URL_SAFE : 테이블에서 62번, 63번 문자인 + 와 / 는 URL에서 이미 정해진 의미가 있어서, 원래는 인코딩된 문자열을 URL에 사용할 수 없다. 따라서 62번을 - , 63번을 _ 로 사용해서 인코딩과 디코딩에서 모두 사용할 수 있다.
7. Retrofit과 Coil
이미지 로딩을 직접 구현하는 것은 복잡하고 번거롭다. 일반적으로 이미지 로더에 기대되는 기능만 하더라도,
비동기와 병렬 처리 / 스토리지와 메모리 캐시 / 플레이스홀더(Placeholder) / 애니메이션 효과 / 포맷 지원 등등 너무나도 많다.
따라서 구글의 Glide나, 페이스북의 Fresco 같은 오픈 소스를 사용하는 경우가 많다.
여기서는 Coil을 사용해보자. Coil은 "Coroutine Image Loader" 의 약자로,
코루틴 기반으로 만들어졌고, 코틀린 익스텐션으로 ImageView를 확장하여 편의를 제공한다.
'안드로이드 > 도서 내용 정리' 카테고리의 다른 글
내용 정리 Part. 2 - [SNS 앱을 만들면서 배우는 안드로이드 클라이언트 개발] (0) | 2024.04.09 |
---|---|
내용 정리 - [핵심만 골라 배우는 젯팩 컴포즈] (0) | 2024.03.14 |
[2023년 11월 기준] 직접 해보면서 참고/추가 설명 - [Joyce의 안드로이드 앱 프로그래밍] (0) | 2023.12.15 |