Search
정해진 종류를 타입으로 표현하기 코드를 짜다보면, 이 값은 정해진 몇 가지 중 하나인 경우가 자주 나온다. 화면: 로딩·성공·실패 중 하나 요청 실패: 네트워크·서버·미발견 중 하나 이렇게 종류가 닫혀 있는 값 을 코틀린에서 여러 방법으로 표현할 수 있는데 그중에 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(도메인 모델): 애플리케이션이 해결하려는 문제 영역의 핵심 개념과 규칙을 표현하는 모델이다.
DTODTO(Data Transfer Object): 계층이나 시스템 사이에서 데이터를 전달하기 위해 사용하는 객체이다.
EntityDomain Entity(도메인 엔티티): 속성 일부가 변하더라도 식별자가 같으면 동일한 대상으로 보는 도메인 객체이다. Room Entity(Room 엔티티): Room에서 데이터베이스 테이블의 구조를 표현하는 저장용 모델이다.
HiltHilt: 안드로이드에서 DI를 쉽게 구현하도록 구글이 공식적으로 권장하는, Dagger 기반의 DI 라이브러리다.
IoCIoC(Inversion of Control): 객체의 생성과 흐름 제어권을 개발자가 아니라 프레임워크가 갖도록 뒤집는 개념이다.
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 같은 검사를 통과하면, 그 블록 안에서는 컴파일러가 변수를 자동으로 좁혀진 타입으로 취급한다.
UI ModelUI Model: 화면을 표시하는 데 필요한 값과 상태를 표현하는 모델이다.
UseCaseUseCase(유스케이스): 애플리케이션에서 수행하는 하나의 행위와 그에 필요한 비즈니스 규칙을 표현한다.
Value ObjectValue Object(값 객체): 별도의 식별자가 아니라 값 자체로 구분되며, 값의 의미와 규칙을 하나의 타입으로 표현하는 객체이다.
ViewModelViewModel: 화면에서 발생한 요청을 처리하고, 화면에 필요한 상태를 관리하여 View에 제공하는 구성요소이다.
검색 결과가 없습니다.
HOME 9
-
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까지 연결된 기록 시스템
선택한 조건에 등록된 글이 없습니다.