[디자인 패턴] 구글 아키텍처 - Login 전체 구현

Ju-unn·2026년 5월 23일

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;
    }
    
    // Getter
    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

/**
 * 로그인 요청 데이터
 * - Activity에서 입력받은 이메일/비밀번호를 담아서 전달
 */
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 (인터페이스)

/**
 * 서버 API 통신 인터페이스
 * - 실제로는 Retrofit을 사용하지만, 여기서는 간단히 구현
 */
public interface ApiService {
    /**
     * 로그인 API 호출
     * @param email 이메일
     * @param password 비밀번호
     * @return 로그인 응답
     */
    LoginResponse login(String email, String password);
    
    /**
     * 사용자 정보 조회 API
     * @param userId 사용자 ID
     * @return 사용자 정보
     */
    User getUserInfo(String userId);
}

ApiServiceImpl.java

/**
 * API 통신 실제 구현
 * - 실무에서는 Retrofit + OkHttp 사용
 * - 여기서는 예시로 간단히 구현
 */
public class ApiServiceImpl implements ApiService {
    
    // 실무에서는 실제 서버 URL
    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(),  // userId
                extractNameFromEmail(email),            // userName
                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");
    }
    
    // 이메일에서 이름 추출 (예: junseok@email.com → junseok)
    private String extractNameFromEmail(String email) {
        return email.split("@")[0];
    }
}

4. Data Layer - Local (로컬 저장소)

📄 LocalStorageManager.java

/**
 * 로컬 저장소 관리자
 * - 실무에서는 SharedPreferences, DataStore, Room 사용
 * - 여기서는 메모리에 간단히 저장
 */
public class LocalStorageManager {
    
    // 실제로는 SharedPreferences나 DataStore
    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;
        }
        
        // 예시: 토큰 유효 시간 24시간
        long currentTime = System.currentTimeMillis();
        long tokenAge = currentTime - lastLoginTime;
        long oneDay = 24 * 60 * 60 * 1000; // 24시간
        
        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 (인터페이스)

/**
 * 로그인 관련 데이터 처리 인터페이스
 * - ViewModel은 이 인터페이스만 의존
 * - 구현체가 바뀌어도 ViewModel은 수정 불필요
 */
public interface LoginRepository {
    
    /**
     * 로그인
     */
    LoginResponse login(String email, String password);
    
    /**
     * 로그아웃
     */
    void logout();
    
    /**
     * 현재 로그인된 사용자 정보
     */
    User getCurrentUser();
    
    /**
     * 로그인 상태 확인
     */
    boolean isLoggedIn();
    
    /**
     * 자동 로그인 체크 (토큰 유효성 확인)
     */
    boolean checkAutoLogin();
}

LoginRepositoryImpl.java (구현체)

/**
 * LoginRepository 실제 구현
 * - Remote(API)와 Local(캐시)를 조합
 * - Single Source of Truth 원칙 적용
 */
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] 로그인 처리 시작 =====");
        
        // 1. API 호출
        LoginResponse response = apiService.login(email, password);
        
        // 2. 성공 시 로컬에 저장
        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();
        
        // 실무에서는 서버에도 로그아웃 요청
        // apiService.logout(localStorage.getAuthToken());
        
        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

/**
 * Login 화면의 ViewModel
 * - UI 로직 처리
 * - Repository를 통해 데이터 요청
 * - UI 상태 관리
 */
public class LoginViewModel {
    
    private LoginRepository repository;
    
    // UI 상태
    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] 로그인 요청 <<<<<");
        
        // 1. 로딩 시작
        isLoading = true;
        statusMessage = "로그인 중...";
        
        // 2. 입력값 검증 (UI 레벨)
        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;
        }
        
        // 3. Repository에 로그인 요청
        LoginResponse response = repository.login(email, password);
        
        // 4. 결과 처리
        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;
    }
    
    // Getter 메서드들
    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

/**
 * 로그인 화면 Activity
 * - 사용자 입력 받기
 * - ViewModel에 요청
 * - 결과를 화면에 표시
 */
public class LoginActivity {
    
    private LoginViewModel viewModel;
    
    // UI 요소들 (실제로는 findViewById로 가져옴)
    private String emailInput = "";
    private String passwordInput = "";
    
    /**
     * Activity 생성 (onCreate)
     */
    public void onCreate() {
        System.out.println("\n========================================");
        System.out.println("[Activity] LoginActivity 시작");
        System.out.println("========================================\n");
        
        // 의존성 주입 (실무에서는 Dagger/Hilt 사용)
        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에 위임
        viewModel.login(email, password);
        
        // 결과 확인 및 UI 업데이트
        updateUI();
        
        if (viewModel.isLoggedIn()) {
            showHomeScreen();
        }
    }
    
    /**
     * 로그아웃 버튼 클릭
     */
    public void onLogoutButtonClick() {
        System.out.println("\n[Activity] 로그아웃 버튼 클릭");
        
        viewModel.logout();
        updateUI();
        showLoginScreen();
    }
    
    /**
     * UI 업데이트
     */
    private void updateUI() {
        // 실제로는 TextView, Toast 등으로 표시
        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");
    }
    
    /**
     * Activity 종료 (onDestroy)
     */
    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("╚════════════════════════════════════════╝");
        
        // Activity 생성
        LoginActivity activity = new LoginActivity();
        activity.onCreate();
        
        // 시나리오 1: 잘못된 로그인 시도
        testScenario1(activity);
        
        // 시나리오 2: 올바른 로그인 시도
        testScenario2(activity);
        
        // 시나리오 3: 로그아웃
        testScenario3(activity);
        
        // 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) 전체 흐름

  1. View (Activity): 사용자 입력 → ViewModel 호출
  2. ViewModel: 입력 검증 → Repository 호출
  3. Repository: Local 확인 → Remote 요청 → Local 저장
  4. ViewModel: 결과 받아서 상태 업데이트
  5. View (Activity): 상태 관찰 → UI 업데이트

각 레이어의 책임

  • View: UI 표시, 사용자 입력 받기
  • ViewModel: UI 로직, 상태 관리
  • Repository: 데이터 출처 통합
  • Model: 데이터 구조 정의
profile
한줄한줄 기록해보자

0개의 댓글