1. 전체 구조 복습
프로젝트 구조
LoginApp/
│
├── model/ # 데이터 모델
│ ├── User.java
│ ├── LoginRequest.java
│ └── LoginResponse.java
│
├── data/ # Data Layer
│ ├── remote/
│ │ ├── ApiService.java
│ │ └── ApiServiceImpl.java
│ ├── local/
│ │ └── LocalStorageManager.java
│ └── repository/
│ ├── LoginRepository.java
│ └── LoginRepositoryImpl.java
│
├── ui/ # Presentation Layer
│ ├── LoginViewModel.java
│ └── LoginActivity.java
│
└── Main.java # 테스트
데이터 흐름
User Action (버튼 클릭)
↓
Activity
↓
ViewModel
↓
Repository
↓
┌────┴────┐
Remote Local
(API) (Cache)
└────┬────┘
↓
Repository
↓
ViewModel
↓
Activity
↓
UI Update
2. Model Layer (데이터 모델)
User.java
public class User {
private String userId;
private String userName;
private String email;
public User(String userId, String userName, String email) {
this.userId = userId;
this.userName = userName;
this.email = email;
}
public String getUserId() {
return userId;
}
public String getUserName() {
return userName;
}
public String getEmail() {
return email;
}
@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", userName='" + userName + '\'' +
", email='" + email + '\'' +
'}';
}
}
LoginRequest.java
public class LoginRequest {
private String email;
private String password;
public LoginRequest(String email, String password) {
this.email = email;
this.password = password;
}
public String getEmail() {
return email;
}
public String getPassword() {
return password;
}
}
LoginResponse.java
public class LoginResponse {
private boolean success;
private String message;
private String token;
private User user;
public LoginResponse(boolean success, String message, String token, User user) {
this.success = success;
this.message = message;
this.token = token;
this.user = user;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
public String getToken() {
return token;
}
public User getUser() {
return user;
}
}
3. Data Layer - Remote (API 통신)
ApiService.java (인터페이스)
public interface ApiService {
LoginResponse login(String email, String password);
User getUserInfo(String userId);
}
ApiServiceImpl.java
public class ApiServiceImpl implements ApiService {
private static final String BASE_URL = "https://api.example.com";
@Override
public LoginResponse login(String email, String password) {
System.out.println(" [API] POST " + BASE_URL + "/auth/login");
System.out.println(" [API] Request - email: " + email);
try {
Thread.sleep(1000);
if (!email.contains("@")) {
return new LoginResponse(
false,
"올바른 이메일 형식이 아닙니다",
null,
null
);
}
if (password.length() < 6) {
return new LoginResponse(
false,
"비밀번호는 6자 이상이어야 합니다",
null,
null
);
}
User user = new User(
"user_" + System.currentTimeMillis(),
extractNameFromEmail(email),
email
);
String token = "token_" + System.currentTimeMillis();
System.out.println(" [API] Response - 로그인 성공");
return new LoginResponse(true, "로그인 성공", token, user);
} catch (InterruptedException e) {
System.out.println(" [API] Error - " + e.getMessage());
return new LoginResponse(false, "네트워크 오류", null, null);
}
}
@Override
public User getUserInfo(String userId) {
System.out.println(" [API] GET " + BASE_URL + "/users/" + userId);
return new User(userId, "김준석", "junseok@email.com");
}
private String extractNameFromEmail(String email) {
return email.split("@")[0];
}
}
4. Data Layer - Local (로컬 저장소)
📄 LocalStorageManager.java
public class LocalStorageManager {
private String authToken = null;
private User cachedUser = null;
private long lastLoginTime = 0;
public void saveAuthToken(String token) {
this.authToken = token;
this.lastLoginTime = System.currentTimeMillis();
System.out.println("[Local] 토큰 저장: " + token);
}
public String getAuthToken() {
return authToken;
}
public boolean isTokenValid() {
if (authToken == null) {
return false;
}
long currentTime = System.currentTimeMillis();
long tokenAge = currentTime - lastLoginTime;
long oneDay = 24 * 60 * 60 * 1000;
return tokenAge < oneDay;
}
public void saveUser(User user) {
this.cachedUser = user;
System.out.println("[Local] 사용자 정보 저장: " + user.getUserName());
}
public User getUser() {
return cachedUser;
}
public void clearAll() {
this.authToken = null;
this.cachedUser = null;
this.lastLoginTime = 0;
System.out.println("[Local] 모든 데이터 삭제");
}
public boolean isLoggedIn() {
return authToken != null && cachedUser != null && isTokenValid();
}
}
5. Data Layer - Repository (데이터 통합)
LoginRepository.java (인터페이스)
public interface LoginRepository {
LoginResponse login(String email, String password);
void logout();
User getCurrentUser();
boolean isLoggedIn();
boolean checkAutoLogin();
}
LoginRepositoryImpl.java (구현체)
public class LoginRepositoryImpl implements LoginRepository {
private ApiService apiService;
private LocalStorageManager localStorage;
public LoginRepositoryImpl(ApiService apiService, LocalStorageManager localStorage) {
this.apiService = apiService;
this.localStorage = localStorage;
}
@Override
public LoginResponse login(String email, String password) {
System.out.println("\n===== [Repository] 로그인 처리 시작 =====");
LoginResponse response = apiService.login(email, password);
if (response.isSuccess()) {
localStorage.saveAuthToken(response.getToken());
localStorage.saveUser(response.getUser());
System.out.println("✅ [Repository] 로그인 성공 및 로컬 저장 완료");
} else {
System.out.println("❌ [Repository] 로그인 실패: " + response.getMessage());
}
System.out.println("===== [Repository] 로그인 처리 종료 =====\n");
return response;
}
@Override
public void logout() {
System.out.println("\n===== [Repository] 로그아웃 처리 =====");
localStorage.clearAll();
System.out.println("✅ [Repository] 로그아웃 완료");
System.out.println("===== [Repository] 로그아웃 처리 종료 =====\n");
}
@Override
public User getCurrentUser() {
User user = localStorage.getUser();
if (user != null) {
System.out.println("[Repository] 캐시에서 사용자 정보 반환: " + user.getUserName());
} else {
System.out.println("[Repository] 캐시된 사용자 정보 없음");
}
return user;
}
@Override
public boolean isLoggedIn() {
boolean loggedIn = localStorage.isLoggedIn();
System.out.println("[Repository] 로그인 상태: " + (loggedIn ? "로그인됨" : "로그아웃됨"));
return loggedIn;
}
@Override
public boolean checkAutoLogin() {
System.out.println("\n===== [Repository] 자동 로그인 체크 =====");
if (!localStorage.isTokenValid()) {
System.out.println("❌ [Repository] 토큰 만료 또는 없음");
System.out.println("===== [Repository] 자동 로그인 실패 =====\n");
return false;
}
User user = localStorage.getUser();
if (user == null) {
System.out.println("❌ [Repository] 사용자 정보 없음");
System.out.println("===== [Repository] 자동 로그인 실패 =====\n");
return false;
}
System.out.println("✅ [Repository] 자동 로그인 가능: " + user.getUserName());
System.out.println("===== [Repository] 자동 로그인 성공 =====\n");
return true;
}
}
6. Presentation Layer - ViewModel
LoginViewModel.java
public class LoginViewModel {
private LoginRepository repository;
private String statusMessage = "";
private User currentUser = null;
private boolean isLoading = false;
public LoginViewModel(LoginRepository repository) {
this.repository = repository;
}
public void login(String email, String password) {
System.out.println("\n>>>>> [ViewModel] 로그인 요청 <<<<<");
isLoading = true;
statusMessage = "로그인 중...";
if (email == null || email.trim().isEmpty()) {
statusMessage = "이메일을 입력하세요";
isLoading = false;
System.out.println("❌ [ViewModel] " + statusMessage);
return;
}
if (password == null || password.trim().isEmpty()) {
statusMessage = "비밀번호를 입력하세요";
isLoading = false;
System.out.println("❌ [ViewModel] " + statusMessage);
return;
}
LoginResponse response = repository.login(email, password);
isLoading = false;
statusMessage = response.getMessage();
if (response.isSuccess()) {
currentUser = response.getUser();
System.out.println("✅ [ViewModel] 로그인 성공: " + currentUser.getUserName());
} else {
currentUser = null;
System.out.println("❌ [ViewModel] 로그인 실패: " + statusMessage);
}
System.out.println(">>>>> [ViewModel] 로그인 요청 종료 <<<<<\n");
}
public void logout() {
System.out.println("\n>>>>> [ViewModel] 로그아웃 요청 <<<<<");
repository.logout();
currentUser = null;
statusMessage = "로그아웃 되었습니다";
System.out.println("✅ [ViewModel] 로그아웃 완료");
System.out.println(">>>>> [ViewModel] 로그아웃 요청 종료 <<<<<\n");
}
public boolean checkAutoLogin() {
System.out.println("\n>>>>> [ViewModel] 자동 로그인 체크 <<<<<");
boolean canAutoLogin = repository.checkAutoLogin();
if (canAutoLogin) {
currentUser = repository.getCurrentUser();
statusMessage = "자동 로그인 성공";
System.out.println("✅ [ViewModel] 자동 로그인 성공");
} else {
statusMessage = "로그인이 필요합니다";
System.out.println("ℹ️ [ViewModel] 자동 로그인 불가");
}
System.out.println(">>>>> [ViewModel] 자동 로그인 체크 종료 <<<<<\n");
return canAutoLogin;
}
public String getStatusMessage() {
return statusMessage;
}
public User getCurrentUser() {
return currentUser;
}
public boolean isLoading() {
return isLoading;
}
public boolean isLoggedIn() {
return repository.isLoggedIn();
}
}
7. Presentation Layer - View (Activity)
LoginActivity.java
public class LoginActivity {
private LoginViewModel viewModel;
private String emailInput = "";
private String passwordInput = "";
public void onCreate() {
System.out.println("\n========================================");
System.out.println("[Activity] LoginActivity 시작");
System.out.println("========================================\n");
ApiService apiService = new ApiServiceImpl();
LocalStorageManager localStorage = new LocalStorageManager();
LoginRepository repository = new LoginRepositoryImpl(apiService, localStorage);
viewModel = new LoginViewModel(repository);
if (viewModel.checkAutoLogin()) {
showHomeScreen();
} else {
showLoginScreen();
}
}
public void onLoginButtonClick(String email, String password) {
System.out.println("\n[Activity] 로그인 버튼 클릭");
System.out.println("[Activity] 입력 - email: " + email + ", password: " + password);
viewModel.login(email, password);
updateUI();
if (viewModel.isLoggedIn()) {
showHomeScreen();
}
}
public void onLogoutButtonClick() {
System.out.println("\n[Activity] 로그아웃 버튼 클릭");
viewModel.logout();
updateUI();
showLoginScreen();
}
private void updateUI() {
String message = viewModel.getStatusMessage();
if (viewModel.isLoading()) {
System.out.println("[Activity] UI 업데이트: 로딩 중...");
} else {
System.out.println("[Activity] UI 업데이트: " + message);
}
}
private void showLoginScreen() {
System.out.println("\n========================================");
System.out.println("📱 [Activity] 로그인 화면 표시");
System.out.println("========================================\n");
}
private void showHomeScreen() {
User user = viewModel.getCurrentUser();
System.out.println("\n========================================");
System.out.println("[Activity] 홈 화면 이동");
System.out.println("환영합니다, " + user.getUserName() + "님!");
System.out.println("========================================\n");
}
public void onDestroy() {
System.out.println("\n========================================");
System.out.println("[Activity] LoginActivity 종료");
System.out.println("========================================\n");
}
}
8. 전체 동작 테스트
Main.java
public class Main {
public static void main(String[] args) {
System.out.println("\n");
System.out.println("╔════════════════════════════════════════╗");
System.out.println("║ 구글 아키텍처 Login App 테스트 ║");
System.out.println("╚════════════════════════════════════════╝");
LoginActivity activity = new LoginActivity();
activity.onCreate();
testScenario1(activity);
testScenario2(activity);
testScenario3(activity);
activity.onDestroy();
System.out.println("\n");
System.out.println("╔════════════════════════════════════════╗");
System.out.println("║ 테스트 완료! ║");
System.out.println("╚════════════════════════════════════════╝");
}
private static void testScenario1(LoginActivity activity) {
System.out.println("\n");
System.out.println("┌────────────────────────────────────────┐");
System.out.println("│ 시나리오 1: 잘못된 로그인 시도 │");
System.out.println("└────────────────────────────────────────┘");
activity.onLoginButtonClick("invalid-email", "12345");
sleep(2000);
}
private static void testScenario2(LoginActivity activity) {
System.out.println("\n");
System.out.println("┌────────────────────────────────────────┐");
System.out.println("│ 시나리오 2: 올바른 로그인 시도 │");
System.out.println("└────────────────────────────────────────┘");
activity.onLoginButtonClick("junseok@email.com", "password123");
sleep(2000);
}
private static void testScenario3(LoginActivity activity) {
System.out.println("\n");
System.out.println("┌────────────────────────────────────────┐");
System.out.println("│ 시나리오 3: 로그아웃 │");
System.out<.println("└────────────────────────────────────────┘");
activity.onLogoutButtonClick();
sleep(2000);
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
9. 구글 아키텍처의 핵심 원칙 정리
✅ 1. 관심사의 분리 (Separation of Concerns)
Activity → UI만 담당
ViewModel → UI 로직만 담당
Repository → 데이터 처리만 담당
✅ 2. 단방향 데이터 흐름
User Action → View → ViewModel → Repository → Data Source
↓
UI Update ← View ← ViewModel ← Repository
✅ 3. Single Source of Truth
모든 데이터는 Repository를 통해서만 접근
✅ 4. 생명주기 안정성
ViewModel은 Activity 생명주기와 독립적
→ 화면 회전해도 데이터 유지
핵심 정리
구글 아키텍처(MVVM) 전체 흐름
- View (Activity): 사용자 입력 → ViewModel 호출
- ViewModel: 입력 검증 → Repository 호출
- Repository: Local 확인 → Remote 요청 → Local 저장
- ViewModel: 결과 받아서 상태 업데이트
- View (Activity): 상태 관찰 → UI 업데이트
각 레이어의 책임
- View: UI 표시, 사용자 입력 받기
- ViewModel: UI 로직, 상태 관리
- Repository: 데이터 출처 통합
- Model: 데이터 구조 정의