Search
개발이 끝나면 바로 출시해야 할까 클라이언트 개발은 끝났는데 백엔드 API는 아직 준비되지 않았다고 해보자. 개발이 모두 끝나더라도 마케팅이나 이벤트 일정 때문에 공개를 기다려야 할 수 있다. 그때마다 작업 브랜치를 오래 유지하면 다른 작업과 충돌할 가능성이 커진다. 모바일 앱에는 스토어 심사에 걸리는 시간도 있다. Android와 iPhone 앱을 동시에 제출해도 같은 날 심사가 끝난다는 보장은 없다. 스토어 공식 안내 기준 심사·처리 시간 Google Play 몇 시간에서 최대 7일, 예외적으로 그 이상. 제출부터 게시까지 최소 1주일의 여유를 권장 App Store 평균적으로 제출 건의 90%를 24시간 이내에 심사. 반려나 추가 확인으로 지연될 수 있음 2026년 9월 확인한 공식 안내이며, 사용자의 설치 완료까지 걸리는 시간은 아니다. Google Play — 검토 및 게시 시점 관리, Apple — App Review 그래서 코드를 전달하는 시점과 사용자가 기능을 쓰기 시작하는 시점 을 나눈다. 구분 의미 배포(Deployment) 새 기능이 포함된 앱 버전을 전달하는 것 출시(Release) 사용자가 해당 기능을 이용할 수 있게 공개하는 것 새 기능을 OFF 상태로 미리 배포하고, 양쪽 앱과 백엔드가 준비되면 활성화할 수 있다. 다만 사용자가 앱을 업데이트해야 새 코드가 기기에 도달하므로, 구버전 앱에 없는 기능까지 켤 수는 없다. Feature Flag란 무엇인가 Feature Flag는 코드를 다시 배포하지 않고 설정에 따라 기능의 동작을 선택 할 수 있게 하는 장치다. 홈에 새로운 추천 영역을 추가했다면, Flag가 ON일 때는 새 영역을 보여주고 OFF일 때는 기존 홈을 보여준다. 여기서는 ON/OFF로 설명하지만, 설정값이 Boolean으로만 제한되는 것은 아니다. 배포와 출시를 분리하는 가치 개발 일정 분리 : 클라이언트가 먼저 끝나면 비활성 상태로 배포하고 다음 작업을 진행할 수 있다. 비즈니스 일정 분리 : 마케팅이나 이벤트 시작일에 맞춰 기능을 공개할 수 있다. 점진적 공개 : 내부 사용자부터 일부 사용자, 전체 사용자로 대상을 넓힐 수 있다. 장애 대응 : 문제가 생기면 새 기능을 끄고 기존 기능으로 전환할 수 있다. 외부 일정 때문에 완료된 코드를 오래 보관할 필요가 줄어든다. 다만 코드 배포를 기다리게 하던 의존성을 기능 활성화 조건으로 옮기는 것 이므로, 공개 전에는 백엔드와 관련 작업이 준비되어야 한다. OFF 상태로 배포하기 전에 확인할 것 버튼만 숨겨도 백그라운드 작업이나 딥링크를 통해 새 로직이 실행될 수 있다. 실제 Flag 분기와 최종 API를 함께 검증해야 한다. 상태 확인할 내용 OFF 기존 기능이 유지되고 준비되지 않은 API를 호출하지 않는가 ON Mock이 아닌 최종 API와 정상 동작하는가 설정 조회 실패 정해둔 fallback으로 동작하는가 실행 중 설정 변경 진행 중인 화면과 작업이 어긋나지 않는가 클라이언트가 새 백엔드 기능에 의존한다면 서버를 먼저 준비하고 검증한 뒤 클라이언트 노출을 연다. Flag를 끄더라도 이미 실행된 작업이나 저장된 데이터까지 되돌아가지는 않는다. Feature Flag 기능 요구사항 누구에게 제공하고, 언제 적용하며, 실패하면 어떻게 동작할지 를 정해야 한다. 요구사항 정해야 할 내용 환경 분리 QA와 프로덕션의 설정 구분 대상 지정 사용자 ID, 앱 버전, 내부 사용자 여부 등의 조건 fallback 설정이 없거나 조회에 실패했을 때의 기본 동작 캐시와 적용 시점 설정을 언제 갱신하고 화면에 반영할지 공통 사용 Presentation과 Domain에서 같은 기준으로 조회 fallback fallback은 설정을 정상적으로 얻지 못했을 때 사용자에게 제공할 동작 이다. 새 추천 영역에 문제가 생겼다면 기존 추천을 보여주거나 해당 영역을 숨길 수 있다. 기능별로 기본 동작을 정하고, 정상적인 OFF와 조회 실패를 구분해 기록한다. 설정 적용 시점 사용자가 입력 중일 때 설정이 바뀌어 화면이 교체되면 흐름이 끊길 수 있다. 새 설정은 다음 화면 진입처럼 안전한 시점에 적용 하도록 정한다. Firebase — 설정 로딩 전략 캐시나 오프라인 상태 때문에 긴급 OFF가 모든 기기에 즉시 반영되지는 않을 수 있다. A/B Test는 무엇을 검증하는가 새 추천 화면을 출시한 뒤 클릭이 늘었다고 해보자. 새 화면의 효과일 수도 있지만, 같은 기간에 진행한 이벤트 때문일 수도 있다. A/B Test는 사용자를 무작위로 나누어 서로 다른 시안을 같은 기간에 제공하고, 사전에 정한 지표를 비교하는 실험 이다. 보통 A는 기존 동작인 대조군, B는 새 동작인 실험군이며 A/B/C처럼 여러 시안을 비교할 수도 있다. 구분 Feature Flag A/B Test 핵심 질문 누구에게 어떤 기능을 열 것인가 어떤 시안이 목표 지표를 개선하는가 필요한 것 조건 평가와 기본 동작 그룹 배정, 노출·행동 기록, 결과 분석 Feature Flag로 실험 시안을 제공할 수 있지만, 화면을 나누어 보여주는 것만으로는 부족하다. 배정과 측정, 분석이 연결되어야 한다. 먼저 가설과 지표를 정한다 추천 화면을 바꾼다면 다음처럼 가설을 세울 수 있다. 추천 코스의 정보를 자세히 보여주면, 사용자가 자신에게 맞는 코스를 찾아 저장할 확률이 높아질 것이다. 개선하려는 지표와 악화되어서는 안 되는 지표를 함께 정한다. 구분 예 주 지표 정해진 기간 안에 코스를 저장한 사용자 비율 보조 지표 추천 영역 클릭률, 상세 페이지 진입률 보호 지표(Guardrail) 크래시율, 홈 로딩 시간, 이탈률 비율을 계산할 대상도 미리 정해야 한다. 클릭한 사용자만 골라 비교하면 시안에 따라 분석 대상이 달라져 결과가 왜곡될 수 있다. 토스의 푸시 실험도 CTR과 함께 활성 사용자·매출을 보호 지표로 관찰했다. 토스 — A/B Test 사례 A/B Test 기능 요구사항 요구사항 정해야 할 내용 실험 대상 어떤 사용자에게 실험을 진행할지 시안과 배정 비율 A/B/C 중 무엇을 어떤 비율로 제공할지 배정 유지 같은 사용자가 같은 시안을 경험하도록 할 기준 fallback 평가 실패 시 제공할 기본 시안과 실패 기록 공통 사용 Presentation과 Domain에서 같은 배정 결과 사용 측정 실제 노출과 목표 행동을 연결하는 로그 실험 대상과 배정 비율 먼저 실험 대상을 정하고, 그 안에서 그룹을 나눈다. 사용자의 20%를 실험 대상으로 삼고 A와 B에 절반씩 배정하면, 전체의 약 10%씩 각 시안을 보게 된다. 반드시 50:50이어야 하는 것은 아니며, 실험 목적에 맞춰 비율을 정할 수 있다. 같은 사용자에게 같은 시안 제공하기 화면을 열 때마다 새로 배정하면 한 사용자가 A와 B를 번갈아 보게 된다. 이를 막기 위해 실험 ID와 사용자 식별자를 해시해 일관된 그룹에 배정할 수 있다. 결정적 버킷 배정 방식 일반적인 사용자 단위 실험에서는 실험 기간 동안 배정을 유지하는 것을 기본 으로 한다. 홈에서 A를 보고 상세 화면에서 B로 바뀌면, 저장 행동이 어느 시안의 영향을 받았는지 판단하기 어려워진다. 배정, 노출, 전환을 구분한다 0009 로깅에서 정리한 비즈니스 메트릭 로깅이 여기서 필요하다. 단계 의미 예 배정 어떤 시안을 제공할지 결정 추천 화면 B에 배정 노출 시안이 실제 사용자 경험에 적용 B 추천 영역이 표시됨 전환 목표 행동이 발생 추천 코스를 저장 설정값을 읽었다고 사용자가 기능을 봤다고 판단해서는 안 된다. 홈 아래쪽의 추천 영역까지 스크롤하지 않았다면 실제 UI 노출은 없을 수 있다. 로그에는 실험 ID, 적용한 시안, 분석용 사용자 식별자, 이벤트와 시각을 남겨 노출과 전환을 연결한다. Compose 재구성 등으로 같은 노출을 중복 기록하지 않도록 주의한다. 결과는 어떻게 판단할까 전환율이 A는 10%, B는 11%라면 B가 1%p 높고, 상대적으로 10% 개선 된 것이다. 하지만 표본이 적다면 우연한 차이일 수 있다. 시작 전에 필요한 표본 수, 실험 기간, 중단 기준 을 정한다. 진행 중에는 로그 누락과 크래시·성능 같은 보호 지표를 확인한다. 종료 후에는 차이가 우연인지, 실제 개선 폭이 의미 있는지, 보호 지표가 악화되지 않았는지 함께 판단한다. 수치가 잠깐 좋아졌다는 이유만으로 실험을 끝내거나 전체 출시를 결정하지 않는다. Microsoft — 실험 운영 가이드 Presentation과 Domain에서 함께 사용하기 홈 추천 기능을 기준으로 책임을 나누면 다음과 같다. 아래는 특정 SDK에 종속되지 않는 설계 예시다. 구성 요소 맡는 책임 FeatureFlagReader 기능 활성화 여부 제공 ExperimentReader 실험 참여 여부와 시안 제공 Data 계층 원격 설정 조회, 캐시와 fallback 처리 UseCase / ViewModel 결정값을 데이터 조회와 화면 상태에 전달 Presentation 결정에 맞는 UI 표시와 실제 노출 기록 Domain에는 SDK 타입 대신 앱에서 사용하는 인터페이스와 모델 을 두고, Data 계층에서 구현한다. Feature Flag의 노출 여부는 Boolean으로 표현할 수 있지만, A/B Test는 여러 시안과 미참여·평가 실패 상태를 구분해야 한다. 평가 실패로 기존 화면을 보여줬다고 정상 대조군으로 기록해서는 안 된다. 한 흐름에서 확보한 결정값을 데이터 조회, 화면 표시, 전환 로그에 함께 사용한다. Domain과 Presentation이 각각 다시 조회해 다른 시안을 적용하는 일을 막기 위해서다. 두 기능을 함께 쓴다면 Flag가 OFF일 때는 기존 기능을 제공하고, ON일 때 실험 시안을 판단하는 식으로 우선순위를 정한다. 출시 후에는 분기를 정리한다 실험을 끝내고 전체 공개했다면 불필요한 Flag와 이전 시안의 코드를 제거한다. 분기가 쌓일수록 검증해야 할 조합과 유지보수 비용도 늘어난다. 다만 구버전 앱이 사용하는 원격 설정과 API는 호환성을 확인한 뒤 정리해야 한다.
0009 - 로깅로깅이란 로깅은 개발하는 동안 값을 찍어보기 위한 도구가 아니다. 통합 QA와 프로덕션에 배포된 서비스가 실제로 어떻게 동작하는지, 예상하지 못한 문제가 없는지 검증하기 위해 남기는 흔적이자 힌트다. 남기는 목적에 따라 크게 세 가지로 나눌 수 있다. 종류 무엇을 검증하는가 대표 로그 동작 검증 로깅 서비스가 의도한 대로 동작하는가 API 실패, 예외, 백그라운드 작업 시작/종료 비즈니스 메트릭 로깅 계획한 비즈니스와 유저 플로우가 의도대로 동작하는가 페이지 노출, 유저 이벤트 퍼포먼스 로깅 서비스가 충분히 빠르고 부드럽게 동작하는가 TTI, Jank Frame, CPU/메모리 동작 검증 로깅 서비스가 의도한 대로 동작하는지 확인하기 위한 로깅이다. 같은 목적이라도 QA 환경과 프로덕션 환경에서 남겨야 하는 양이 다르다. QA 백엔드와 모바일이 엮여서 통합 QA를 하면 의도하지 않은 버그가 나온다. 이를 빠르게 해결하려면 QA 빌드에서 남겨야 할 로그를 충실히 남겨야 한다. 로그가 부족하면 재현부터 다시 해야 하지만, 로그가 남아 있으면 어느 지점에서 어긋났는지 바로 좁힐 수 있다. QA 빌드에서는 e , w , i , d , v 를 모두 남겨도 된다. 프로덕션 개발과 QA 과정에서는 원인을 직접 확인하고 협업으로 문제를 수정할 수 있다. 하지만 유저가 실제로 사용하는 프로덕션에서는 그런 수정이 불가능하다. 이슈가 Crash 에러만 있는 것도 아니다. 그래서 프로덕션에서는 다음을 알 수 있어야 문제를 해결할 수 있다. 어떤 유저가 어떤 플로우를 거쳤는지 이슈가 나는 플로우에서 페이지의 관련된 상태값들이 어땠는지 단, 프로덕션에서 많은 로그를 남기면 인프라 비용과 유저의 네트워크 리소스가 소모된다. 꼭 필수적인 힌트에 대해서만 로그를 남겨야 하고, 레벨을 낮췄더라도 같은 로그가 연속적으로 불필요하게 발생하는 케이스는 없어야 한다. 두 환경의 차이 구분 QA 프로덕션 목적 통합 과정에서 나온 버그의 원인 좁히기 재현할 수 없는 이슈의 원인 추론 원인 파악 직접 확인하고 협업으로 수정 가능 남긴 로그로만 추론 가능 남기는 레벨 e , w , i , d , v 모두 e , w , i 만 로그 양 충실하게 많이 필수적인 힌트만 제약 거의 없음 인프라 비용, 유저 네트워크 리소스 비즈니스 메트릭 로깅 우리가 계획한 비즈니스나 유저 플로우가 의도대로 동작하는지를 검증하기 위한 흔적이다. 페이지 노출 로깅 어떤 페이지로 진입할 때, 유저가 그 페이지로 진입하게 유도한 UI 요소의 데이터 상태 등을 스냅샷을 찍듯이 남긴다. 유저가 어떤 요인에 이끌려 핵심 기능으로 한 Depth 더 들어왔는지를 살펴보고, 이를 지속적으로 개선하기 위한 로깅이다. 유저 이벤트 로깅 어떤 페이지에 있는 중요한 UI 요소(버튼, 리스트 아이템 등)를 클릭하거나 동작시킬 때 발생하는 로깅이다. 이때 해당 액션을 유도한 UI 요소의 데이터 상태도 함께 남겨야 한다. 클릭했다는 사실만으로는 왜 클릭했는지를 알 수 없기 때문이다. 퍼포먼스 로깅 서비스가 충분히 빠르고 부드럽게 동작하는지를 검증하기 위한 흔적이다. 느리다는 체감은 주관적이므로 숫자로 남겨야 개선 여부를 판단할 수 있다. TTI(Time to Interactive) 로깅 어떤 페이지로 진입할 때, 유저가 의미 있는 인터랙션을 할 수 있기까지 페이지가 로딩되는 데 걸리는 시간을 단계별로 남긴다. Jank Frame 로깅 앱이 동작하면서 UI 렌더링이 블락되거나 끊기는 상황을 "Jank Frame이 존재한다"라고 한다. 일정 이상의 Jank Frame이 발생할 때, 어떤 페이지에서 얼마나 발생하는지를 남긴다. Jank Frame 로깅과 동작 검증 로깅을 함께 분석하면 앱이 어떤 플로우와 동작을 실행할 때 Jank Frame이 발생하는지 추론할 수 있다. Resource(CPU, 메모리) 상태 로깅 위와 같은 로깅을 통해 서비스에 이상 증상이 있다고 판단할 때, 해당 상황에서 디바이스의 CPU와 메모리 여유 상태를 남긴다. 퍼포먼스 문제가 우리 서비스의 리소스 사용 문제인지, 아니면 디바이스 차원의 퍼포먼스 문제인지를 밝혀내기 위한 로깅이다. 로그 레벨 Android의 Log 는 다섯 단계의 레벨을 제공한다. 위쪽일수록 치명적이고, 아래로 갈수록 중요도가 낮아진다. 레벨 의미 언제 쓰는가 프로덕션 Log.e() Error / 에러 치명적인 문제가 발생했거나 예외로 기능이 동작하지 않을 때 남김 Log.w() Warning / 경고 중단할 정도는 아니지만 잠재적인 문제가 될 수 있을 때 남김 Log.i() Info / 정보 정상적인 흐름과 주요 상태 변화를 기록할 때 남김 Log.d() Debug / 디버그 개발 중 코드 흐름이나 변수 값을 확인할 때 남기지 않음 Log.v() Verbose / 상세 개발 중에서도 아주 상세하고 방대한 정보를 기록할 때 남기지 않음 Log.e() — Error 앱 실행 중 치명적인 문제가 발생했거나, 예외가 잡혀 기능이 정상 동작하지 않을 때 사용한다. try catch 블록에서 예외가 발생했을 때 API 요청이 완전히 실패하여 화면을 그릴 수 없을 때 필수 데이터베이스 트랜잭션이 실패했을 때 Log.w() — Warning 앱이 멈추거나 동작을 중단할 정도는 아니지만, 예상치 못한 상황이 발생했거나 잠재적인 문제가 될 수 있을 때 사용한다. 권장하지 않는(Deprecated) API를 호출했을 때 네트워크 연결이 비정상적이어서 캐시 데이터를 대신 불러올 때 복구 가능한 수준의 비정상적인 입력값이 들어왔을 때 Log.i() — Info 앱의 정상적인 흐름과 주요 상태 변화를 기록할 때 사용한다. 운영 환경에서도 흐름을 파악하기 위해 남겨두는 경우가 많다. 화면(Activity/Fragment) 진입 및 전환 사용자 로그인/로그아웃 성공 시점 주요 서비스나 백그라운드 작업의 시작/종료 시점 Log.d() — Debug 개발 과정에서 코드의 흐름이나 변수 값을 확인하기 위한 용도다. 실제 상용(Release) 앱에서는 출력되지 않도록 관리해야 한다. 특정 함수로 전달된 인자(Parameter) 값 확인 조건문( if / when )의 분기 진입 여부 확인 뷰(View)의 가시성(Visibility) 상태 변경 확인 Log.v() — Verbose 개발 중에서도 아주 상세하고 방대한 정보를 기록할 때 사용한다. 가장 낮은 우선순위를 가지며 Logcat에 엄청난 양의 로그를 남긴다. 반복문(Loop) 안에서 매 회차마다 변하는 상태 출력 터치 이벤트나 스크롤 이벤트처럼 연속적으로 빠르게 발생하는 이벤트 추적 네트워크 통신 시 전송되는 생(Raw) 데이터 전문 출력 정리 로깅은 개발용 출력이 아니라 QA와 프로덕션에서 문제를 추적하기 위한 흔적이다. QA 빌드는 모든 레벨을 남겨도 되지만, 프로덕션은 e , w , i 만 남기고 연속적으로 반복되는 로그가 없어야 한다. 이벤트를 남길 때는 "무엇을 했는지"뿐 아니라 "그때 상태가 어땠는지"를 함께 남겨야 원인을 추론할 수 있다.
0008 - Lazy Layout목록 안의 무수한 아이템들 앱에서는 메시지, 상품, 영상처럼 많은 항목을 세로 목록으로 보여주는 경우가 많다. 세로 목록을 만드는 가장 단순한 방법은 Column 안에서 목록을 반복하는 것이다. 하지만 Column 은 항목이 화면에 보이는지와 관계없이 모든 자식 항목을 구성하고 배치한다. 항목이 많거나 목록의 길이를 알 수 없다면 불필요한 작업이 늘어나 성능 문제가 생길 수 있다. Lazy Layout이란 무엇인가 Lazy Layout 은 많은 항목을 효율적으로 표시하기 위한 Compose 컴포저블이다. 전체 항목을 한 번에 구성하는 대신 현재 표시 영역에 보이는 항목을 중심으로 구성하고 배치한다. LazyColumn : 항목을 한 열로 배치하고 세로로 스크롤하는 목록 LazyRow : 항목을 한 행으로 배치하고 가로로 스크롤하는 목록 LazyVerticalGrid : 항목을 여러 열로 배치하고 세로로 스크롤하는 그리드 LazyHorizontalGrid : 항목을 여러 행으로 배치하고 가로로 스크롤하는 그리드 지연 레이아웃은 기존 View 시스템의 RecyclerView 와 비슷한 원칙을 따르지만 Adapter나 ViewHolder를 작성하는 대신 Compose의 DSL로 항목 구성을 선언한다. DSL로 항목 선언하기 지연 레이아웃은 DSL을 통해 항목 구성을 전달받는다. LazyColumn 은 LazyListScope 를 수신 객체로 하는 DSL을 제공한다. 이 DSL에는 목록의 항목 구성을 설명하는 함수가 있다. item : 단일 항목 추가 items(count) : 주어진 개수만큼 항목 추가 items(list) : 컬렉션의 각 데이터를 항목으로 추가 itemsIndexed(list) : 데이터와 인덱스를 함께 제공 항목 키 기본적으로 Lazy Layout은 내부 위치를 기준으로 항목을 추적한다. 항목이 추가되거나 순서가 바뀌면 기존 항목의 위치도 달라진다. 고유하고 안정적인 값을 key 로 제공하면 Compose는 위치가 바뀌어도 같은 항목을 식별할 수 있다. key 는 목록 안에서 유일해야 하며, 항목이 이동해도 바뀌지 않는 값을 사용해야 한다. 스크롤 상태 다루기 각 지연 레이아웃은 스크롤 상태를 다루는 State를 제공한다. LazyColumn , LazyRow : LazyListState LazyVerticalGrid , LazyHorizontalGrid : LazyGridState rememberLazyListState() 는 LazyColumn 의 스크롤 상태를 저장하는 LazyListState 를 생성한다. 이 객체를 통해 다음 정보를 확인하거나 제어할 수 있다. 현재 처음 보이는 항목 항목이 얼마나 스크롤되었는지 특정 항목으로 이동하기 다음은 LazyListState 를 사용해 LazyColumn 의 첫 번째 항목으로 이동하는 예시다. firstVisibleItemIndex : 현재 화면에서 처음 보이는 항목의 인덱스 확인 scrollToItem() : 지정한 항목으로 즉시 이동 animateScrollToItem() : 지정한 항목으로 애니메이션과 함께 이동 두 이동 함수는 정지 함수이므로 rememberCoroutineScope() 로 만든 코루틴 안에서 호출한다. 사용할 때 주의할 점 지연 레이아웃은 UI를 필요한 시점에 구성하지만 데이터를 자동으로 나누어 불러오지는 않는다. 데이터 로딩에는 Paging 같은 별도의 방식이 필요하다. 항목 UI 자체가 무거우면 화면에 처음 나타날 때 스크롤이 끊길 수 있으므로 각 항목의 구성 비용도 줄여야 한다. 크기가 정해지지 않은 지연 레이아웃을 같은 방향의 다른 스크롤 안에 중첩하면 측정 문제가 발생할 수 있다.
0007 - Deep Link와 App LinkAndroid에서의 링크란? 링크라고 하면 보통 웹 브라우저에서 다른 페이지로 이동하는 URL을 떠올린다. 하지만 Android에서 링크는 웹 페이지만 가리키는 것이 아니다. 링크를 눌렀을 때 앱을 열고, 홈 화면이 아니라 특정 상품이나 게시글 화면으로 바로 이동하게 만들 수도 있다. 예를 들어 사용자가 메시지에서 상품 링크를 눌렀다고 해보자. 앱이 없다면 웹의 상품 페이지를 연다. 앱이 있고 링크와 연결되어 있다면 앱의 상품 상세 화면을 연다. 즉 Android의 링크는 외부에 표현된 주소를 앱 내부의 특정 화면과 연결하는 진입점 으로 사용할 수 있다. 앱에서 링크가 사용되는 방식 앱의 화면은 일반적으로 화면 전환 코드로 이동한다. 이 코드는 이미 앱 안에 있는 사용자를 상품 상세 화면으로 이동시킨다. 반면 링크는 브라우저, 검색 결과, 알림, QR 코드, 이메일, 다른 앱처럼 앱 외부에서 같은 화면으로 들어올 수 있게 한다. 링크가 화면 자체를 만드는 것은 아니다. Android가 링크 정보를 Intent 로 앱에 전달하면, 앱이 그 URI를 해석해 알맞은 화면으로 이동한다. 일반 웹 링크와 앱 화면 이동의 관계 웹에서는 URL이 웹 서버의 리소스를 가리킨다. 앱에서는 같은 주소를 내부 화면의 목적지로 대응시킬 수 있다. 따라서 링크는 웹 주소와 앱 내비게이션 사이의 약속이다. 웹과 앱이 같은 콘텐츠를 제공한다면 하나의 HTTPS 주소를 공유할 수 있고, 앱 전용 기능이라면 별도의 Custom Scheme을 사용할 수도 있다. URI 기본 구조 Android의 링크를 이해하려면 URI 구조를 먼저 알아야 한다. 구성 예시 역할 Scheme https URI를 처리하는 방식 Host shop.example.com 대상 도메인 또는 호스트 Port 443 접속 포트, 보통 생략 Path /products/42 대상 리소스나 앱 화면 경로 Query ref=message 추가 매개변수 Fragment review 리소스 내부의 특정 위치 Custom Scheme도 같은 구조를 사용할 수 있다. 여기서는 myapp 이 Scheme, products 가 Host, /42 가 Path다. Deep Link Deep Link란? Deep Link는 사용자를 앱의 첫 화면에만 보내지 않고 앱 내부의 특정 콘텐츠나 기능으로 바로 연결하는 링크 다. 이 링크는 앱을 열기만 하는 것이 아니라 42 번 상품의 상세 화면을 열어야 한다는 정보까지 담고 있다. Android의 Deep Link는 Intent 시스템을 이용한다. 앱이 AndroidManifest.xml 에 처리할 URI 규칙을 선언하면, Android는 해당 규칙과 들어온 링크를 비교해 처리 가능한 Activity를 찾는다. Deep Link의 동작 과정 mermaid flowchart LR Link "사용자가 링크 클릭" Intent "Android가 ACTION VIEW Intent 생성" Match "설치된 앱의 Intent Filter와 비교" Select "처리할 앱 결정" Activity "Activity에 URI 전달" Screen "앱 내부 화면으로 이동" Link Intent Intent Match Match Select Select Activity Activity Screen 일반적인 처리 순서는 다음과 같다. 1. 사용자가 링크를 누른다. 2. Android가 URI를 담은 ACTION VIEW Intent를 만든다. 3. 시스템이 설치된 앱의 Intent Filter 중 URI와 일치하는 항목을 찾는다. 4. 기본 앱이 있으면 그 앱을 열고, 하나만 일치하면 해당 앱을 연다. 5. 여러 앱이 일치하면 사용자가 앱을 선택해야 할 수 있다. 6. 선택된 Activity가 intent.data 에서 URI를 읽고 내부 화면으로 이동한다. 여기서 중요한 점은 Intent Filter가 일치한다고 해서 반드시 내 앱이 열리는 것은 아니라는 것 이다. 다른 앱도 같은 URI 규칙을 선언할 수 있기 때문이다. 특히 Android 12 이상에서는 검증되지 않은 일반 웹 링크가 기본적으로 브라우저에서 열릴 수 있다. 자신이 소유한 웹 도메인을 안정적으로 앱에 연결하려면 뒤에서 다룰 App Link가 필요하다. Custom Scheme Custom Scheme은 앱이 직접 정의한 Scheme을 사용하는 방식이다. https 대신 앱 이름이나 서비스 이름을 Scheme으로 사용하므로 구현이 단순하고 별도의 웹 도메인이 없어도 된다. 하지만 Custom Scheme에는 소유권 검증이 없다. 다른 앱도 courmy Scheme을 처리한다고 선언할 수 있어 어떤 앱이 열릴지 보장할 수 없고, 일반 웹 주소가 아니므로 앱이 설치되지 않았을 때 보여줄 웹 페이지도 없다. 간단한 구현 방식 먼저 링크를 받을 Activity의 AndroidManifest.xml 에 Intent Filter를 선언한다. VIEW : URI의 콘텐츠를 보여주는 Intent임을 나타낸다. DEFAULT : 암시적 Intent를 Activity가 받을 수 있게 한다. BROWSABLE : 브라우저 같은 앱 외부에서 Activity를 실행할 수 있게 한다. data : 처리할 URI의 Scheme, Host, Path 규칙을 선언한다. 이제 Activity에서 전달된 URI를 읽을 수 있다. courmy://course/42 에서 42 를 추출해 코스 상세 화면의 인자로 넘기는 방식이다. 실제 앱에서는 값의 누락이나 잘못된 형식, 로그인이 필요한 화면, 삭제된 콘텐츠 같은 경우도 함께 처리해야 한다. Navigation Compose를 사용한다면 navDeepLink 로 URI 패턴과 목적지를 연결할 수도 있다. Manifest는 어떤 Activity가 링크를 받을지 정하고, 내비게이션 코드는 받은 링크를 앱 내부의 어떤 화면에 연결할지 정한다. Android App Link App Link란? Android App Link는 앱과 웹사이트의 관계를 검증한 HTTP 또는 HTTPS Deep Link 다. 링크의 모양은 평범한 웹 URL이지만, Android가 해당 도메인과 앱의 관계를 확인한 뒤 앱이 설치되어 있으면 연결된 화면을 바로 열 수 있다. 앱이 설치되어 있지 않다면 같은 URL이 브라우저에서 열리므로 웹 콘텐츠로 자연스럽게 이어질 수 있다. Deep Link와의 차이 App Link는 Deep Link와 완전히 별개의 기능이 아니다. Deep Link의 Intent 기반 화면 이동에 웹 도메인 소유권 검증 을 더한 형태다. Custom Scheme Deep Link는 같은 Scheme을 선언한 앱이 여럿일 수 있다. 반면 검증된 App Link는 Android가 웹사이트와 앱의 연결을 확인하므로, 사용자가 앱 선택 창을 거치지 않고 해당 앱으로 이동할 수 있다. HTTPS 기반 링크 App Link를 처리할 Activity에는 android:autoVerify="true" 를 포함한 Intent Filter를 선언한다. autoVerify 는 설치 과정에서 선언한 Host와 앱의 관계를 확인해 달라고 Android에 요청한다. 도메인에는 다음 위치에 assetlinks.json 파일을 배포한다. 이 파일에는 연결을 허용할 앱의 패키지 이름과 서명 인증서의 SHA 256 지문이 들어간다. 도메인 검증이 필요한 이유 Manifest 선언만으로 링크의 소유권이 생기지는 않는다. 누구나 자신의 앱에서 https://courmy.example.com 을 처리하겠다고 선언할 수 있기 때문이다. App Link 검증은 양쪽의 선언을 맞춰본다. mermaid flowchart LR App "앱 Manifest<br/ 이 도메인을 처리하겠다" Android "Android 검증" Web "assetlinks.json<br/ 이 앱에 처리를 허용한다" App Android Web Android Android Result "검증된 연결" 1. 앱은 Manifest에 처리할 HTTPS 도메인을 선언한다. 2. 웹사이트는 assetlinks.json 에 허용할 앱과 서명 인증서를 선언한다. 3. Android는 앱 설치 시 해당 파일을 가져와 두 선언이 일치하는지 확인한다. 4. 검증에 성공하면 그 도메인의 링크를 앱으로 바로 전달할 수 있다. 이 과정은 다른 앱이 링크를 가로채는 것을 막고, 사용자에게 앱 선택 창을 반복해서 보여주지 않기 위해 필요하다. 검증에 실패하면 App Link의 직접 연결을 보장할 수 없다. 패키지 이름, 릴리스 서명 인증서 지문, Host, assetlinks.json 의 접근 경로를 함께 확인해야 한다. Play App Signing을 사용한다면 로컬 키가 아니라 Play Console에서 확인한 앱 서명 인증서 지문이 필요한 경우도 있다. Deep Link vs App Link 앞에서 본 것처럼 App Link도 넓은 의미에서는 Deep Link다. 아래 표에서는 차이를 분명히 보기 위해 Custom Scheme Deep Link 와 Android App Link 를 비교한다. 구분 Custom Scheme Deep Link Android App Link 주소 예시 courmy://course/42 https://courmy.example.com/course/42 Scheme 앱이 정의한 Scheme http , https 웹 도메인 없어도 됨 필요함 소유권 검증 없음 assetlinks.json 으로 검증 앱 선택 가능성 다른 앱도 같은 Scheme을 선언할 수 있음 검증 성공 시 연결된 앱으로 직접 이동 앱 미설치 시 처리할 앱이 없어 실패할 수 있음 웹 페이지로 이동 가능 구현 범위 앱의 Intent Filter 중심 앱 설정과 웹 서버 설정 모두 필요 주요 용도 앱 전용 이동, 내부 연동, 빠른 실험 웹과 앱이 공유하는 공개 콘텐츠 Custom Scheme Deep Link가 적합한 경우 연결할 웹사이트나 소유한 도메인이 없다. 링크가 외부 웹 콘텐츠가 아니라 앱 전용 기능을 나타낸다. 개발 중 빠르게 링크 기반 화면 이동을 확인한다. Scheme 충돌 가능성을 통제할 수 있는 앱 간 연동에 사용한다. 장점은 도메인과 서버 설정 없이 앱에서 간단히 구현할 수 있다는 것이다. 단점은 Scheme의 소유권을 증명할 수 없고, 앱이 설치되지 않았을 때 자연스럽게 이동할 웹 목적지가 없다는 것이다. Android App Link가 적합한 경우 웹사이트의 상품, 게시글, 코스 같은 콘텐츠를 앱 화면과 연결한다. 검색, 광고, 이메일, 메시지에 공유할 표준 HTTPS 주소가 필요하다. 앱이 설치되어 있으면 앱으로, 없으면 웹으로 보내고 싶다. 링크를 다른 앱이 가로채지 못하도록 신뢰할 수 있는 연결이 필요하다. 장점은 하나의 HTTPS 주소를 웹과 앱이 공유하고, 검증된 도메인에서 앱으로 자연스럽게 이동할 수 있다는 것이다. 단점은 소유한 도메인과 서버 설정이 필요하고, 앱 서명이나 assetlinks.json 설정이 어긋나면 검증이 실패한다는 것이다. 정리 Android에서 링크는 단순히 앱을 실행하는 주소가 아니라, 외부의 URI와 앱 내부 화면을 연결하는 진입점이다. Deep Link는 이 연결을 만드는 전체 개념이고, Custom Scheme은 도메인 없이 간단히 앱 전용 링크를 만드는 방법이다. Android App Link는 HTTPS Deep Link에 도메인 검증을 추가해 웹사이트와 앱의 관계를 보장한다. 따라서 앱 전용 기능이나 제한된 연동에는 Custom Scheme이 간단할 수 있다. 하지만 자신이 소유한 웹 콘텐츠를 사용자에게 공유하고 앱과 웹을 자연스럽게 연결해야 한다면, 검증된 Android App Link를 사용하는 편이 적합하다. 참고 Android Developers About deep links Android Developers Create deep links Android Developers About App Links Android Developers Verify App Links
0006 - BFF (Backend for Frontend)하나의 API를 여럿이 나눠 쓸 때 서버 하나가 웹, iOS, Android, 심지어 외부 파트너까지 상대한다고 해보자. 보통은 범용 API 하나를 두고 모두가 같은 엔드포인트를 호출한다. 문제는 클라이언트마다 필요한 데이터가 다르다는 것이다. 모바일 목록 화면: 썸네일·제목·가격 정도만 필요 웹 상세 화면: 리뷰 전문, 판매자 정보, 배송 정책까지 필요 워치 앱: 제목 한 줄이면 충분 같은 GET /products/{id} 를 부르는데, 누구는 응답의 90%를 버리고 누구는 부족해서 API를 여러 번 더 부른다. 범용 API의 한계 이 상황을 두 가지 증상으로 나눠 볼 수 있다. Over fetching — 응답에 안 쓰는 필드가 잔뜩 딸려 온다. 모바일 목록인데 리뷰 배열까지 내려받는 식. Under fetching — 한 화면을 그리는 데 필요한 데이터가 여러 API에 흩어져 있어, 클라이언트가 요청을 여러 번 날려 조립한다. 특히 under fetching이 커지면, 화면을 조립하는 책임이 클라이언트로 넘어온다. mermaid flowchart LR Client "클라이언트 (화면 조립)" A "GET /products/1" B "GET /products/1/reviews" C "GET /sellers/9" D "GET /shipping/policy" Client A Client B Client C Client D 상세 화면 하나를 위해 네 번을 부르고, 받아온 조각들을 클라이언트가 이어 붙인다. API가 바뀌면 각 클라이언트(웹·iOS·Android)가 각자 조립 로직을 고쳐야 한다. 즉 범용 API는 "모두를 위한 API"라서 어느 화면에도 딱 맞지 않는다. BFF란 무엇인가 BFF(Backend for Frontend)는 특정 프론트엔드를 위한 전용 백엔드 계층 이다. 범용 백엔드 앞에 클라이언트 종류별로 얇은 서버를 하나씩 두고, 그 서버가 그 화면에 필요한 만큼만 조립해서 내려준다. mermaid flowchart LR Mobile "모바일 앱" Web "웹" MBFF "Mobile BFF" WBFF "Web BFF" Core "범용 백엔드 (products / reviews / sellers)" Mobile MBFF Web WBFF MBFF Core WBFF Core 모바일 앱은 Mobile BFF 의 GET /screens/product detail 한 번 만 부른다. 그 안에서 BFF가 products·reviews·sellers를 모아, 모바일 화면에 필요한 모양으로 응답을 만든다. 웹은 별도의 Web BFF 가 웹에 맞는 응답을 만든다. 핵심은 조립 책임을 클라이언트에서 서버(BFF)로 옮긴다 는 것이다. 경계를 어디에 긋나 BFF의 본질은 "새 서버를 만드는 것"이 아니라 책임의 경계를 다시 긋는 것 이다. 구분 범용 API BFF 화면 조립 클라이언트 BFF 응답 모양 모두에게 동일 클라이언트마다 다름 API가 바뀔 때 각 클라이언트가 대응 BFF가 흡수 여러 API를 모으고, 필요 없는 필드를 걷어내고, 화면이 원하는 형태로 다듬는 일 — 이 조립(aggregation) 을 어디서 할 것인가의 문제다. 클라이언트에 두면 배포가 무겁고 플랫폼마다 중복되지만, BFF에 두면 서버에서 한 번에 바꿀 수 있다. SDUI와의 관계 0002에서 "화면 구성을 서버가 결정하고 클라이언트는 그린다"는 SDUI를 정리했다. 그때 자연스럽게 남는 질문이 있다. 그 UI 정의 JSON은 누가 만들어 주나? 범용 백엔드는 "제품 데이터"를 알지, "이 화면을 어떤 컴포넌트로 조립할지"는 모른다. 화면 단위로 응답을 만들어 주는 계층이 필요하고, 그게 사실상 BFF의 역할이다. mermaid flowchart LR Core "범용 백엔드" BFF "BFF" Client "클라이언트" Screen "화면" Core 도메인 데이터 BFF BFF 화면 단위 UI 정의 JSON Client Client 정의 해석 후 렌더링 Screen 범용 백엔드 : "무슨 데이터가 있나" BFF : "이 화면을 어떤 컴포넌트·순서·데이터로 구성할까" → SDUI 응답 조립 클라이언트 : "어떻게 그릴까" 즉 SDUI는 응답을 어떻게 해석할지 의 약속이고, BFF는 그 응답을 누가 만들지 의 자리다. 둘은 짝이다. 클라이언트 Repository 관점에서 0004에서 클라이언트의 Repository가 데이터 출처를 감추고 도메인 모델로 바꿔 올려보낸다고 했다. BFF가 있으면 이 Repository가 상대하는 API의 성격이 달라진다. BFF 없이(범용 API) : Repository가 여러 API를 부르고, 응답 조각들을 도메인 모델로 조립한다. 조립 로직이 클라이언트에 있다. BFF 있이 : Repository는 화면 단위 API 하나를 부른다. 조립은 이미 BFF가 끝냈다. 클라이언트가 가벼워지는 대신, 조립의 복잡도가 사라지는 게 아니라 BFF로 옮겨갔을 뿐 이라는 점을 기억해야 한다. 트레이드오프 BFF가 공짜는 아니다. 서버가 하나 더 생긴다. 클라이언트 종류마다 BFF를 두면 그만큼 배포·운영 대상이 늘어난다. BFF끼리 중복이 생긴다. Mobile BFF와 Web BFF가 비슷한 조립을 각자 하기도 한다. (공통 조립을 어디까지 공유할지가 또 하나의 설계 문제) 경계가 흐려질 위험. BFF에 비즈니스 로직까지 스며들면, 얇은 조립 계층이 아니라 또 하나의 무거운 백엔드가 된다. BFF는 화면을 위한 조립 까지만 맡는 게 원칙이다. 언제 BFF가 맞나 클라이언트 종류가 여럿이고, 화면마다 필요한 데이터 모양이 크게 다를 때 under fetching으로 클라이언트가 API를 여러 번 부르며 조립하고 있을 때 SDUI처럼 화면 단위 응답 을 서버가 만들어 줘야 할 때 반대로 클라이언트가 사실상 하나거나, 범용 API가 이미 화면과 잘 맞는다면 BFF는 과한 계층이 된다. 결국 BFF는 "조립 책임을 클라이언트에 둘 것인가, 서버 경계 안으로 들일 것인가"라는 선택의 문제다.
0005 - sealed class로 상태와 에러 표현하기정해진 종류를 타입으로 표현하기 코드를 짜다보면, 이 값은 정해진 몇 가지 중 하나인 경우가 자주 나온다. 화면: 로딩·성공·실패 중 하나 요청 실패: 네트워크·서버·미발견 중 하나 이렇게 종류가 닫혀 있는 값 을 코틀린에서 여러 방법으로 표현할 수 있는데 그중에 sealed가 있다. sealed class란 무엇인가 sealed class는 하위 타입의 집합이 컴파일 시점에 고정되는 타입이다. 상위 타입을 상속하는 하위 타입은 같은 모듈 안에만 존재할 수 있고, 외부에서 새 하위 타입을 임의로 추가할 수 없다. 그래서 "닫힌 계층(closed hierarchy)"이라고 부른다. ProductUiState 인 값은 반드시 Loading , Success , Error 중 하나고, 이 세 가지가 전부라는 사실을 컴파일러가 안다. 비슷한 것끼리 비교하기 종류가 닫혀 있나 종류마다 다른 데이터를 갖나 일반 interface X (누구나 구현) O enum O X (구조가 동일) sealed O O 일반 인터페이스는 누구나 구현할 수 있어 열려 있다. 하위 타입이 몇 개일지 컴파일러가 알 수 없다. enum도 닫힌 집합이지만, 각 상수는 같은 구조를 공유한다. Success 만 목록을 갖고 Error 만 에러를 갖는 식으로 종류마다 다른 데이터를 담을 수 없다. sealed는 닫혀 있으면서 종류마다 다른 데이터를 가질 수 있다. sealed class의 장점 1. 표현할 수 있는 값 == 실제 종류 ProductUiState 가 표현할 수 있는 값은 정확히 세 가지뿐이다. "로딩이면서 동시에 에러"처럼 현실에 없는 상태를 타입 차원에서 만들 수 없다. 2. when 을 빠짐없이 강제함 값을 만들어내는 표현식 when 은 모든 경우를 빠짐없이 다뤄야 한다(exhaustive). 그래서 값을 다 나열할 수 없는 Int 나 String 같은 타입에는 보통 else 로 나머지를 메꿔야 한다. 반면 sealed는 하위 타입이 전부 알려져 있어, 종류를 모두 나열하면 그 자체로 exhaustive가 된다. else 없이도 컴파일이 통과한다. else가 없는게 뭐 어쩌라고? 새로운 종류를 추가할 때, 그 차이를 알 수 있다. else 가 있는 when 의 경우: 실수하면 그냥 else 로 감 else 가 없는 when 의 경우: 컴파일 에러가 나면서 처리가 빠진 곳을 컴파일러가 대신 짚어주게 됨 적용 1: UI State 화면 상태를 boolean 필드로 나열하는 방식부터 보자. 필드가 서로 독립적이라, 실제로는 있을 수 없는 조합까지 표현된다. 앞서 만든 sealed 버전과 비교하면 차이가 분명하다. 플래그 방식: 있을 수 없는 상태도 만들어짐 → 화면이 필드 조합을 매번 따져야 함 sealed 방식: 상태는 항상 셋 중 하나 → 각 상태는 자기 데이터만 가짐 셋으로 안 나뉠 때는? 단순히 Loading / Success / Error 3개의 상태로 부족한 경우도 있다. (ex. 결과가 비어 있는 화면/목록을 유지한 채 새로고침하는 화면 등) 이때 sealed의 성질을 유지한 채 조정할 수 있도록 해야한다. 별개의 화면: 새로운 종류로 승격시킨다. 특정 상태 안에서만 의미 있는 경우: 그 하위 타입 안에 필드로 둔다. isRefreshing 은 Success 일 때만 존재하므로 "로딩 중인데 새로고침 중" 같은 모순이 안 생긴다. 아무 상태에서나 켜지는 플래그가 문제였고, 하위 타입 안에 가두면 그 값은 다시 실제 상태와 맞아떨어진다. 적용 2: 에러 타입 실패를 문자열 하나로 뭉뚱그리면 화면이 실패 종류에 따라 대응을 나눌 수 없다. 네트워크가 끊긴 것과 서버가 500을 반환한 것은 안내가 달라야 한다. 여기서도 종류를 sealed로 닫는 게 핵심이다. 예외( Throwable ): 누구나 상속 가능 → 종류가 무한한 열린 계층 DataError (sealed): 앱이 다룰 실패만 → 닫힌 계층 Repository나 UseCase에서 잡은 예외를 이 도메인 에러로 번역해서 올려보낸다. 그러면 화면은 프레임워크의 예외 타입이 아니라 앱이 정의한 닫힌 종류를 기준으로 대응할 수 있다. 언제 sealed class가 맞나 정해진 몇 가지 중 하나이고, 그 목록을 우리가 통제할 때(종류가 배타적이고 닫혀 있을 때) 사용하면 좋다.
0004 - Repository와 데이터 흐름Repository Pattern 앱에서 상품 목록을 보여주기 위해 서버에 데이터를 요청한다고 생각해보자. 아무것도 모르는 A씨는 ViewModel 에서 API를 직접 호출하는 방식으로 구현을 하였다. 코드가 단순할 때는 큰 문제가 없어 보인다. 하지만 데이터가 서버뿐만 아니라 로컬 DB에서도 필요하거나, 네트워크 오류가 발생했을 때 저장된 데이터를 사용해야 한다면 ViewModel 이 알아야 할 내용이 점점 많아진다. 데이터를 서버와 DB 중 어디에서 가져올 것인가? 서버에서 받은 데이터를 DB에 저장할 것인가? 네트워크 요청에 실패하면 어떻게 할 것인가? API 응답을 앱에서 사용하는 모델로 어떻게 변환할 것인가? 이러한 데이터 처리 책임을 ViewModel 에서 분리하기 위해 Repository를 사용할 수 있다. Repository란 무엇인가 Repository는 애플리케이션에서 필요한 데이터를 제공하는 데이터 접근 창구 이다. 데이터를 사용하는 쪽에서는 Repository에 필요한 데이터를 요청한다. 실제 데이터가 API에서 오는지, DB에서 오는지, 또는 캐시된 값인지는 알 필요가 없다. ViewModel 은 이제 ProductApi 대신 ProductRepository 를 사용한다. Repository 구현체에서는 데이터를 실제로 어디에서 가져올지 결정한다. 서버 요청에 성공하면 새로운 데이터를 반환하고 로컬에 저장한다. 요청에 실패하면 로컬에 저장된 데이터를 반환한다. 이 과정이 Repository 안에 감춰져 있으므로 ViewModel 은 데이터가 어디에서 왔는지 몰라도 된다. 캐시 사용 여부, 원격 요청 실패 시 로컬 데이터를 사용할지와 같은 것은 데이터 제공 정책 이므로 Repository가 담당할 수 있다. 반면 품절 상품을 제외하는 것과 같은 비즈니스 규칙은 UseCase나 Domain에서, 가격을 30,000원으로 바꾸는 것과 같은 화면용 가공은 ViewModel 또는 UI 계층에서 담당하는 편이 각 책임을 구분하기 쉽다. Repository 주변의 구성요소 Repository와 함께 자주 등장하는 구성요소로 DataSource , UseCase , ViewModel 이 있다. DataSource DataSource는 API나 DB처럼 특정 데이터 출처와 직접 통신 한다. DataSource와 Repository는 모두 데이터를 다루지만 책임은 다르다. DataSource는 API나 DB와 통신하며 데이터를 어떻게 가져오고 저장할지 안다. Repository는 여러 DataSource 중 어떤 데이터를 제공할지 결정한다. 예를 들어 Retrofit을 다른 네트워크 기술로 교체하거나 Room의 쿼리가 변경되는 것은 DataSource의 변화에 가깝다. 반면 서버 데이터를 언제 새로 요청할지, 실패했을 때 캐시를 사용할지와 같은 데이터 제공 정책은 Repository의 변화에 가깝다. 데이터 출처가 API 하나뿐이고 로직도 단순하다면 별도의 DataSource 없이 Repository가 API를 직접 사용할 수도 있다. UseCase UseCase는 애플리케이션에서 사용자가 수행하는 하나의 행위 를 표현한다. 상품 목록 중 판매 가능한 상품만 가격순으로 보여줘야 한다고 생각해보자. Repository는 상품 데이터를 제공한다. UseCase는 그 데이터를 이용해 판매 가능한 상품 목록을 만든다. ViewModel ViewModel은 화면에서 발생한 요청을 받아 UseCase를 실행하고, 그 결과를 화면 상태에 반영한다. 책임을 나누고 나면 ViewModel은 데이터를 가져오는 방법이나 상품을 선별하는 규칙보다 화면 상태 관리에 집중할 수 있다. Domain과 모델 Domain은 애플리케이션이 해결하려는 문제 영역 을 의미한다. 쇼핑 앱이라면 상품, 가격, 재고, 주문과 같은 개념과 그에 관련된 규칙이 Domain에 해당한다. Domain은 특정 라이브러리나 화면이 아니라 앱의 핵심 개념을 표현한다. 여기서 Product 는 앱이 이해하고 사용하는 Domain Model이다. Entity Domain에서 Entity는 속성 일부가 변하더라도 식별자가 같으면 동일한 대상 으로 보는 객체이다. 상품의 이름이나 가격이 변경되어도 id 가 같다면 같은 상품이다. (Domain Entity와 Room의 @Entity 는 다름.) Value Object Value Object는 식별자가 아니라 값 자체로 구분되는 객체 이다. ProductId(1) 과 또 다른 ProductId(1) 은 같은 값을 나타낸다. 금액, 수량, 이메일, 주소처럼 값의 의미와 규칙을 하나의 타입으로 표현할 때도 Value Object를 사용할 수 있다. DTO DTO(Data Transfer Object)는 계층이나 시스템 사이에서 데이터를 전달하기 위한 객체이다. ProductResponse 는 서버 응답 형식에 맞춰진 DTO이다. Repository에서는 이를 앱에서 사용하는 Domain Model로 변환한다. 화면에 "30,000원" 처럼 가공된 값이 필요하다면 별도의 UI Model을 사용할 수도 있다. 각 모델은 같은 상품을 표현하더라도 서로 다른 목적을 가진다. 모든 프로젝트에서 모델을 반드시 세 종류로 나눠야 하는 것은 아니다. 외부 데이터 형식과 Domain 또는 화면의 요구사항이 서로 다르고, 한쪽의 변경이 다른 쪽에 미치는 영향을 줄여야 할 때 분리할 수 있다. 전체 흐름으로 이해하기 지금까지 살펴본 구성요소를 연결하면 다음과 같은 흐름이 만들어진다. ViewModel: 화면 상태 관리 UseCase: 하나의 행위와 그에 필요한 비즈니스 규칙 Repository: 데이터 출처를 감추고 어떤 데이터를 제공할지 결정 DataSource: 특정 데이터 출처와 통신하는 기술적 세부사항 담당 Domain Model: 앱의 핵심 개념 표현
0003 - DI시작: 카카오 로그인만 있던 앱 이해를 돕기 위해 하나의 상황을 가정해보자. 어떤 안드로이드 앱을 처음 만들 때, 로그인은 카카오 하나면 충분했다고 하자. 그래서 로그인을 처리하는 LoginService 가 KakaoLogin 을 직접 생성해서 쓰도록 만들었다. 이 코드는 카카오 로그인만 쓸 때는 아무 문제가 없다. 잘 동작하고, 코드도 단순하다. 문제는 여기서부터 시작된다. 문제: 애플 출시, Apple 로그인을 추가해야 한다 그러던 어느 날, 이 앱을 iOS에도 출시하게 되었다고 하자. 그런데 애플 앱스토어 심사 정책상, 다른 소셜 로그인을 제공하는 앱은 Apple 로그인도 함께 제공해야 한다. 즉, 이제 AppleLogin 을 추가해야 한다. 그런데 막상 추가하려고 보니, LoginService 가 KakaoLogin 을 직접 생성해서 강하게 붙들고 있다. Apple 로그인을 끼워 넣으려면 결국 LoginService 내부를 이렇게 뜯어고쳐야 한다. 로그인 수단이 하나 늘었을 뿐인데, LoginService 가 KakaoLogin 과 AppleLogin 두 구현을 모두 직접 알아야 하고, if 분기까지 떠안게 됐다. 이렇게 한 객체가 다른 객체의 구체 타입을 직접 알고, 직접 생성까지 책임지는 상태를 결합도가 높다(tight coupling) 고 한다. 결합도가 높으면 다음과 같은 문제가 생긴다. 로그인 수단을 추가할 때마다(구글, 네이버…) LoginService 코드를 계속 수정해야 한다. 테스트할 때 실제 소셜 인증 대신 가짜 객체(Mock)를 끼워 넣기 어렵다. KakaoLogin 이나 AppleLogin 의 변경이 LoginService 에까지 영향을 준다. 즉, 서로 다른 객체들이 너무 강하게 묶여 있어서 하나를 바꾸거나 추가하면 다른 하나도 함께 바꿔야 하는 상태가 된 것이다. 이 결합도를 낮추기 위한 첫 번째 원칙이 DIP다. DIP (Dependency Inversion Principle) DIP는 의존관계 역전 원칙 으로, 핵심은 "구체적인 구현이 아니라 추상화에 의존하자 "는 것이다. 다시 생각해보면, LoginService 는 "소셜 로그인을 한다"는 행위만 필요할 뿐, 그것이 카카오인지 애플인지는 사실 알 필요가 없다. 그래서 "소셜 로그인을 한다"는 역할을 인터페이스(추상화)로 분리한다. 이제 LoginService 는 KakaoLogin 이나 AppleLogin 이라는 구체 클래스를 몰라도 되고, SocialLogin 이라는 인터페이스에만 의존한다. 아까와 달리 if 분기도, 구현체를 직접 생성하는 코드도 사라졌다. 나중에 구글·네이버 로그인을 추가하더라도 SocialLogin 을 구현하는 클래스를 하나 더 만들면 될 뿐, LoginService 코드는 건드리지 않아도 된다. 여기서 "역전(Inversion)"이라는 단어가 붙은 이유는, 원래 상위 모듈( LoginService )이 하위 모듈( KakaoLogin )에 의존하던 방향이, 둘 다 추상화( SocialLogin )에 의존하는 형태로 방향이 뒤집혔기 때문이다. 마지막 문장이 잘 와닿지 않아서, 조금 더 찾아보았다. 변경 전 변경 후 상위? 하위? 두 모듈 사이의 상대적 위치로 정책에 가까우면 상위, 세부 구현에 가까우면 하위이다. (간단하게는 조금 더 넓은 범위를 상위라고 생각한다.) LoginService = 상위 모듈 "로그인을 한다"는 흐름을 지휘하는 고수준 정책 KakaoLogin = 하위 모듈 실제 카카오 인증을 처리하는 저수준 세부 구현 "역전"의 진짜 의미 제어 흐름 vs 의존 방향 객체 사이에는 방향이 다른 두 가지가 있다. 제어 흐름 실행할 때 누가 누구를 호출하는가. LoginService 가 KakaoLogin.authenticate() 를 부른다. 이건 DIP를 적용하든 안 하든 항상 그대로 다. 소스코드 의존 컴파일할 때 누가 누구를 알아야 하는가. 이것이 DIP로 뒤집히는 대상이다. 변경 전에는 이 둘이 같은 방향이었다. 실행도 LoginService → KakaoLogin , 의존도 LoginService → KakaoLogin . 변경 후에는 제어 흐름은 그대로인데 의존 방향만 뒤집힌다. KakaoLogin 은 실행할 땐 여전히 LoginService 에게 불려가는 쪽인데, 소스코드에서는 자기가 SocialLogin 을 향해 의존하는 쪽이 되었다. 실행 방향과 의존 방향이 반대가 된 것 — 이것이 "역전(Inversion)"이다. 쉽게 비유하면 식당의 메뉴판 과 같다. 메뉴판이 없는 식당이 있다. 그리고 주방에는 한식·중식·양식 셰프가 따로 있다고 하자. 그럼 손님( LoginService )은 원하는 메뉴에 맞는 셰프를 직접 불러야 했다. "중식 메뉴 먹을 거니까 중식 셰프님 나와주세요"처럼, 손님이 주방에 누가 있는지까지 알고 지목해야 했던 셈이다. 하지만 이제 손님은 그냥 메뉴판( SocialLogin ) 을 보고 주문만 하면 된다. 그러면 그 주문에 맞는 셰프( KakaoLogin , AppleLogin …)가 들어온 주문서를 보고 알아서 요리를 내온다. 손님은 누가 요리하는지 알 필요가 없고, 주방 쪽이 메뉴판이라는 규격에 맞춰 움직인다. 손님이 셰프를 직접 지목하던 관계가, 주방이 메뉴판에 맞춰 나서는 관계로 뒤집힌 것 이다. 그런데 여기서 한 가지 의문이 남는다. LoginService 가 SocialLogin 추상화에 의존하는 것은 좋은데, 그렇다면 실제 KakaoLogin 이나 AppleLogin 객체는 누가, 어디서 넣어주는가? 이 질문에 대한 답이 DI다. DI (Dependency Injection) DI는 의존성 주입 으로, "객체가 필요로 하는 의존성을 내부에서 생성하지 말고, 외부에서 주입하자 "는 개념이다. 앞의 LoginService 는 이미 생성자를 통해 SocialLogin 을 받도록 되어 있다. 이 객체를 실제로 만들 때, 외부에서 어떤 구현체를 넣어줄지 결정한다. LoginService 는 자기가 쓸 SocialLogin 을 직접 만들지 않는다. 누군가 밖에서 만들어서 넣어준다. 이렇게 하면 상황에 따라 주입할 객체를 자유롭게 바꿀 수 있다. DI를 구현하는 방법은 크게 세 가지가 있다. 1) 생성자 주입 (Constructor Injection) 생성자를 통해 주입한다. 객체를 만들 때 필요한 의존성이 반드시 채워지고, val 로 불변으로 둘 수 있어 가장 권장되는 방식 이다. 2) 필드 주입 (Field Injection) 필드에 직접 주입한다. 코드는 간결하지만, 객체 생성 후 주입 전까지는 값이 비어 있을 수 있고 테스트에서 갈아끼우기 어렵다. 3) 메서드 주입 (Setter Injection) setter 메서드를 통해 주입한다. 생성 이후에 의존성을 바꿔 끼울 수 있다. DIP가 "추상화에 의존하라"는 원칙 이라면, DI는 그 의존성을 실제로 어떻게 연결할지에 대한 방법 이다. 그런데 객체가 많아지고 의존 관계가 복잡해지면, 이 "외부에서 주입하는" 코드를 매번 직접 작성하는 것이 부담스러워진다. LoginService 하나를 만들기 위해 KakaoLogin 을 만들고, KakaoLogin 이 또 다른 객체를 필요로 하면 그것도 만들어야 한다. 이 객체 생성과 주입의 책임을 개발자가 아닌 다른 주체가 대신 관리해주면 어떨까? 그것이 IoC다. IoC (Inversion of Control) IoC는 제어의 역전 으로, " 객체의 생성과 제어권을 개발자가 아니라 프레임워크가 관리한다 "는 개념이다. 기존에는 개발자가 직접 객체를 생성하고, 언제 어떤 객체를 넣을지 흐름을 제어했다. IoC에서는 이 제어권이 프레임워크로 넘어간다. 개발자는 "이 객체가 필요하다"고 선언만 하고, 실제로 객체를 만들어서 연결하는 일은 프레임워크(컨테이너)가 대신한다. 즉, 제어의 흐름이 개발자 → 프레임워크로 역전된다. 개발자는 "무엇이 필요한지"만 선언한다. 프레임워크가 필요한 객체를 만들어서 적절한 곳에 주입한다. 이 차이를 코드로 보면 확실해진다. 아래는 안드로이드의 대표적인 DI 프레임워크인 Hilt를 쓴 예시다. 변경 전 개발자가 new (객체 생성)와 조립을 직접 한다. 변경 후 개발자는 "무엇이 필요한지"만 애노테이션으로 선언하고, 생성·조립 코드는 사라진다. 이 셋이 맞물리면 Hilt가 " LoginActivity 에 LoginService 가 필요하다 → LoginService 는 SocialLogin 이 필요하다 → SocialLogin 은 KakaoLogin 으로 만들면 된다" 는 의존성 그래프를 스스로 따라가며 객체들을 만들고 연결한다. 결국 val loginService = LoginService(KakaoLogin()) 같은 생성·조립 코드가 통째로 사라진 것 이 핵심이다. "언제 무엇을 만들어서 넣을지" 결정하는 제어권이 개발자에게서 프레임워크로 넘어갔기 때문이다. DI는 IoC를 구현하는 대표적인 방식이다. 프레임워크가 객체의 생성과 생명주기를 관리하면서, 필요한 곳에 의존성을 자동으로 주입해주는 것이다. 이렇게 프레임워크가 객체의 생성과 제어를 관리해주는 대표적인 예가 Spring의 IoC 컨테이너이고, Android 진영에서는 Hilt 같은 DI 프레임워크가 같은 역할을 한다. Hilt Hilt는 안드로이드에서 DI를 쉽게 구현하도록 도와주는 구글이 공식적으로 권장하는 DI 라이브러리다. 내부적으로 Dagger를 기반으로 하되 복잡한 설정을 줄여, 앞에서 본 것처럼 애노테이션( @Inject , @AndroidEntryPoint 등)만 붙이면 프레임워크가 의존성 그래프를 분석해 알아서 객체를 만들고 주입해준다. 안드로이드에서 특히 Hilt가 필요한 이유는 다음과 같다. 컴포넌트를 직접 생성할 수 없다. Activity , Fragment , ViewModel 같은 안드로이드 컴포넌트는 개발자가 생성자를 호출해 만드는 게 아니라 시스템(프레임워크)이 대신 생성 한다. 그래서 여기에 의존성을 넣어주기가 까다로운데, Hilt가 이 지점에 자동으로 주입해준다. 보일러플레이트가 줄어든다. 수동 DI에서 필요한 팩토리·조립 코드 없이, 애노테이션만으로 의존성 연결이 끝난다. 생명주기에 맞춘 관리. 객체를 앱 전체( @Singleton )나 화면 단위 등 원하는 범위에 맞춰 살아있게 관리할 수 있다. 테스트가 쉬워진다. 실제 구현 대신 가짜 객체로 교체해 주입하기 편하다. 테스트하기 테스트를 어떻게 쉽게 한다는 걸까? 1) 직접 만든 가짜 객체 (Fake) SocialLogin 인터페이스만 구현하면 무엇이든 주입할 수 있으니, 테스트용 가짜를 하나 만든다. 이 가짜는 실제 인증 대신 "호출됐다"는 사실만 기록한다. 주입 지점이 열려 있기 때문에 실제 카카오 서버 없이, 네트워크 없이, LoginService 의 동작만 딱 떼어내어 검증할 수 있다. 2) Mockito로 만든 Mock 가짜 클래스를 매번 손으로 만들기 번거롭다면, Mockito 같은 라이브러리가 인터페이스로부터 가짜 객체를 자동으로 만들어준다. verify 는 "그 메서드가 실제로 호출됐는가"를 확인해준다. LoginService 가 구현체를 직접 만들지 않고 주입받도록 되어 있기 때문에, 테스트에서 자유롭게 가짜를 밀어 넣을 수 있다. 테스트가 쉬워지는 이유 의존성을 외부에서 주입받는 구조라서, 그 자리에 진짜 대신 가짜를 끼워 넣을 수 있기 때문이다. 강하게 결합된 코드였다면 이 자리에 손을 댈 방법 자체가 없었다. 결론 결국 이 모든 개념은 결합도를 낮추고, 로그인 수단이 늘어나도 유연하게 대응할 수 있으며, 테스트하기 쉬운 코드를 만들기 위한 하나의 흐름으로 이어진다.
프로그래머스 - [1차] 캐시문제 사이트 https://school.programmers.co.kr/learn/courses/30/lessons/17680 문제 간단 요약 입력 캐시 크기( cacheSize ) 기억할 수 있는 도시의 수 0 <= cacheSize <= 30 도시이름 배열( cities ) 입력되는 도시 이름 순서 도시이름은 영문자로 되어있음 도시이름은 대소문자를 구분하지 않음 (ex. Jeju == jeJU) 1 <= len(cities) <= 100000 출력 입력된 도시이름 배열을 순서대로 처리할 때 걸리는 총 실행시간을 구하는 문제 조건 캐시 교체 알고리즘: LRU (Least Recently Used) 가장 최근에 사용한 것이 캐시에 남아있는 알고리즘 (가장 사용한지 오래된 것을 캐시에서 빼는 방식) cache hit 일 경우 실행시간: 1 캐시 안에 도시 이름이 있는 경우 cache miss 일 경우 실행시간: 5 캐시 안에 도시 이름이 없는 경우 풀이 고려할 점 대소문자가 섞여있는 도시 이름 lower() : 소문자로 이름 통일 도시 이름을 기억하는 캐시가 필요함 deque : FIFO cache miss 가 났을 때, popleft() 로 가장 오래된 도시 이름을 빼기에 적합함 cache 라는 큐에 cacheSize 이하의 도시 이름들을 저장한다. 그리고 처리할 도시 이름이 cache 에 있는지 없는지에 따라 cache hit 와 cache miss 로 나누어 처리한다. cache hit 의 경우 원래 캐시 내에 존재하던 도시 이름을 제거하고( remove ) 제일 뒤(가장 최신)에 이름을 넣어준다. ( append ) 그리고 총 실행시간에 +1을 해준다. cache miss 의 경우 캐시에 도시 이름을 넣어주고( append ) 만약 캐시가 넘쳤다면, 가장 오래된 이름을 제거해준다. ( popleft ) 그리고 총 실행시간에 +5를 해준다. ❋ 실제 캐시 시스템은 공간을 확보한 후 데이터를 저장하지만, 구현을 단순화하고 cacheSize == 0 인 경우까지 자연스럽게 처리하기 위해 먼저 삽입한 뒤 캐시 크기를 초과하면 가장 오래된 데이터를 제거하도록 구현함 AI's comment 좋은 점 1 ) cache miss 시 축출(가장 오래된 항목 제거)을 popleft 로 O(1) 에 처리함 2 ) 삽입 후 초과 시 제거하는 구조라 cacheSize == 0 엣지 케이스를 별도 분기 없이 흡수함 아쉬운 점 1 ) (핵심 로직·성능) cache hit 경로의 in 과 remove 가 앞에서부터 찾아 각각 O(n) 임 (핵심 병목) 다만 이 문제는 cacheSize <= 30 이라 n이 작아 실제 성능 차이는 미미하고, 점근적으로 더 나은 방식이 있다는 관점의 지적임 2 ) (가독성·유지보수) cache hit =1, cache miss =5가 코드에 그대로 박힌 매직 넘버임. 이름 붙은 상수로 빼면 의미가 드러나고 조건이 바뀌어도 한 곳만 고치면 됨 3 ) (견고성) if len(cache) cacheSize 는 반복마다 하나만 추가하므로 정확하지만, "한 번에 하나만 넣는다"는 암묵적 전제에 기댐 — 방어적으로는 while 이 더 안전함 (이 문제 범위에선 문제 없음) 4 ) (사소) deque( ) 의 는 불필요함 — deque() 만으로 빈 덱이 생성됨 다른 풀이 1 ) collections 의 OrderedDict deque 대신 해시 기반 자료구조를 써서 조회·삭제·최근사용 갱신을 모두 O(1)로 처리 2 ) 일반 dict 의 삽입 순서 유지를 이용한 코드 OrderedDict 없이도 dict가 삽입 순서를 보존하는 점(Python 3.7부터 언어 명세로 보장)을 활용
프로그래머스 - n^2 배열 자르기문제 사이트 https://school.programmers.co.kr/learn/courses/30/lessons/87390 문제 간단 요약 입력 배열 크기( n ) n x n 크기의 2차원 배열 1 <= n <= 10^7 구간( left , right ) 1차원 배열의 특정 구간을 의미하는 인덱스 0 <= left <= right <= n^2 right left < 10^5 출력 2차원 배열로 만든 1차원 배열의 특정 구간을 출력하는 문제 조건 1. 2차원 배열 채우기 2. 1차원 배열 만들기 3. 원하는 구간 추출하기 풀이 고려할 점 2차원 배열을 생성해서 모두 넣기에는 너무 많음 n^2 은 최대 10^14 까지 가능하여 규칙을 찾아야 함 채워진 2차원 배열을 잘 살펴보면 규칙이 존재한다. (0, 0): 1 (1, 3): 4 (2, 1): 3 (3, 3): 4 ... 행과 열 중에 큰 값에서 +1을 하면 해당 위치에 들어가는 숫자를 알 수 있다. left 부터 right 까지 숫자를 올리면서 좌표로 치환하여 계산하면, 최대 10^5 만 반복하면 된다. 좌표값은 n 으로 사이즈를 알고있으니 쉽게 구할 수 있다. 행: i//n 열: i%n AI's comment 좋은 점 1 ) 배열을 만들지 않고 인덱스 → 좌표 → 값 규칙( max(행, 열)+1 )만으로 계산해, n^2 이 최대 10^14 여도 right left < 10^5 번만 반복하는 최적 접근임 2 ) 시간·공간 모두 출력 크기에 비례( O(right left) )해 이 문제에서 더 줄일 여지가 없음 아쉬운 점 1 ) (가독성·파이썬다움) append 루프는 리스트 컴프리헨션으로 한 줄에 담는 게 더 관용적이고, 파이썬에선 반복 append 호출이 사라져 약간 더 빠름 2 ) (사소) i//n 과 i%n 은 몫·나머지를 따로 두 번 나누는 셈임. divmod 로 한 번에 구하면 의도가 더 드러남 (성능 차이는 미미) 다른 풀이 1 ) 컴프리헨션 + divmod 위 두 지적을 합친 형태 — 접근·복잡도는 원본과 동일하고 표현만 더 간결함
프로그래머스 - 숫자 야구문제 사이트 https://school.programmers.co.kr/learn/courses/30/lessons/451808 문제 간단 요약 입력 최대 제출 횟수( n ) submit 을 제출할 수 있는 횟수 제한 6 <= n <= 3024 숫자 제출 함수( submit ) 1000이상 9999이하의 정수를 입력받음 "xS yB" 형식의 문자열로 return xS: 스트라이크가 x개 있음 yB: 볼이 y개 있음 출력 submit 함수를 n 회 초과하지 않은 상태에서 올바른 번호를 return하기 조건 S: 스크라이크 숫자가 있으며, 올바른 위치에 들어감 B: 볼 숫자가 있지만, 올바르지 않은 위치에 들어감 4자리 숫자의 각 자리 수는 다름 풀이 고려할 점 숫자가 중복으로 들어가는 경우의 수 제거 set 으로 중복 숫자를 제거하여 사용 제출 결과를 통해 가능한 후보군을 줄이는 과정 필요 compare(a, b) : a 와 b 를 비교했을 때의 결과 반환 다음 제출할 최적의 후보군 선택 pick(cands) : 후보군 중에서 가장 최적의 후보군을 선택하여 반환함 Minimax 알고리즘 : 최악의 피해를 최소화하는 질문을 선택하는 알고리즘 Max : 이 질문에서 가장 나쁜 경우(후보가 가장 많이 남는 경우) Min : 그 가장 나쁜 경우를 가장 작게 만드는 질문을 선택 전체적인 과정 1. 중복 숫자가 있는 4자리 번호는 제거한다. 2. 후보군 중에서 가장 피해가 작은 최적의 후보군을 선택한다. 3. 해당 후보군을 제출하여 실제 결과를 바탕으로 후보군을 필터링한다. 4. 이를 반복하여, 최종 비밀번호를 찾는다. 1. 중복 숫자가 있는 후보군 제거 우선 1000 9999 중에서 숫자가 중복으로 들어가는 경우를 제거해준다. 문자열을 set 자료형으로 바꾸게되면, 중복되는 문자가 제거된 상태로 남는다. (ex. "3444" : {"3", "4"}) 이를 이용하여, set 의 길이가 4인 경우만 nums 에 넣어준다. py nums = for i in range(1000, 10000): if len(set(str(i))) == 4: nums.append(i) py def pick(cands): best worst = len(cands) + 1 best num = cands 0 for g in cands: 예상 후보군 groups = defaultdict(int) 다른 후보군과의 관계가 집계되는 곳 for c in cands: key = compare(g, c) groups key += 1 worst = max(groups.values()) 집계된 것 중에서 가장 최악의 수 선택 if worst < best worst: 그 최악의 수가 가장 작은 예상 후보군을 선택 best worst = worst best num = g return best num 최적 후보군 반환 py first = 1234 임의의 1차 필터링 while not len(nums) == 1: if first: target = first.pop() else: target = pick(nums) 최적 후보군 선택 (1차 필터링이 다 없어진 후) r s, r b = check(target) submit이 반환한 문자열을 정수로 바꿔서 반환해주는 함수 next nums = for n in nums: c s, c b = compare(target, n) if r s == c s and r b == c b: next nums.append(n) nums = next nums.copy() py from collections import defaultdict def compare(a, b): a, b = str(a), str(b) s cnt = 0 b cnt = 0 for i in range(4): if a i == b i : s cnt += 1 b cnt = 1 b cnt += b.count(a i ) return s cnt, b cnt def solution(n, submit): nums = for i in range(1000, 10000): if len(set(str(i))) == 4: nums.append(i) def check(num): result = submit(num) return int(result 0 ), int(result 3 ) def pick(cands): best worst = len(cands) + 1 best num = cands 0 for g in cands: groups = defaultdict(int) for c in cands: key = compare(g, c) groups key += 1 worst = max(groups.values()) if worst < best worst: best worst = worst best num = g return best num first = 1234 while not len(nums) == 1: if first: target = first.pop() else: target = pick(nums) r s, r b = check(target) next nums = for n in nums: c s, c b = compare(target, n) if r s == c s and r b == c b: next nums.append(n) nums = next nums return nums 0 py while not len(nums) == 1: 수정 전 while len(nums) 1: 수정 후 py return int(result 0 ), int(result 3 ) 수정 전 s, b = result.split() 수정 후 return int(s : 1 ), int(b : 1 ) py def compare(a, b): a, b = str(a), str(b) strike = sum(x == y for x, y in zip(a, b)) 원본 반복문의 s cnt ball = len(set(a) & set(b)) strike 공통 숫자 수 스트라이크 = 볼 return strike, ball py from itertools import permutations nums = int(''.join(p)) for p in permutations('0123456789', 4) if p 0 != '0' range+set 필터 대체
0002 - SDUISDUI SDUI는 Server Driven UI의 약자로, 화면을 어떤 UI로 구성할지를 서버가 결정하고, 클라이언트는 서버가 내려준 정의를 해석하여 화면을 그리는 방식을 의미한다. 기존 방식에서는 화면의 구조와 배치가 클라이언트 코드에 고정되어 있고, 서버는 그 화면을 채울 데이터만 내려준다. 반면 SDUI에서는 어떤 컴포넌트를, 어떤 순서로, 어떤 값으로 보여줄지 를 서버 응답이 함께 정의한다. 기존 방식의 한계 앞서 본 광고나 실험(AB 테스트)처럼 기간·위치·대상에 따라 다른 View를 노출 해야 하는 상황을 생각해보자. 일반적인 클라이언트 중심 UI에서는 화면 구조가 앱 코드에 하드코딩된다. 예를 들어 목록 화면이 다음과 같이 구성되어 있다고 하자. 이 코드에서 서버는 각 영역에 들어갈 데이터 만 내려줄 뿐, 화면의 구조는 앱에 고정되어 있다. 이 상태에서 "특정 기간 동안, 목록 중간에, 특정 사용자에게만 광고 배너를 끼워 넣고 싶다"는 요구가 생기면 클라이언트 코드를 수정하고 다시 배포 해야 한다. 문제는 모바일의 특성상 다음과 같은 제약이 따른다는 점이다. 화면 구성을 바꿀 때마다 앱 배포가 필요하다. 앱 스토어 심사 기간만큼 반영이 지연된다. 사용자가 앱을 업데이트하지 않으면 최신 화면을 볼 수 없다. 즉, 광고나 실험처럼 짧은 주기로 바뀌고, 조건에 따라 다르게 노출되어야 하는 화면 을 클라이언트 코드만으로 대응하기는 어렵다. 이러한 문제를 해결하기 위해, 화면의 구성 자체를 서버가 제어하는 SDUI가 등장하게 되었다. SDUI의 핵심 아이디어 SDUI의 핵심은 화면을 데이터로 표현하는 것 이다. 즉, "이 화면은 제목·설명 영역과 이미지가 붙은 영역으로 구성된다"라는 구조 자체를 서버 응답(JSON)으로 표현 한다. 이렇게 하면 광고나 실험용 컴포넌트를 원하는 위치에, 원하는 기간 동안 서버 응답에 끼워 넣기만 하면 되므로, 앱 배포 없이 화면을 유연하게 바꿀 수 있다. 클라이언트는 화면을 직접 정의하지 않고, 서버가 내려준 UI 정의를 순회하며 대응되는 컴포넌트를 그린다. mermaid flowchart LR Server "서버" Response "UI 정의 (JSON)" Client "클라이언트" Screen "화면" Server UI 구조 + 데이터 생성 Response Response 응답 전달 Client Client 정의 해석 후 렌더링 Screen 1. 서버가 화면을 구성하는 UI 정의를 생성한다. 2. 서버는 UI 정의를 JSON 형태의 응답으로 내려준다. 3. 클라이언트는 응답을 파싱하여 각 컴포넌트를 렌더링한다. 즉, 서버는 "무엇을 보여줄지"를 결정하고, 클라이언트는 "어떻게 그릴지"만 담당한다. 구성 요소 SDUI 응답은 화면을 구성하는 컴포넌트들의 목록으로 이루어진다. 각 컴포넌트는 일반적으로 다음과 같은 요소로 구성된다. viewType (컴포넌트 종류) 화면을 구성하는 UI 조각의 종류를 나타낸다. 클라이언트는 이 값을 기준으로 어떤 UI를 그릴지 결정한다. TitleDescriptionViewType 처럼 구성 요소와 배치를 이름에 담아 표현하기도 한다. content (데이터) 해당 컴포넌트가 실제로 표시할 값이다. 제목, 설명, 이미지 정보 등이 포함되며 viewType 마다 형태가 다르다. image / icon (시각 요소) 이미지 URL뿐 아니라 크기( size ), 배경색( bgColor ) 등 표현에 필요한 속성을 함께 정의한다. action (액션) 컴포넌트에서 발생하는 사용자 상호작용을 정의한다. 화면 이동( navigate ), 토스트( showToast ), 스낵바( showSnackBar ) 등이 해당된다. 여기서 주목할 점은 viewType 의 작명 방식 이다. StartImageTitleDescriptionActionIconType 처럼 어떤 요소가 어떤 위치에 배치되는지를 이름에 그대로 담으면, 별도의 레이아웃 필드 없이도 서버와 클라이언트가 화면 구조를 공유할 수 있다. (자세한 내용은 아래 API 응답 설계에서 다룬다.) 서버와 클라이언트의 역할 SDUI에서 서버와 클라이언트의 책임은 명확하게 나뉜다. 구분 책임 서버 어떤 컴포넌트를, 어떤 순서로, 어떤 데이터와 액션으로 보여줄지 결정 클라이언트 viewType 에 대응하는 UI를 렌더링하고, 액션을 실행 여기서 중요한 점은 클라이언트가 컴포넌트의 종류( viewType )를 미리 알고 있어야 한다 는 것이다. 서버는 화면의 "구성"을 바꿀 수 있지만, 클라이언트가 해석할 수 없는 새로운 viewType 을 만들 수는 없다. 즉, 서버는 이미 정의된 컴포넌트들을 조합 하여 화면을 구성한다. mermaid flowchart LR subgraph Server "서버" direction TB S1 "컴포넌트 조합" S2 "content 매핑" S3 "action 정의" end subgraph Client "클라이언트" direction TB C1 "viewType 매칭" C2 "컴포넌트 렌더링" C3 "action 처리" end Server UI 정의 JSON Client API 응답 설계 SDUI에서 가장 중요한 것은 화면을 표현하는 응답(스키마) 설계 이다. 응답은 보통 컴포넌트들의 목록으로 구성되며, 각 컴포넌트는 viewType 과 content 를 가진다. 예를 들어 다음과 같은 응답을 설계할 수 있다. 이 응답을 해석해보면 세 개의 컴포넌트가 위에서 아래로 순서대로 그려진다. 1. TitleDescriptionViewType — 제목과 설명만 있는 가장 단순한 형태 2. TitleDescriptionEndImageViewType — 제목/설명의 오른쪽 끝( End )에 아이콘 이미지가 붙는 형태 3. StartImageTitleDescriptionActionIconType — 왼쪽( Start )에 이미지, 오른쪽에 액션 아이콘이 붙는 형태 만약 서버가 sduiComponents 배열의 순서를 바꾸거나, 노출 대상·기간에 따라 컴포넌트를 추가/삭제하면 앱 배포 없이 화면 구성이 바뀐다. viewType — 화면 구조를 담은 이름 이 스키마에서 viewType 은 단순한 종류 구분을 넘어 레이아웃 자체를 이름으로 표현 한다. StartImage... → 이미지가 앞( Start )에 위치 ...EndImage... → 이미지가 뒤( End )에 위치 ...ActionIcon... → 상호작용 가능한 아이콘 포함 덕분에 별도의 레이아웃 필드 없이도, viewType 이름만으로 클라이언트가 어떤 구조를 그려야 하는지 알 수 있다. content — viewType마다 달라지는 데이터 content 는 viewType 에 따라 형태가 달라진다. titleText / descriptionText — 기본 텍스트 값이다. descriptionTextV2 — 부분 스타일링을 위한 확장 필드이다. text 배열의 각 조각(span)마다 style (예: bold )을 지정할 수 있어, 한 문장 안에서 일부만 강조할 수 있다. image — 이미지 URL뿐 아니라 size , bgColor 등 표현 속성을 함께 정의한다. actionIcon — 이미지에 action 이 결합된 형태이다. actionType 으로 동작을 구분한다. ( navigate , showToast , showSnackBar 등) navigate 인 경우 navigateLink 로 이동할 위치를 지정한다. 여기서 descriptionText 와 descriptionTextV2 가 함께 존재하는 점에 주목할 수 있다. descriptionTextV2 를 해석하지 못하는 구버전 클라이언트는 기존 descriptionText 를 그대로 사용하고, 신버전 클라이언트는 descriptionTextV2 로 더 풍부한 표현을 그린다. 이렇게 하위 호환을 고려한 필드 확장(V2) 방식으로 스키마를 점진적으로 발전시킬 수 있다. 필드 설계 원칙 응답 스키마를 설계할 때 다음을 고려한다. viewType은 클라이언트와 서버가 공유하는 약속이다. 클라이언트가 이미 구현해 둔 값만 사용할 수 있다. content는 viewType마다 형태가 다르다. viewType 에 따라 어떤 content 구조가 오는지 명세가 필요하다. 클라이언트가 모르는 viewType은 무시할 수 있어야 한다. 서버가 새 컴포넌트를 추가해도 구버전 앱이 깨지지 않도록 한다. 여기까지 정리하면, SDUI에서 API 응답을 설계한다는 것은 결국 화면을 컴포넌트 단위로 쪼개고, 각 컴포넌트를 viewType 과 content 로 표현하는 규칙을 서버와 클라이언트가 함께 약속하는 일 이라고 할 수 있다.
0001 - MVW ArchitectureMVW Architecture MVW는 Model View Whatever의 약자로, Model과 View를 중심으로 각 구성 요소의 책임과 상호작용 방식을 정의하는 아키텍처 패턴들을 의미한다. 대표적인 MVW 패턴으로는 MVC, MVP, MVVM, MVI가 있다. 각 패턴은 모두 관심사 분리를 목표로 하지만, 책임을 분리하는 방식과 데이터 흐름에 차이가 있으며 각각 고유한 장단점을 가진다. MVC 이전 GUI 애플리케이션은 사용자 입력에 따라 데이터를 변경하고, 그 결과를 화면에 반영하는 과정을 반복한다. 이러한 처리들이 하나의 클래스나 컴포넌트에 함께 작성되는 경우가 많았다. 상품 재고를 관리하는 간단한 예제를 살펴보자. 위 코드에는 데이터( stock ), 사용자 입력 처리( setOnClickListener ), 그리고 비즈니스 규칙(재고 차감 및 재고 검증)이 모두 함께 존재한다. 이처럼 UI 코드와 비즈니스 로직의 결합성이 높으면, 애플리케이션 규모가 커질수록 유지보수와 테스트가 어려워진다. 이러한 문제를 해결하기 위해 데이터 관리, 화면 표현, 사용자 입력 처리의 책임을 분리하는 다양한 아키텍처가 등장하게 되었으며, 이를 통해 유지보수성과 재사용성을 높이고자 하였다. MVC 가장 먼저 등장한 대표적인 아키택처는 MVC이다. MVC(Model View Controller)는 애플리케이션을 Model, View, Controller의 세 가지 구성요소로 나누어 각 요소의 책임을 분리하는 아키텍처 패턴이다. 구성 요소 Model 애플리케이션의 데이터와 비즈니스 로직을 담당한다. View 사용자에게 화면(UI)을 보여주고 사용자 입력이 발생하는 영역이다. Controller View로부터 전달받은 입력을 처리하고, Model을 변경하는 역할을 담당한다. 일반적인 데이터 흐름 일반적인 MVC는 다음과 같은 흐름으로 동작한다. mermaid flowchart LR User "사용자" View "View" Controller "Controller" Model "Model" User 입력 View View 이벤트 전달 Controller Controller 비즈니스 로직 수행 Model Model 변경된 데이터 View 1. 사용자가 View를 통해 입력한다. 2. View는 입력을 Controller에게 전달한다. 3. Controller는 Model의 비즈니스 로직을 호출하여 데이터를 변경한다. 4. View는 변경된 Model의 상태를 화면에 반영한다. 즉, Controller가 View와 Model 사이를 중재하는 역할 을 수행한다. Android에서의 MVC Android에는 Controller라는 독립적인 컴포넌트가 존재하지 않는다. 따라서 일반적으로 다음과 같이 역할을 대응시킨다. MVC Android Model Model, Repository 등 View XML 기반 UI(View System) Controller Activity / Fragment Android에서 XML은 화면의 구조만 정의하며, 실제 사용자 입력 처리와 UI 갱신은 Activity(Fragment)가 담당한다 따라서 실제 데이터 흐름은 다음과 같이 이루어진다. mermaid flowchart LR User "사용자" Activity "Activity / Fragment<br/ (Controller)" Model "Model" View "XML(View)" User 입력 Activity Activity 데이터 변경 Model Model 결과 반환 Activity Activity UI 갱신 View 즉, Activity(Fragment)는 Controller의 역할 뿐만 아니라 View와 Model을 중재하고 UI를 직접 갱신하는 역할까지 수행하게 된다. 한계 MVC는 각 구정 요소의 책임을 분리하기 위해 등장했지만, Android에서는 Controller 역할을 Activity(Fragment)가 담당하게 된다. 문제는 Activity(Fragment)가 Controller의 역할뿐만 아니라 Android 컴포넌트가 수행해야 하는 역할까지 함께 담당한다는 점이다. 사용자 입력 처리: Controller 역할 Model 호출: Controller 역할 화면(UI) 갱신: View를 직접 제어하는 역할 생명주기 관리: Android Framework 권한 요청: Android Framework 화면 전환: Android Framework 결과적으로 하나의 클래스에 여러 책임이 집중되면서 유지보수와 테스트가 어려워지고, 이를 Massive Activity 문제라고 한다. 이러한 문제를 해결하기 위해 화면 로직을 Activity로부터 분리하고, Controller의 화면 처리 책임을 별도의 Presenter에게 위임한 MVP(Model View Presenter)가 등장하게 된다. MVP MVP(Model View Presenter)는 MVC에서 Controller가 담당하던 역할을 Presenter로 분리한 아키텍처 패턴이다. 구성 요소 Model 애플리케이션의 데이터와 비즈니스 로직을 담당한다. View 사용자에게 화면(UI)을 보여주고, 사용자 입력을 Presenter에게 전달한다. Presenter View로부터 전달받은 입력을 처리하고, Model을 호출한다. Model의 결과를 View가 표시할 수 있는 형태로 전달한다. 일반적인 데이터 흐름 mermaid flowchart LR User "사용자" View "View" Presenter "Presenter" Model "Model" User 입력 View View 이벤트 전달 Presenter Presenter 비즈니스 로직 수행 Model Model 결과 반환 Presenter Presenter UI 갱신 요청 View 1. 사용자가 View를 통해 입력한다. 2. View는 입력 이벤트를 Presenter에게 전달한다. 3. Presenter는 Model을 호출하여 비즈니스 로직을 수행한다. 4. Model은 처리 결과를 Presenter에게 반환한다. 5. Presenter는 View에게 화면 갱신을 요청한다. 흐름 자체만을 보면 Controller과 비슷하게 View와 Model 사이를 중재하는 역할을 한다. Android에서의 MVP Android에서는 일반적으로 다음과 같은 역할을 대응시킨다. MVP Android Model Model, Repository 등 View Activity / Fragment Presenter Presenter 클래스 MVC에서는 Activity(Fragment)가 Controller 역할과 UI 갱신 책임을 함께 담당하지만, MVP에서는 Activity(Fragment)가 View 역할을 집중하고, 입력 처리와 화면 로직은 Presenter가 담당한다. 따라서 실제 데이터 흐름은 다음과 같이 이루어진다. mermaid flowchart LR User "사용자" View "Activity / Fragment<br/ (View)" Presenter "Presenter" Model "Model" User 입력 View View 이벤트 전달 Presenter Presenter 데이터 요청/변경 Model Model 결과 반환 Presenter Presenter UI 갱신 요청 View 이 구조에서는 Activity(Fragment)가 직접 Model을 호출하지 않고, Presenter에게 사용자 입력을 전달한다. Presenter는 Model의 결과를 바탕으로 View에게 어떤 화면을 보여줄지 요청한다. 한계 MVP는 Activity(Fragment)의 책임을 줄여주지만, View와 Presenter가 서로 강하게 연결되는 구조를 가진다. Presenter는 View 인터페이스를 알고 있고, View는 Presenter를 호출한다. 화면이 복잡해질수록 Presenter에서 View에게 요청하는 메서드가 많아질 수 있다. 또한 Android에서는 Activity(Fragment)의 생명주기에 따라 Presenter와 View의 연결을 적절히 관리해야 한다. 그렇지 않으면 이미 사라진 View를 Presenter가 참조하게 되어 메모리 누수나 잘못된 UI 갱신이 발생할 수 있다. 이러한 문제를 줄이기 위해 View가 직접 갱신 요청을 받는 방식이 아니라, View가 상태를 관찰하고 화면을 갱신하는 MVVM이 사용되기 시작했다. MVVM MVVM(Model View ViewModel)는 View와 Model 사이에 ViewModel을 두어 화면에 필요한 상태와 화면 로직을 관리하는 아키텍처 패턴이다. MVP에서는 Presenter가 View에게 직접 UI 갱신을 요청하는 반면 MVVM에서는 ViewModel이 화면 상태를 제공하고, View는 해당 상태를 관찰하여 스스로 화면을 갱신한다. 구성 요소 Model 애플리케이션의 데이터와 비즈니스 로직을 담당한다. View 사용자에게 화면(UI)을 보여준다. 사용자 입력을 ViewModel에게 전달한다. ViewModel의 상태를 관찰하고 화면에 반영한다. ViewModel 화면에 필요한 상태를 보관한다. 사용자 입력에 따라 Model을 호출한다. View가 관찰할 수 있는 상태를 제공한다. 일반적인 데이터 흐름 MVVM은 다음과 같은 흐름으로 동작한다. 1. 사용자가 View를 통해 입력한다. 2. View는 입력 이벤트를 ViewModel에게 전달한다. 3. ViewModel은 Model을 호출하여 비즈니스 로직을 수행한다. 4. Model은 처리 결과를 ViewModel에게 반환한다. 5. ViewModel은 변경된 상태를 View에게 제공한다. 6. View는 ViewModel의 상태를 관찰하고 화면을 갱신한다. 즉, ViewModel은 View에게 직접 UI 갱신을 명령하지 않고, View가 사용할 상태를 제공한다. Android에서의 MVVM Android에서는 일반적으로 다음과 같이 역할을 대응시킨다. MVVM Android Model Model, Repository, UseCase 등 View Activity / Fragment / XML 기반 UI / Compose ViewModel Jetpack ViewModel Android의 MVVM에서는 ViewModel이 화면 상태를 관리하고, Activity(Fragment) 또는 Compose UI가 이 상태를 관찰한다. XML 기반 UI에서는 Activity(Fragment)가 ViewModel의 상태를 관찰한 뒤 직접 UI를 갱신한다. 반면 Jetpack Compose에서는 상태가 변경되면 Compose가 자동으로 UI를 다시 그린다. 이 때문에 Compose는 상태 중심으로 동작하는 MVVM과 잘 맞는다. 한계 MVVM은 View와 ViewModel의 결합도를 낮추고, 화면 상태를 관리하기 쉽게 만들어준다. 하지만 화면이 복잡해질수록 ViewModel 내부에 많은 로직이 집중될 수 있다. 예를 들어 ViewModel은 다음과 같은 상태를 모두 관리하게 될 수 있다. 로딩 상태 성공 상태 에러 상태 입력값 상태 버튼 활성화 여부 일회성 이벤트 화면 이동 이벤트 상태 관리 기준이 명확하지 않으면 ViewModel이 비대해지고, 데이터 흐름을 추적하기 어려워질 수 있다. 또한 Toast, Dialog, Navigation과 같은 일회성 이벤트를 일반 상태와 함께 관리하면 화면 회전이나 상태 재수집 시 이벤트가 반복 실행될 수 있다. 이러한 문제를 줄이기 위해 사용자 입력을 명확한 Intent로 표현하고, 하나의 상태로 화면을 관리하는 MVI가 사용되기 시작했다. MVI MVI(Model View Intent)는 사용자의 입력을 Intent로 표현하고 이를 처리하여 하나의 상태로 화면을 갱신하는 아키텍처 패턴이다. MVI의 핵심은 단방향 데이터 흐름 이다. View에서 발생한 사용자 입력은 Intent로 전달되고, Intent를 처리한 결과는 새로운 State로 만들어진다. View는 State를 관찰하고, 해당 State에 따라 화면을 렌더링한다. 구성 요소 Model 화면의 상태를 의미한다. 일반적으로 ViewState 또는 UiState로 표현한다. View 사용자에게 화면(UI)을 보여준다. 사용자 입력을 Intent로 변환하여 전달한다. State를 관찰하고 화면을 렌더링한다. Intent 사용자의 의도 또는 액션을 의미한다. 버튼 클릭, 새로고침, 검색어 입력 등이 해당된다. 일반적인 데이터 흐름 MVI는 다음과 같은 흐름으로 동작한다. 1. 사용자가 View를 통해 입력한다. 2. View는 사용자 입력을 Intent로 변환하여 전달한다. 3. ViewModel 또는 Processor는 Intent를 처리한다. 4. 필요한 경우 Model을 호출하여 비즈니스 로직을 수행한다. 5. 처리 결과를 바탕으로 새로운 State를 만든다. 6. View는 State를 관찰하고 화면을 렌더링한다. 즉, MVI는 입력과 상태 변화를 명확히 분리하고, 화면을 하나의 State로 표현한다. Android에서의 MVI Android에서는 일반적으로 다음과 같이 역할을 대응시킨다. MVI Android Model UiState, State View Activity / Fragment / Compose Intent UiIntent, UiEvent Processor ViewModel MVI는 XML 기반 UI에서도 사용할 수 있지만, 상태를 기반으로 화면을 다시 그리는 Jetpack Compose와 특히 잘 맞는다. Compose에서는 State가 변경되면 자동으로 Recomposition이 발생하기 때문에, View는 현재 State에 맞는 UI를 선언하면 된다. 예를 들어 사용자가 구매 버튼을 누르면 BuyClicked 라는 Intent가 ViewModel로 전달된다. ViewModel은 이 Intent를 처리하여 재고를 감소시키거나, 재고가 없을 경우 에러 메시지가 포함된 새로운 State를 만든다. View는 새로운 State를 관찰하고 화면을 다시 렌더링한다. 한계 MVI는 데이터 흐름이 단방향이기 때문에 상태 변화를 추적하기 쉽고, 화면의 현재 상태를 명확하게 표현할 수 있다. 하지만 간단한 화면에서도 Intent, State, Reducer와 같은 구조를 정의해야 하므로 코드가 많아질 수 있다. 또한 모든 화면 상태를 하나의 State로 관리하기 때문에 State 설계를 잘못하면 오히려 ViewModel이 복잡해질 수 있다. 특히 Toast, Dialog, Navigation과 같은 일회성 이벤트는 주의해서 관리해야 한다. 일회성 이벤트를 State에 그대로 포함하면 화면 회전이나 State 재수집 시 같은 이벤트가 다시 실행될 수 있다. 따라서 MVI에서는 지속적으로 유지되어야 하는 화면 상태와 한 번만 소비되어야 하는 이벤트를 구분해서 관리하는 것이 중요하다.
나만의 블로그 구축하기블로그를 만들게 된 이유 지난 4년동안 대부분의 기록을 노션에서 작성해왔다. 대학 공부부터 대외 협력, 프로젝트까지 다양한 내용을 작성해왔고, 가끔 티스토리에 글을 작성하여 공부한 내용을 공유하기도 했다. 하지만 시간이 지나면서 몇 가지 아쉬운 점이 생기기 시작했다. 곧 노션 학교 무료 요금제가 만료됨 내가 공부한 내용이 개인 기록으로만 남아 포트폴리오로 사용하기 어려움 Github이나 블로그로 하려면 같은 글을 여러 번 작성해서 번거로움 결론적으로, 한 번 작성한 기록을 블로그와 학습 노트로 동시에 활용 하고 싶었고, 그러다 보니 개인 블로그가 좋을 것 같다는 생각이 들었다. 이 고민을 해결하기 위해 Markdown기반으로 기록을 관리하고, Github Pages를 통해 직접 운영하는 개발 블로그를 만들게 되었다. 다양한 선택지 Obsidian 노션과 유사한 기록 방식이라 적응이 쉬움 Markdown기반이라 익숙함 로컬에서 작성되어 Github와 연동하기 편리함 블로그 게시글의 원본 문서로 활용 가능함 Github Pages 무료로 운영 가능 원하는 형태로 자유롭게 커스터마이징 가능 Github활동과 연결되어 개발자 포트폴리오로 활용 가능 Astro Markdown파일을 기반으로 게시글 생성 가능 정적 HTML을 생성하여 성능이 뛰어남 블로그 구축에 필요한 기능을 비교적 간단하게 구현 가능 GoatCounter 방문자 통계를 확인하기 위한 도구 개인정보 보호에 친화적인 오픈소스 분석 서비스 복잡한 설정 없이 가볍게 사용 가능 결국 하나의 Markdown 문서를 기준으로 학습 기록과 블로그 게시글을 함께 관리할 수 있도록 위 기술들을 선택하게 되었다. 결론 한 번 작성한 기록이 Github와 블로그에 자연스럽게 반영되는 환경을 구축했다는 점이 매우 만족스럽다. 아직은 기본적인 형태의 블로그이지만, 앞으로 공부한 내용과 프로젝트 경험을 꾸준히 기록할 공간이 될 예정이다. 또한 직접 만든 블로그이기 때문에, 필요한 기능이 생기면 하나씩 추가하고 개선해 나갈 수 있다는 점도 기대된다.
Architecture시스템 아키텍처(system architecture): 시스템의 구성 요소와 구성 요소 간의 관계, 그리고 동작 방식을 정의하는 설계도(청사진)이다.
Coupling결합도(coupling): 한 객체나 모듈이 다른 객체나 모듈에 얼마나 강하게 의존하고 있는지를 나타내는 정도이다.
DataSourceDataSource(데이터 소스): API나 DB 같은 특정 데이터 출처와 직접 통신하며, 데이터를 가져오고 저장하는 기술적인 세부사항을 담당한다.
DIDI(Dependency Injection): 객체가 필요로 하는 의존성을 내부에서 직접 생성하지 않고, 외부에서 주입받도록 하는 것이다.
DIPDIP(Dependency Inversion Principle): 구체적인 구현이 아니라 추상화에 의존하도록 하여, 상위 모듈과 하위 모듈이 모두 추상화에 의존하게 만드는 설계 원칙이다.
Domain ModelDomain Model(도메인 모델): 애플리케이션이 해결하려는 문제 영역의 핵심 개념과 규칙을 표현하는 모델이다.
DSLDSL(Domain-Specific Language, 도메인 특화 언어): 특정 문제 영역의 개념과 규칙을 간결하게 표현하도록 설계된 언어이다.
DTODTO(Data Transfer Object): 계층이나 시스템 사이에서 데이터를 전달하기 위해 사용하는 객체이다.
EntityDomain Entity(도메인 엔티티): 속성 일부가 변하더라도 식별자가 같으면 동일한 대상으로 보는 도메인 객체이다. Room Entity(Room 엔티티): Room에서 데이터베이스 테이블의 구조를 표현하는 저장용 모델이다.
HiltHilt: 안드로이드에서 DI를 쉽게 구현하도록 구글이 공식적으로 권장하는, Dagger 기반의 DI 라이브러리다.
IoCIoC(Inversion of Control): 객체의 생성과 흐름 제어권을 개발자가 아니라 프레임워크가 갖도록 뒤집는 개념이다.
Jank FrameJank Frame(잰크 프레임): UI 렌더링이 정해진 시간 안에 끝나지 못해 화면이 끊기거나 밀리는 프레임이다.
Log Level로그 레벨(Log Level): 로그의 중요도를 나눈 단계다.
Logging로깅(Logging): 서비스가 실제로 어떻게 동작했는지 나중에 확인할 수 있도록 흔적을 남기는 일이다.
MockMock(mock object): 테스트에서 실제 구현 대신 끼워 넣는 가짜 객체로, 특정 메서드가 호출됐는지 등을 검증하는 데 쓰인다.
MVCMVC(Model-View-Controller): 애플리케이션을 Model, View, Controller로 나누고, Controller가 View와 Model 사이를 중재하는 아키텍처 패턴이다.
MVIMVI(Model-View-Intent): 사용자 입력을 Intent로 표현하고, 이를 처리한 결과를 하나의 State로 만들어 단방향으로 화면을 갱신하는 아키텍처 패턴이다.
MVPMVP(Model-View-Presenter): MVC의 Controller 역할을 Presenter로 분리해, Presenter가 View에 UI 갱신을 직접 요청하는 아키텍처 패턴이다.
MVVMMVVM(Model-View-ViewModel): ViewModel이 화면 상태를 제공하고, View가 그 상태를 관찰해 스스로 화면을 갱신하는 아키텍처 패턴이다.
RepositoryRepository(리포지토리): 데이터가 어디에서 오는지 감추고, 애플리케이션에 필요한 데이터를 제공하는 데이터 접근 창구이다.
SDUISDUI(Server Driven UI): 화면을 어떤 UI로 구성할지를 서버가 결정하고, 클라이언트는 서버가 내려준 정의를 해석해 화면을 그리는 방식이다.
Smart CastSmart Cast(스마트 캐스트): is나 != null 같은 검사를 통과하면, 그 블록 안에서는 컴파일러가 변수를 자동으로 좁혀진 타입으로 취급한다.
TTITTI(Time to Interactive, 인터랙션 가능 시점): 페이지에 진입한 순간부터 유저가 의미 있는 인터랙션을 할 수 있을 때까지 걸리는 시간이다.
UI ModelUI Model: 화면을 표시하는 데 필요한 값과 상태를 표현하는 모델이다.
UseCaseUseCase(유스케이스): 애플리케이션에서 수행하는 하나의 행위와 그에 필요한 비즈니스 규칙을 표현한다.
Value ObjectValue Object(값 객체): 별도의 식별자가 아니라 값 자체로 구분되며, 값의 의미와 규칙을 하나의 타입으로 표현하는 객체이다.
ViewModelViewModel: 화면에서 발생한 요청을 처리하고, 화면에 필요한 상태를 관리하여 View에 제공하는 구성요소이다.
검색 결과가 없습니다.
HOME 14
-
0010 - Feature Flag와 AB Test
Feature Flag로 배포와 출시를 분리하고, A/B Test의 그룹 배정·노출 로깅·지표 분석으로 기능의 효과를 검증하는 방법
-
0009 - 로깅
QA와 프로덕션에서 문제를 추적하기 위한 동작 검증 로깅, 비즈니스 메트릭 로깅, 퍼포먼스 로깅과 로그 레벨 정리
-
0008 - Lazy Layout
Jetpack Compose의 지연 목록과 지연 그리드가 필요한 이유와 DSL, 항목 키, 스크롤 상태 정리
-
0007 - Deep Link와 App Link
Android에서 링크가 앱 화면으로 연결되는 원리와 Deep Link, Custom Scheme, 검증된 App Link의 차이 정리
-
0006 - BFF (Backend for Frontend)
하나의 범용 API를 여러 클라이언트가 나눠 쓸 때 생기는 문제와, 화면 전용 백엔드로 조립 책임을 옮기는 BFF 패턴 정리
-
0005 - sealed class로 상태와 에러 표현하기
정해진 몇 가지 중 하나를 표현하는 sealed class(sealed interface)를 이해하고, UI State와 에러 타입에 적용하는 방법 정리
-
0004 - Repository와 데이터 흐름
Repository를 중심으로 UseCase, DataSource, Domain Model 등 주변 구성요소의 역할과 관계 정리
-
0003 - DI
높은 결합도 문제에서 출발해, DIP → DI → IoC로 이어지는 흐름
-
프로그래머스 - [1차] 캐시
| Lv. 2 | 2018 KAKAO BLIND RECRUITMENT
-
프로그래머스 - n^2 배열 자르기
| Lv. 2 | 월간 코드 챌린지 시즌3
-
프로그래머스 - 숫자 야구
| Lv. 3 | 연습문제
-
0002 - SDUI
Server Driven UI의 개념과 구성 요소, 서버-클라이언트의 역할 분리 및 API 응답(스키마) 설계 방법 정리
-
0001 - MVW Architecture
MVC, MVP, MVVM, MVI의 등장 배경과 각 아키텍처 패턴의 특징 및 차이점 정리
-
나만의 블로그 구축하기
Obsidian → GitHub → Astro Blog까지 연결된 기록 시스템
선택한 조건에 등록된 글이 없습니다.