[Flutter] 면접 질문을 통해 알아보는 플러터 (2) - 개념

2025. 3. 4. 06:35Knowledge/Flutter

https://kwonputer.tistory.com/573

 

[ Flutter ] 면접 질문을 통해 알아보는 플러터 (3) - Flutter 기술 & 경험

※ 아직 포스트를 작성하는 중입니다. ※ 아래의 '개념'에 대한 포스트를 읽고 오시면 이해하기 더 쉽습니다.https://kwonputer.tistory.com/571 [ Flutter ] 면접 질문을 통해 알아보는 플러터 (1) - 목차안

kwonputer.tistory.com

 
 
안녕하세요!
이번 포스트에서는 Flutter보다는 '개념'에 대한 면접 질문에 대해서 다루겠습니다.
실제 면접이라고 생각하고 포스트를 작성해 보겠습니다.
주관적인 생각을 바탕으로 작성한 포스트라서 참고만 부탁드립니다.
정확한 정보전달이 아닐 수도 있습니다.
 
아래의 목차를 참고해 주세요.

 
1. 알고 있는 디자인 패턴에 대해서 말해주세요.
2. MVC, MVP, MVVM 패턴의 차이점을 말해주세요.
3. 싱글톤 디자인 패턴의 장점과 단점에 대해 말해주세요.
4. 클린 아키텍처(Clean Architecture)를 반드시 사용해야 하는 이유를 말해주세요.
5. 의존성 역전에 대해 말해주세요.
6. 스레드와 프로세스, 멀티 스레드와 멀티 프로세스에 대해 말해주세요.
7. 이미지 캐시(Memory, Disk)에 대해 말해주세요.
8. 해시(Hash)에 대해 말해주세요.
9. 대칭 키와 비대칭 키에 대해 말해주세요.
10. 동기(Synchronous)와 비동기(Asynchronous)에 대해 말해주세요.
11. 접근 토큰(Access Token)과 갱신 토큰(Refresh Token)에 대해 말해주세요.
12. TCP와 UDP에 대해 말해주세요.
13. Git의 Merge와 Rebase의 차이점에 대해 말해주세요.
14. GraphQL에 대해 말해주세요.
15. CI/CD에 대해 말해주세요.
16. 선언형 & 명령형 & 함수형 프로그래밍에 대해 말해주세요.
17. 메모리 누수(Memory Leak)에 대한 설명과 방지하는 방법에 대해 말해주세요.
18. BDD & TDD의 차이점에 대해 말해주세요.
19. SDK 개발과 서비스 개발의 차이점에 대해 말해주세요.
20. HTTP와 HTTPS의 차이점에 대해 말해주세요.
21. HTTPS의 SSL Handshaking에 대해 말해주세요.


1. 알고 있는 디자인 패턴에 대해서 말해주세요.
└ 기억나는 건 디자인 패턴으로는 싱글톤 패턴과 [1]빌더 패턴이 있고, 아키텍처 패턴으로는 MVC, MVP, MVVM, Clean Architecture가 있네요.
 
* [1]빌더 패턴: 동일한 생성 과정으로 서로 다른 표현의 결과를 만들 수 있게 해주는 패턴입니다. 가독성이 좋습니다.

// 생성자 사용 (가독성 떨어짐)
User user = User("John", "Doe", 30, "john@example.com", "123 Street", null, true);

// 빌더 패턴 사용 (가독성 좋음)
User user = UserBuilder()
    .firstName("John")
    .lastName("Doe")
    .age(30)
    .email("john@example.com")
    .address("123 Street")
    .isActive(true)
    .build();

2. MVC, MVP, MVVM 패턴의 차이점을 말해주세요.
└ MVC, MVP, MVVM, Clean Architecture는 모두 [1]관심사 분리(SoC)를 위한 아키텍처 패턴입니다. MVC, MVP, MVVM의 차이점은 [2]컴포넌트 간의 통신 방식이라고 생각합니다.
MVC는 Model(데이터) & View(UI) & Controllor(비즈니스 로직)로 구성됩니다. 컨트롤러가 모델을 변경하고 모델이 변경되면 뷰를 업데이트하는 방식입니다. Flutter에서는 Widget을 View라고 볼 수 있고, State를 Model로 볼 수 있겠네요.
 
MVP는 Model(데이터) & View(UI) & Presenter(비즈니스 로직)로 구성됩니다. View가 사용자에게 입력을 받으면 Presenter에 전달하고, Presenter가 Model과 통신한 후에 다시 View를 업데이트하는 구조입니다. MVP의 핵심은 View와 Model은 서로 모른다는 점입니다.
 
MVVM은 Model(데이터) & View(UI) & ViewModel(비즈니스 로직)로 구성됩니다. ViewModel이 상태를 감지하면 View에 알려서 View를 업데이트하고, View가 사용자에게 입력을 받으면 이를 ViewModel에 알려서 ViewModel이 Model과 통신해서 데이터를 변경하고 변경된 데이터를 View에게 알려서 View를 업데이트하는 구조입니다. MVVM의 핵심은 View와 Model이 서로 모른다는 점과 [3]데이터 바인딩입니다.
 
즉, MVC & MVP & MVVM은 모두 비슷합니다. 똑같은 목적을 가지고 있기 때문입니다. 처음에 말한 관심사 분리(SoC)를 위해서 더 효율적인 방식을 찾다가 나온 아키텍처 패턴입니다.
 
주관적인 생각이지만, 디자인 패턴이란 어떠한 목적을 위해서 더 효율적인 코드를 생각하고 고민해서 적용하고 찾아내서 이름을 붙이는 거로 생각합니다. 그렇기에, 검색해서 나오는 디자인 패턴들은 과거의 영광이라고 부를 수도 있을 것 같습니다. 이를 무시하거나 깎아내리려는 의도가 아닙니다. 다만, 과거의 영광도 기억해야 하지만, 꾸준히 더 나은 방식을 찾아내기 위해 노력을 해야, 본인만의 새로운 디자인 패턴을 만들 수 있을거라는 생각을 합니다. 
 
(클린 아키텍처도 이미 한참 옛날에 나온 이론입니다. 이제는 IT 기기의 성능이 아주 좋아졌기 때문에 지금에 와서야 적용이 되는 거로 생각합니다. 사실, 과거의 영광만을 쫓아도 벅차고 충분합니다. 다만, 항상 마음가짐으로 향상심을 가지고 있어야 새로운 지식을 습득하는 데에 거부감이 적어지는 것 같습니다.)
 
 
* [1] 관심사 분리(SoC): 각각의 레이어의 독립성을 확보하고 의존성을 줄이기 위함을 뜻합니다.
* [2] 컴포넌트: MVC로 예를 들면, Model & View & Controllor가 레이어가 되고, 그 안의 파일들이 컴포넌트라고 할 수 있습니다. 레이어가 더 큰 범위라고 생각하시면 됩니다.
* [3] 데이터바인딩: 데이터가 변경될 때, 자동으로 UI가 업데이트되는 메커니즘이라고 생각하시면 됩니다. Android의 Observer 패턴을 생각하시면 됩니다.
 
 


3. 싱글톤 디자인 패턴의 장점과 단점에 대해 말해주세요.
└ 싱글톤 디자인 패턴의 장점으로는 단일 인스턴스를 보장하기 때문에 메모리 사용을 줄여줍니다. 그리고 인스턴스 생성 비용이 큰 경우에 효율적입니다. 또한, 전역 접근을 제공하기 때문에 데이터 공유가 쉽습니다.
단점으로는 의존성 주입이 어렵기 때문에 [1]단위 테스트가 어렵습니다. 멀티 스레드 환경에서 동기화 문제가 발생할 가능성이 높습니다.
 
멀티 스레드 환경에서 2개의 스레드(A, B)가 싱글톤 객체의 카운터 변수인 int counter = 0을 counter++를 통해 1을 증가시켰을 때, 원하는 결과값인 2이지만, 실제 결과값은 1이 나옵니다.
아래는 "counter++"의 연산이 이루어지는 과정입니다.

초기 counter 값: 0

1. 스레드 A: counter 값(0)을 읽음
2. 스레드 B: counter 값(0)을 읽음 (아직 A가 값을 업데이트하기 전)
3. 스레드 A: 0 + 1 = 1 계산
4. 스레드 B: 0 + 1 = 1 계산
5. 스레드 A: 값 1을 메모리에 씀
6. 스레드 B: 값 1을 메모리에 씀 (B는 A가 이미 쓴 값을 덮어씀)

최종 counter 값: 1 (원하는 값 2가 아님)

 
단위 테스트를 하기 위해서는 모의 객체(Mock)가 필요한데, 싱글톤은 클래스 내부에서 직접 생성되고 접근하기 때문에 외부에서 다른 구현체로 교체하기가 어렵습니다. 즉, 모의 객체(Mock)를 사용하기가 어렵습니다.
 
* [1] 단위 테스트: 코드의 가장 작은 단위([2]함수 & 메서드 & 클래스)를 격리해서 해당 단위만 정확하게 작동하는지 검증하는 테스트를 말합니다. 단위 테스트에는 핵심 원칙이 있습니다. 다른 외부 요소(네트워크, 데이터베이스, 파일 시스템 등)에 의존하지 않고 독립적으로 실행되어야 합니다. 그렇기에 실제 데이터가 아닌 모의 객체(Mock)를 사용합니다.
* [2] 함수 & 메서드: 함수는 독립적으로 사용할 수 있는 코드를 말하고, 메서드는 클래스 내부에서 변수를 활용해 구현된 코드라고 보시면 됩니다.
 


4. 클린 아키텍처(Clean Architecture)를 반드시 사용해야 하는 이유를 말해주세요.
└ MVC, MVP, MVVM과의 차이점이 있다면 외부 프레임워크나 라이브러리 변경에도 핵심 비즈니스 로직을 보호할 수 있습니다. 또한, 프로젝트의 테스트 용이성 & 유지 보수성 & 확장성이 높아집니다. 
● 의존성 역전을 통해 의존성의 방향이 항상 외부에서 내부로 향하기 때문에 핵심 비즈니스 로직이 외부 변화에 영향을 받지 않습니다.
● 관심사 분리(SoC)로 코드 이해와 유지보수가 쉬워집니다. (어느 프로젝트를 열어도 같은 구조와 패턴이기 때문입니다)
● 각 계층(data, domain, presenter)별로 독립적인 테스트가 가능합니다.
●  프레임워크 & 라이브러리의 교체가 용이합니다.
 
 


5. 의존성 역전에 대해 말해주세요.
└ 의존성 역전의 핵심은 서로 의존해서는 안 되며, 추상화에 의존해야 한다는 점입니다. 예를 들어, 비즈니스 로직 클래스가 데이터베이스 클래스에 직접 의존하게 되면, 데이터베이스 변경 시, 비즈니스 로직도 수정해야 합니다. 하지만, 인터페이스(추상화)를 통해 의존성을 역전시키면, 비즈니스 로직은 변경 없이 다양한 데이터 베이스 구현체를 사용할 수 있습니다.
 
 


6. 스레드와 프로세스, 멀티 스레드와 멀티 프로세스에 대해 말해주세요.
└ 프로세스는 독립적인 실행 단위이고, 스레드는 프로세스 내부에서 실행되는 더 작은 실행 단위입니다.
 
[1]프로세스는 독립적인 실행 단위이며, 자체 메모리 공간과 시스템 자원을 가집니다. 그리고 각 프로세스는 서로 격리되어 있어 한 프로세스의 문제가 다른 프로세스에 영향을 주지 않습니다.
 
[2]스레드는 프로세스 내부에서 실행되는 더 작은 실행 단위이며, 같은 프로세스 내의 스레드들은 서로 메모리와 자원을 공유합니다. 이에 따라 스레드 간 통신이 더 효율적이지만, 한 스레드의 오류가 전체 프로세스에 영향을 줄 수 있습니다.
 
[4]멀티 프로세스는 여러 프로세스를 동시에 실행하는 방식이며, 높은 안정성을 제공합니다. 하지만, 프로세스 간 통신 비용이 큽니다.
 
[3]멀티 스레드는 하나의 프로세스 내에서 여러 스레드를 동시에 실행하는 방식이며, 자원 공유가 효율적이지만 동기화 문제가 발생할 가능성이 있습니다.
 
Flutter로 추가 설명을 해보겠습니다.
* [1]프로세스

// 앱 실행 코드
void main() {
  runApp(MyApp());  // 이 코드가 실행되면 앱 프로세스가 시작됩니다
}

 
Flutter 앱 자체가 하나의 프로세스라고 볼 수 있습니다. AOS & IOS에서 Flutter로 만든 앱이 실행될 때, 운영체제(OS)는 해당 앱에 대한 별도의 프로세스를 할당합니다.
 
* [2]스레드

// UI 업데이트 - 메인 스레드에서 실행
setState(() {
  counter++;  // 이 코드는 메인 스레드에서 실행됩니다
});

 
Flutter에서 기본적으로 모든 코드는 메인 스레드(UI 스레드)에서 실행됩니다. 
● 모든 UI 업데이트
● 애니메이션
● 사용자 입력 처리
● ...등등
 
* [3]멀티 스레드

import 'package:flutter/foundation.dart';

// 복잡한 연산을 수행하는 함수
int calculateFactorial(int n) {
  int result = 1;
  for (int i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

// 앱에서 사용
void onButtonPressed() async {
  // 별도의 Isolate에서 계산 실행
  final result = await compute(calculateFactorial, 20);
  setState(() {
    factorialResult = result;
  });
}

 
Flutter에서는 전통적인 스레드 대신에 Isolate를 사용합니다. Isolate는 자체 메모리를 가진 별도의 실행 환경으로 다른 Isolate와 메모리를 공유하지 않는다는 점이 특징입니다.
또한, Flutter는 복잡한 Isolate 설정을 간소화하기 위해 compute 함수를 제공합니다.
● 대용량 JSON 파싱
● 이미지 처리/필터링
● 복잡한 연산(암호화, 해시 계산... 등)
● 대용량 파일처리
● ...등등
 
* [4]멀티 프로세스

// Flutter 측 코드
const platform = MethodChannel('com.example.app/background_service');

Future<void> startBackgroundService() async {
  try {
    await platform.invokeMethod('startService');
    print('백그라운드 서비스 시작됨');
  } catch (e) {
    print('에러: $e');
  }
}

// Android에서 별도의 서비스(프로세스) 실행 (Kotlin 코드)
// Android 측 코드 (MainActivity.kt)
/*
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
  super.configureFlutterEngine(flutterEngine)
  
  MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/background_service")
    .setMethodCallHandler { call, result ->
      if (call.method == "startService") {
        val intent = Intent(this, BackgroundService::class.java)
        startService(intent)
        result.success(null)
      } else {
        result.notImplemented()
      }
    }
}
*/

 
Flutter는 멀티 프로세스를 지원하지 않지만, 플랫폼 채널(Platform Channels)을 통해 다른 프로세스와 통신을 할 수 있습니다.
● 위치 추적 백그라운드 서비스
● 음악 재생 서비스
● 푸시 알림 처리
● 지속적인 네트워크 모니터링
● ...등등
 
 


7. 이미지 캐시(Memory, Disk)에 대해 말해주세요.
└ 이미지 캐시는 앱의 성능을 향상시키기 위해 이미지를 메모리나 디스크에 저장하는 최적화 기술입니다.
 
메모리 캐시는 RAM에 이미지를 저장하는 방식으로, 접근 속도가 매우 빠르지만, 용량이 제한적이고 앱이 종료되면 데이터가 사라집니다.
주로 현재 화면에 표시되는 이미지나 자주 사용되는 이미지를 저장할 때 사용합니다.
 
디스크 캐시는 기기의 저장소에 이미지를 저장하는 방식으로, 메모리보다 용량이 크고 앱이 재시작되어도 데이터가 유지됩니다. 다만 접근 속도가 메모리보다 느립니다.
 
 


8. 해시(Hash)에 대해 말해주세요.
└ 해시는 임의의 크기를 가진 데이터를 고정된 크기의 값으로 변환하는 함수입니다.
 
해시 함수의 주요 특성은 일방향성입니다. 해시값에서 원본 데이터를 복원하는 것은 불가능에 가깝습니다. 또한, 입력값이 조금만 달라져도 출력값이 크게 달라지는 특성(눈사태 효과)을 가집니다.
 
먼저, 해시를 좀 더 쉽게 설명해 보겠습니다.
해시는 어떤 데이터든지 특정 '지문'처럼 고유한 값으로 변환하는 과정을 말합니다.
1. 지문 생성기: 여러분이 어떤 사람(데이터)이든 그 사람의 지문(해시값)을 추출하는 기계를 가지고 있습니다.
2. 크기 통일: 키가 190cm인 사람이든 키가 150cm인 사람이든 상관없이, 지문은 항상 일정한 크기로 나옵니다.
3. 고유성: 다른 사람이면 다른 지문이 나옵니다.
 
다음으로 "임의의 크기를 가진 데이터를 고정된 크기의 값으로 변환"을 좀 더 쉽게 설명해 보겠습니다.

"안녕하세요" → a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
"긴 소설 전체..." → 다른 64글자 해시값
"대용량 동영상..." → 또 다른 64글자 해시값

 
해시 함수는 모든 다양한 크기의 데이터를 입력받아서, 항상 동일한 길이(ex. 256비트 & 64글자)의 문자열이나 숫자로 변환합니다. 이것이 바로 "임의의 크기 -> 고정된 크기"를 뜻합니다.
 
해시에 대한 Dart 예시를 보여드릴게요.

import 'dart:convert';
import 'package:crypto/crypto.dart';

void main() {
  // 다양한 길이의 입력 문자열
  String shortText = "안녕하세요";
  String mediumText = "이것은 중간 길이의 텍스트입니다. 개발하는 것은 재미있습니다.";
  String longText = "아주 긴 텍스트..." * 1000; // 매우 긴 텍스트
  
  // SHA-256 해시 계산 (항상 64글자 16진수 문자열 생성)
  String shortHash = sha256.convert(utf8.encode(shortText)).toString();
  String mediumHash = sha256.convert(utf8.encode(mediumText)).toString();
  String longHash = sha256.convert(utf8.encode(longText)).toString();
  
  print("짧은 텍스트 해시 (길이: ${shortHash.length}): $shortHash");
  print("중간 텍스트 해시 (길이: ${mediumHash.length}): $mediumHash");
  print("긴 텍스트 해시 (길이: ${longHash.length}): $longHash");
}
짧은 텍스트 해시 (길이: 64): a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
중간 텍스트 해시 (길이: 64): 5d41402abc4b2a76b9719d911017c592
긴 텍스트 해시 (길이: 64): 7b502c3a1f48c8609ae212cdfb639dee

 
모든 결과는 길이가 동일하며, 입력 데이터가 다르다면 출력 해시값도 완전히 다릅니다. 
 
해시의 대표적인 실제 활용 사례로 5개를 알려드리겠습니다.
1. 비밀번호 저장
● 실제 비밀번호를 저장하지 않고 해시값만 저장
● 사용자가 로그인할 때, 입력한 비밀번호의 해시값과 저장된 해시값을 비교
 
2. 데이터 무결성 검증
● 파일 다운로드 후, 해시값을 계산하여 원본과 비교
● 데이터가 변조되었는지 확인 가능
 
3. Flutter에서 위젯 키 관리

class MyWidget extends StatelessWidget {
  final String id;
  
  MyWidget({required this.id}) : super(key: ValueKey(id));
  // id 문자열이 해시 함수를 통해 고유한 키 값으로 변환
  
  @override
  Widget build(BuildContext context) {
    return Container(/* ... */);
  }
}

 
4. 캐싱 시스템

String getCacheFileName(String url) {
  return sha256.convert(utf8.encode(url)).toString();
}

 
● URL이나 키를 해시하여 캐시 파일 이름으로 사용
 
5. 해시 테이블(HashMap)

final Map<String, User> userCache = {};
// 여기서 String 키는 내부적으로 해시값을 사용하여 데이터를 효율적으로 찾음

 
● 빠른 검색을 위한 자료 구조
 
위에 적힌 5개의 실제 사례를 보시면 해시의 특성이 이해하기 쉬워집니다.
* 해시의 특성
● 일방향성: 해시값에서 원래 데이터를 복원할 수 없음
● 결정성: 같은 입력은 항상 같은 해시값 생성
● 눈사태 효과: 입력이 조금만 달라도 출력은 완전히 달라짐
● 고속 계산: 큰 데이터라도 빠르게 해시값 계산 가능
 
그리고 해시에는 단점이 있습니다. 그건 바로 '해시 충돌'입니다. 서로 다른 두 입력이 동일한 해시값을 생성하는 경우를 '충돌'이라고 합니다. 해시 공간은 유한하지만, 가능한 입력은 무한하기 때문에 충돌은 이론적으로 피할 수 없습니다.
Dart의 Map이나 Set에서는 이러한 충돌을 내부적으로 처리할 수 있습니다.

class Person {
  final String name;
  final int age;
  
  Person(this.name, this.age);
  
  // 해시 코드 재정의
  @override
  int get hashCode => name.hashCode ^ age.hashCode;
  
  // 동등성 비교 재정의
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Person && other.name == name && other.age == age;
  }
}

 
hashCode는 두 속성의 해시값을 XOR 연산하여 생성합니다. 충돌이 발생해도 '=='연산자를 통해 추가 확인을 수행합니다.
 
 


9. 대칭 키와 비대칭 키에 대해 말해주세요.
└ 대칭 키는 하나의 키로 암호화 & 복호화를 모두 수행하고, 비대칭 키는 공개 키와 개인 키를 쌍으로 사용하여 더 높은 보안을 제공합니다.
 
대칭 키 암호화는 동일한 키를 사용하여 데이터를 암호화하고 복호화하는 방식입니다. 처리 속도가 빠르고 구현이 간단하지만, 통신 당사자 간에 안전하게 키를 공유하는 것이 중요합니다. 대표적으로 [1]DES, [2]AES가 있습니다.
 
비대칭 키 암호화는 공개 키와 개인 키라는 두 개의 서로 다른 키를 사용합니다. 공개 키로 암호화한 데이터는 개인 키로만 복호화할 수 있고, 개인 키로 암호화한 데이터는 공개 키로만 복호화할 수 있습니다. 대표적으로 [3]RSA, [4]ECC가 있습니다.
 
실제 애플리케이션에서는 대칭 키와 비대칭 키의 장점을 결합한 하이브리드 접근 방식도 사용합니다. 비대칭 키로 세션 키(대칭 키)를 안전하게 교환한 후, 실제 데이터 암호화에는 더 빠른 대칭 키를 사용하는 방식입니다.
 
* [1]DES: 아주 예전에 개발된 초기 표준 암호화 알고리즘입니다. 56비트 키를 사용하여 64비트 블록 단위로 데이터를 암호화합니다. 현재는 키 길이가 짧아서 안전하지 않다고 간주하여 거의 사용하지 않습니다.
* [2]AES: 현재 가장 널리 사용되는 대칭 키 알고리즘입니다. 128 & 192 & 256비트 키 길이를 사용합니다. 데이터를 블록 단위로 처리하며 매우 효율적으로 동작합니다. 모바일 앱에서는 로컬 데이터 암호화에 주로 사용합니다.
* [3]RSA: 현재 가장 널리 사용되는 비대칭 키 알고리즘입니다. 1024 & 2048 & 4096비트 키 길이를 사용합니다. 디지털 서명과 키 교환에 주로 사용합니다. 다만, 연산이 복잡하여 대칭 키보다 속도가 느립니다.
* [4]ECC: RSA보다 짧은 키 길이로도 동등한 보안 수준을 제공(256비트 ECC = 3072비트 RSA)합니다. 모바일 환경에 적합한 경량 암호화 방식입니다. RSA보다 빠른 속도와 낮은 자원을 사용합니다.
 
* ECC보다 RSA를 많이 사용하는 이유: RSA는 오래전에 개발되어 광범위하게 분석되고 검증되었습니다. 보안 시스템에서 신뢰도와 검증 기간은 매우 중요한 요소입니다. RSA는 ECC보다 상대적으로 구현이 쉽습니다. 또한, 대부분의 개발자가이 이미 RSA에 익숙합니다.
 
* Flutter에서 대칭 키 알고리즘 AES 사용 예시

import 'package:encrypt/encrypt.dart';
import 'package:encrypt/encrypt_io.dart';

void main() {
  // 키 생성 (256비트 = 32바이트)
  final key = Key.fromLength(32);
  final iv = IV.fromLength(16);  // 초기화 벡터
  
  // 암호화할 평문
  final plainText = 'Flutter에서 AES 암호화 예제입니다';
  
  // 암호화
  final encrypter = Encrypter(AES(key));
  final encrypted = encrypter.encrypt(plainText, iv: iv);
  
  print('암호화된 텍스트: ${encrypted.base64}');
  
  // 복호화
  final decrypted = encrypter.decrypt(encrypted, iv: iv);
  print('복호화된 텍스트: $decrypted');
}

 
* Flutter에서 비대칭 키 알고리즘 RSA 사용 예시

import 'package:fast_rsa/fast_rsa.dart';

Future<void> main() async {
  // 키 쌍 생성
  final keyPair = await RSA.generate(2048);
  
  // 암호화할 평문
  final plainText = '비대칭 키 암호화 예제입니다';
  
  // 공개 키로 암호화
  final encrypted = await RSA.encryptPKCS1v15(plainText, keyPair.publicKey);
  print('암호화된 텍스트: $encrypted');
  
  // 개인 키로 복호화
  final decrypted = await RSA.decryptPKCS1v15(encrypted, keyPair.privateKey);
  print('복호화된 텍스트: $decrypted');
}

 
* Flutter에서 하이브리드 암호화 구현 예시

import 'dart:convert';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
import 'package:pointycastle/random/fortuna_random.dart';

Future<void> hybridEncryptionExample() async {
  // 1. RSA 키 쌍 생성 (실제로는 서버의 공개 키를 사용)
  final keyPair = await generateRSAKeyPair();
  final publicKey = keyPair.publicKey as RSAPublicKey;
  final privateKey = keyPair.privateKey as RSAPrivateKey;

  // 2. 임시 AES 세션 키 생성
  final sessionKey = Key.fromSecureRandom(32); // 256비트 AES 키
  final iv = IV.fromSecureRandom(16);

  // 3. 데이터 준비
  final message = 'Flutter 앱에서 중요한 데이터를 안전하게 전송하는 예제';

  // 4. 세션 키로 데이터 암호화 (AES - 대칭 암호화)
  final aesEncrypter = Encrypter(AES(sessionKey));
  final encryptedData = aesEncrypter.encrypt(message, iv: iv);

  // 5. 공개 키로 세션 키 암호화 (RSA - 비대칭 암호화)
  final rsaEncrypter = Encrypter(RSA(publicKey: publicKey));
  final encryptedSessionKey = rsaEncrypter.encrypt(base64.encode(sessionKey.bytes));

  print('암호화된 데이터: ${encryptedData.base64}');
  print('암호화된 세션 키: ${encryptedSessionKey.base64}');

  // --- 수신자 측 (서버 또는 다른 앱) ---

  // 6. 개인 키로 세션 키 복호화
  final rsaDecrypter = Encrypter(RSA(privateKey: privateKey));
  final decryptedSessionKeyStr = rsaDecrypter.decrypt(encryptedSessionKey);
  final decryptedSessionKey = Key(base64.decode(decryptedSessionKeyStr));

  // 7. 복호화된 세션 키로 데이터 복호화
  final aesDecrypter = Encrypter(AES(decryptedSessionKey));
  final decryptedData = aesDecrypter.decrypt(encryptedData, iv: iv);

  print('복호화된 데이터: $decryptedData');
}

// RSA 키 쌍 생성 함수
Future<AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>> generateRSAKeyPair() async {
  // RSA 키 생성 로직...
}

 
하이브리드 암호화 방식은 크게 5가지의 단계로 진행됩니다.
1. 임시 대칭 키 생성: 통신할 때마다 새로운 임시 대칭 키(세션 키)를 생성합니다. 이 키는 AES와 같은 대칭 암호화에 사용됩니다.
2. 세션 키 암호화: 수신자의 공개 키를 사용하여 세션 키를 암호화합니다. 이렇게 암호화된 세션 키는 수신자의 개인 키로만 복호화할 수 있습니다.
3. 데이터 암호화: 실제 데이터는 생성된 세션 키를 사용하여 대칭 암호화로 처리합니다.
4. 데이터 전송: 암호화된 세션 키와 암호화된 데이터를 모두 전송합니다.
5. 수신자 측 복호화: 수신자는 자신의 개인 키로 세션키를 복호화합니다. 그리고 복호화된 세션 키를 사용하여 실제 데이터를 복호화합니다.
 
대표적으로 하이브리드 암호화를 사용한 실제 사례는 HTTPS 통신입니다.
핸드셰이크 단계와 데이터 교환 단계를 통해 하이브리드 암호화를 수행합니다.
면접 질문에 HTTPS SSL Handshaking이 있어서 자세한 건 아래에서 설명하겠습니다.
 
 


10. 동기(Synchronous)와 비동기(Asynchronous)에 대해 말해주세요.
└ 동기(Synchronous) 코드는 순차적으로 실행되며 작업이 완료될 때까지 다음 코드가 실행되지 않고, 비동기(Asynchronous) 코드는 작업 완료를 기다리지 않고 다음 코드를 실행할 수 있습니다.
 
동기(Synchronous) 처리는 코드가 순차적으로 실행되며, 하나의 작업이 완료된 후에 다음 작업이 시작됩니다. 이는 코드 흐름을 예측하기 쉽지만, 시간이 오래 걸리는 작업(네트워크 요청, 파일 입출력... 등)이 있으면 전체 프로그램이 차단(blocking)될 수 있습니다.
 
비동기(Asynchronous) 처리는 작업 완료를 기다리지 않고 다음 코드를 실행할 수 있게 해주며, 작업이 완료되면 콜백이나 기타 메커니즘을 통해 결과를 처리합니다. 이는 UI 응답성을 유지하고 자원을 효율적으로 사용할 수 있게 해줍니다.
 
Flutter에서는 [1]Future[2]async & await을 통해 비동기 프로그래밍을 지원합니다.
* [1]Future

// Future 객체의 기본 형태
Future<String> fetchUserName() {
  // 서버에서 사용자 이름을 가져오는 비동기 작업
  return Future.delayed(
    Duration(seconds: 2),
    () => "John Doe"  // 2초 후에 이 값을 반환
  );
}

 
Future는 비동기 작업의 결과를 나타내는 객체입니다. 이것은 "미래의 어느 시점에 완료될 값 또는 에러"를 나타냅니다.
 

void handleFutureWithCallbacks() {
  fetchUserName()
    .then((userName) {
      // 성공적으로 값을 받았을 때 실행
      print("사용자 이름: $userName");
    })
    .catchError((error) {
      // 에러가 발생했을 때 실행
      print("에러 발생: $error");
    })
    .whenComplete(() {
      // 성공이든 실패든 완료되었을 때 실행
      print("작업 완료");
    });
}

 
Future를 처리하는 방법은 크게 두 가지가 있습니다. 첫 번째는 then & catchError & whenComplete 메서드를 사용하는 방법입니다. 이 방식은 콜백 기반 접근법으로, 여러 비동기 작업을 연결할 때, 코드가 복잡해질 수 있습니다. (콜백 지옥)
두 번째는 [2]async & await 방법입니다.
 
* [2]async & await

// async 키워드로 함수를 비동기 함수로 표시
Future<void> handleFutureWithAsyncAwait() async {
  try {
    // await 키워드로 Future의 완료를 기다림
    String userName = await fetchUserName();
    print("사용자 이름: $userName");
  } catch (error) {
    print("에러 발생: $error");
  } finally {
    print("작업 완료");
  }
}

 
async & await 방식을 사용하면 비동기 코드를 마치 동기 코드처럼 직관적으로 작성할 수 있습니다. 코드 흐름을 따라가기 쉽고 오류 처리도 try-catch 구문으로 간단하게 처리할 수 있습니다.
 
 


11. 접근 토큰(Access Token)과 갱신 토큰(Refresh Token)에 대해 말해주세요.
└ 접근 토큰(Access Token)은 보호된 리소스에 접근 권한을 부여하는 단기 자격 증명이며, 갱신 토큰(Refresh Token)은 접근 토큰이 만료되었을 때 새로운 접근 토큰을 발급받기 위한 장기 자격 증명입니다.
 
접근 토큰(Access Token)은 클라이언트가 서버의 보호된 리소스에 접근할 수 있는 권한을 나타내는 문자열입니다. 일반적으로 짧은 유효 기간(보통 몇 분 ~ 몇 시간)을 가지며, API 요청 시에 Authorization 헤더에 포함되어 사용자 인증을 대신합니다. 주로 [1]JWT(JSON Web Token) 형식을 사용합니다.
 
갱신 토큰(Refresh Token)은 접근 토큰이 만료되었을 때 새로운 접근 토큰을 발급받기 위해 사용하는 특별한 토큰입니다. 접근 토큰보다 긴 유효 기간(몇 일 ~ 몇 주)을 가지며, 서버 데이터베이스에 안전하게 저장됩니다. 이 방식으로 사용자는 자주 재로그인할 필요 없이 장기간 인증 상태를 유지할 수 있습니다.
 
* [1]JWT: 당사자 간에 정보를 안전하게 전송하기 위한 개방형 표준(RFC7519)입니다. Header(알고리즘 정보) & Payload(클레임 데이터) & Signature(서명) 세 부분으로 구성되며 각 부분은 Base64로 인코딩되어 점(.)으로 구분됩니다. JWT에는 필요한 모든 정보(사용자 ID & 권한... 등)를 토큰 자체에 포함하므로 서버 측에서는 세션 상태를 유지할 필요가 없습니다. 그리고 서명 메커니즘은 발행자가 [2]비밀 키로 서명하여 토큰의 무결성을 보장합니다. 수신자는 서명을 검증해 토큰이 변조되지 않았는지 확인합니다. 주로 사용자 인증 & API 권한 부여 & 서비스 간 통신에 활용됩니다. 단점으로는 토큰의 크기가 커질 수 있습니다.
 
* [2]비밀 키: JWT에서 사용하는 비밀키는 대칭 키와 비대칭 키처럼 암호화가 아닌 서명을 뜻합니다. JWT의 내용(Payload)은 누구나 읽을 수 있지만, 변조는 불가능합니다. 서버만 비밀키를 알고 있으므로, 변조된 JWT는 서명 검증에 실패합니다.

// Flutter 앱에서 저장된 JWT를 사용해 요청하는 경우
final token = await storage.read(key: 'jwt_token');
final response = await http.get(
  Uri.parse('https://api.example.com/data'),
  headers: {'Authorization': 'Bearer $token'},
);

 
* Flutter에서 토큰 사용 예시

import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final Dio _dio = Dio();
  final FlutterSecureStorage _storage = FlutterSecureStorage();
  
  // 로그인 및 JWT 저장
  Future<bool> login(String username, String password) async {
    try {
      final response = await _dio.post(
        'https://api.example.com/login',
        data: {'username': username, 'password': password},
      );
      
      // 서버로부터 받은 토큰 저장
      await _storage.write(key: 'access_token', value: response.data['access_token']);
      await _storage.write(key: 'refresh_token', value: response.data['refresh_token']);
      return true;
    } catch (e) {
      return false;
    }
  }
  
  // 요청에 JWT 포함하기
  Future<Response> authenticatedRequest(String url) async {
    String? token = await _storage.read(key: 'access_token');
    return _dio.get(
      url,
      options: Options(
        headers: {'Authorization': 'Bearer $token'},
      ),
    );
  }
  
  // 토큰 갱신 처리
  Future<bool> refreshToken() async {
    String? refreshToken = await _storage.read(key: 'refresh_token');
    try {
      final response = await _dio.post(
        'https://api.example.com/refresh',
        data: {'refresh_token': refreshToken},
      );
      await _storage.write(key: 'access_token', value: response.data['access_token']);
      return true;
    } catch (e) {
      return false;
    }
  }
}

 


12. TCP와 UDP에 대해 말해주세요.
└ TCP(Transmission Control Protocol)는 연결 지향적이고 신뢰성 있는 데이터 전송을 보장하는 반면, UDP(User Datagram Protocol)는 비연결 지향적이고 빠른 전송 속도를 우선시하는 프로토콜입니다.
 
TCP(Transmission Control Protocol)는 연결 지향적 프로토콜로, 데이터 전송 전에 [1]3-way handshake를 통해 연결을 설정합니다. 데이터의 순서 보장 & 손실된 패킷 재전송 & 흐름 제어 및 혼잡 제어 기능을 제공하여 신뢰성 있는 데이터 전송을 보장합니다. 주로 웹 브라우징(HTTP/HTTPS), 이메일(SMTP), 파일 전송(FTP)... 등 데이터 무결성이 중요한 애플리케이션에 사용됩니다.
 
UDP(User Datagram Protocol)는 비연결 지향적 프로토콜로, 연결 설정 없이 데이터를 전송합니다. 패킷 순서나 도착 보장이 없어서 일부 데이터가 손실될 수 있지만, 오버헤드가 적어 TCP보다 빠릅니다. 주로 실시간 스트리밍, 온라인 게임, DNS 조회와 같이 속도가 중요하고 일부 데이터 손실이 허용되는 애플리케이션에 적합합니다.
 
Flutter에서는 대부분 HTTP/HTTPS(TCP 기반)를 통해 서버와 통신하지만, 실시간 기능이 필요한 경우 [2]WebSocket(TCP)[3]WebRTC(UDP)를 사용할 수 있습니다.
 
* [1]3-way handshake: TCP 연결을 설정하는 과정으로, 클라이언트와 서버 간에 신뢰성 있는 연결을 보장하기 위한 3단계 과정입니다.
 
1. SYN(Synchronize)

클라이언트 -> 서버: SYN, seq=x

 
클라이언트가 서버에 연결 요청을 보냅니다. 이때 시퀀스 번호(x)를 함께 보냅니다.
 
2. SYN-ACK(Synchronize-Acknowledge)

서버 -> 클라이언트: SYN, seq=y, ACK=x+1

 
서버가 클라이언트의 요청을 받고 응답합니다. 서버는 클라이언트의 시퀀스 번호(x)에 1을 더한 값을 ACK로, 자신의 시퀀스 번호(y)를 SYN으로 보냅니다.
 
3. ACK(Acknowledge)

클라이언트 -> 서버: ACK=y+1

 
클라이언트가 서버의 응답을 확인하고 최종 확인 메시지를 보냅니다. 서버의 시퀀스 번호(y)에 1을 더한 값을 ACK로 보냅니다.
 
위 3단계 과정이 완료되면 TCP 연결이 설정되고 데이터 전송이 시작됩니다. 연결 종료 시에는 비슷한 4-way handshake 과정을 거칩니다.
 
* [2]WebSocket

// Flutter에서 WebSocket 사용 예
final channel = WebSocketChannel.connect(Uri.parse('wss://example.com/ws'));
channel.sink.add('Hello!');
channel.stream.listen((message) {
  print('Received: $message');
});

 
● TCP 기반의 양방향 통신 프로토콜입니다.
● 단일 TCP 연결을 통해 서버와 클라이언트 간 지속적인 양방향 통신을 제공합니다.
● HTTP 프로토콜 위에서 작동하며, 초기 핸드셰이크를 HTTP로 시작하여 WebSocket 프로토콜로 업그레이드합니다.
● 실시간 채팅 & 알림 & 실시간 대시보드... 등에 적합합니다.
 
* [3]WebRTC(Web Real-Time Communication)

// Flutter에서 WebRTC 사용 예시 (간략화)
final _localRenderer = RTCVideoRenderer();
final _peerConnection = await createPeerConnection({'iceServers': []});

// 로컬 비디오 스트림 설정
final _localStream = await navigator.mediaDevices.getUserMedia({
  'audio': true, 'video': true
});
_localRenderer.srcObject = _localStream;

// 피어 연결에 스트림 추가
_localStream.getTracks().forEach((track) {
  _peerConnection.addTrack(track, _localStream);
});

 
● 주로 UDP 기반으로 작동하는 P2P(Peer-to-Peer) 통신 기술입니다.
● 브라우저나 앱 간 직접적인 미디어(오디오, 비디오) 및 데이터 공유가 가능합니다.
● 서버를 통하지 않고 직접 통신하므로 지연 시간이 짧습니다.
● 영상 통화 & 화면 공유 & P2P 파일 전송... 등에 적합합니다.
● 연결 설정을 위한 시그널링 서버는 필요하지만, 데이터 전송은 P2P로 이루어집니다.
 
 


13. Git의 Merge와 Rebase의 차이점에 대해 말해주세요.
└ Merge는 두 브랜치의 변경 사항을 하나로 합치는 비파괴적 방식이고, Rebase는 한 브랜치의 커밋들을 다른 브랜치 위로 재배치하여 선형적 히스토리를 만드는 방식입니다.
 
Git Merge는 두 브랜치의 변경 사항을 하나로 통합하는 과정으로, 두 브랜치의 최신 커밋을 가리키는 새로운 병합 커밋을 생성합니다. 원본 브랜치 구조와 모든 커밋 히스토리가 그대로 유지되어 비파괴적인 방식입니다. 주로 협업 환경에서 안전하게 사용하지만, 복잡한 병합 히스토리가 생길 수도 있습니다.
 
Git Rebase는 한 브랜치의 커밋들을 다른 브랜치의 끝으로 이동시키는 방식으로, 커밋 히스토리를 재작성합니다. 이는 선형적이고 깔끔한 커밋 히스토리를 만들어 주지만, 이미 공유된 브랜치에 적용하면 문제가 발생할 수 있습니다. 주로 로컬에서 작업 중인 개인 브랜치에 적합하며, 충돌 해결이 더 복잡할 수 있습니다.
 
* Merge 예시

      A---B---C (dev)
     /
D---E---F---G (main)

 
여기서 dev 브랜치를 main으로 merge를 하게되면
 

      A---B---C (dev)
     /         \
D---E---F---G---H (main)

 
H라는 병합 커밋이 생성되며, 두 브랜치의 변경사항을 모두 포함합니다. 원래의 모든 커밋 히스토리가 보존됩니다.
 
* Rebase 예시

      A---B---C (dev)
     /
D---E---F---G (main)

 
여기서 dev 브랜치에서 main으로 rebase를 하게되면
 

              A'--B'--C' (dev)
             /
D---E---F---G (main)

 
결과적으로 dev 브랜치의 커밋들(A, B, C)이 main 브랜치의 끝(G 이후)에 재배치됩니다. 이때 원래의 커밋들은 새로운 커밋(A', B', C')으로 다시 만들어집니다.
 
* Merge 사용 사례
● 팀원들과 공유하는 브랜치에 통합할 때 (ex. dev -> feature)
● 작업 내역을 명확하게 분리하고 싶을 때
● 안전하게 브랜치를 합치고 싶을 때
 
* Rebase 사용 사례
● 개인 작업 브랜치를 최신 main과 동기화할 때
● 깔끔한 선형 커밋 히스토리를 원할 때
● PR(Pull Request)을 제출하기 전에 커밋을 정리할 때
 


14. GraphQL에 대해 말해주세요.
└ GraphQL은 클라이언트가 필요한 데이터만 정확히 요청할 수 있게 해주는 쿼리 언어입니다.
 
GraphQL은 Facebook에서 개발한 API를 위한 쿼리 언어입니다. 클라이언트가 필요한 데이터의 구조를 정확히 지정하여 요청할 수 있게 해줍니다. 단일 엔드포인트를 사용하며, 클라이언트는 쿼리(데이터 조회) & 뮤테이션(데이터 변경) & 서브 스크립션(실시간 데이터)을 통해 서버와 통신합니다.
 
REST API와 달리 클라이언트가 정확히 필요한 데이터만 요청할 수 있어서 오버페칭(불필요한 데이터 전송)과 언더페칭(여러 API 호출 필요)을 방지합니다.
 
 


15. CI/CD에 대해 말해주세요.
└ CI/CD는 코드 변경 사항을 자동으로 빌드 & 테스트하고 배포까지 자동화하여 개발 프로세스를 가속화하고 품질을 향상시키는 방법론입니다.
 
CI(지속적 통합)는 개발자들이 코드 변경 사항을 공유 레포지토리에 자주 병합하고, 자동화된 빌드 및 테스트를 통해 통합 문제를 조기에 발견하는 개발 방식입니다. 이를 통해 버그를 빠르게 발견하고 코드 품질을 유지할 수 있습니다.
 
CD는 지속적 배포(Continuous Deployment)와 지속적 제공(Continuous Delivery)을 의미합니다. 지속적 제공은 코드 변경 사항이 테스트를 통과하면 자동으로 스테이징 환경까지 배포하고, 지속적 배포는 한 단계 더 나아가 프로덕션 환경까지 자동으로 배포하는 것을 의미합니다.
 
 


16. 선언형 & 명령형 & 함수형 프로그래밍에 대해 말해주세요.
└ 선언형은 '무엇을(What)'에 중점을 두며, 명령형은 '어떻게(How)'에 중점을 두고, 함수형은 순수 함수와 불변 데이터를 사용해 사이드 이펙트를 최소화합니다.
 
선언형 프로그래밍은 원하는 결과를 명시하고 실행 방법은 시스템에 맡기는 방식입니다. Flutter의 UI 구현 방식이 대표적인 선언형 접근법으로, 위젯을 통해 화면에 보여질 내용을 선언합니다.
 
명령형 프로그래밍은 프로그램의 상태를 변경하는 명령문을 순차적으로 실행하는 방식입니다. 코드가 실행되는 방법과 단계를 명시적으로 정의합니다. C & Java의 전통적인 방식이 여기에 해당합니다.
 
함수형 프로그래밍은 선언형의 일종으로, 순수 함수와 불변 데이터를 사용하여 상태 변경과 부작용을 최소화합니다. 핵심 개념으로는 순수 함수(같은 입력에 항상 같은 출력) & 고차 함수(함수를 인자로 받거나 반환) & 불변성이 있습니다.
 
Flutter에서는 위의 세 가지 패러다임을 모두 활용합니다. 위젯 구성은 선언형, 상태 관리는 함수형 접근법을 사용하며, 필요에 따라 명령형 코드도 작성할 수 있습니다.
 
* 선언형 프로그래밍 예시

// Flutter에서 위젯 구성 예시
Widget buildList() {
  return ListView(
    children: [
      ListTile(title: Text('항목 1')),
      ListTile(title: Text('항목 2')),
      ListTile(title: Text('항목 3')),
    ],
  );
}

 
이 코드는 '무엇을(what)' 화면에 보여줄지 선언합니다. 리스트를 어떻게 그릴지에 대한 세부 과정은 Flutter 프레임워크에 위임하고, 개발자는 원하는 결과물의 구조만 정의합니다.
 
* 명령형 프로그래밍 예시

List<int> getEvenNumbers(List<int> numbers) {
  List<int> result = [];
  for (int i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 == 0) {
      result.add(numbers[i]);
    }
  }
  return result;
}

// 사용 예:
void main() {
  List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  List<int> evenNumbers = getEvenNumbers(numbers);
  print(evenNumbers); // [2, 4, 6, 8, 10]
}

 
이 코드는 '어떻게(how)' 짝수를 찾는지 명시적으로 단계별로 지시합니다. 반복문을 사용해 각 요소를 확인하고, 조건에 맞으면 결과 리스트에 추가하는 과정을 직접 제어합니다.
 
* 함수형 프로그래밍 예시

List<int> getEvenNumbers(List<int> numbers) {
  return numbers.where((number) => number % 2 == 0).toList();
}

// 고차 함수 활용 예시
List<int> applyToEach(List<int> numbers, int Function(int) transform) {
  return numbers.map(transform).toList();
}

// 사용 예:
void main() {
  List<int> numbers = [1, 2, 3, 4, 5];
  
  // 짝수만 필터링
  final evenNumbers = getEvenNumbers(numbers);
  
  // 각 숫자를 제곱
  final squared = applyToEach(numbers, (x) => x * x);
  
  print(evenNumbers); // [2, 4]
  print(squared); // [1, 4, 9, 16, 25]
}

 
함수형 프로그래밍은 함수를 [1]일급 객체로 취급하고, where & map & fold와 같은 [2]고차 함수를 사용해 데이터 변환을 추상화합니다. 코드가 간결해지고 부작용(side effect)이 최소화됩니다.
 
* [1]일급 객체: 다른 객체들에 일반적으로 적용할 수 있는 연산을 모두 지원하는 객체를 말합니다.

// 함수를 변수에 할당
var greet = (String name) => 'Hello, $name!';

// 함수를 인자로 전달
void executeFunction(Function callback) {
  callback();
}

// 함수를 반환값으로 사용
Function createMultiplier(int factor) {
  return (int number) => number * factor;
}

 
Dart에서 함수는 일급 객체입니다.
함수가 일급 객체라는 것은 다음과 같은 특성을 가집니다.
● 변수에 할당할 수 있습니다.
● 다른 함수의 인자로 전달할 수 있습니다.
● 함수의 반환값으로 사용할 수 있습니다.
● 자료구조(리스트, 맵... 등)에 저장할 수 있습니다.
 
* [2]고차 함수

// 함수를 인자로 받는 고차 함수
void forEach(List<int> numbers, void Function(int) action) {
  for (var number in numbers) {
    action(number);
  }
}

// 함수를 반환하는 고차 함수
Function(int) createAdder(int addBy) {
  return (int x) => x + addBy;
}

// 사용 예시
void main() {
  // 리스트의 map, where, reduce 등도 모두 고차 함수입니다
  List<int> numbers = [1, 2, 3, 4, 5];
  
  // map: 각 요소를 변환하는 함수를 인자로 받음
  var doubled = numbers.map((n) => n * 2).toList();
  
  // where: 조건 함수를 인자로 받음
  var evens = numbers.where((n) => n % 2 == 0).toList();
  
  // 직접 만든 고차 함수 사용
  forEach(numbers, (n) => print('Number: $n'));
  
  // 함수를 반환하는 고차 함수 사용
  var addFive = createAdder(5);
  print(addFive(10)); // 15 출력
}

 
고차 함수는 다음 중 하나 이상의 조건을 만족하는 함수입니다.
● 하나 이상의 함수를 인자로 받는 함수
● 함수를 결과로 반환하는 함수
 


17. 메모리 누수(Memory Leak)에 대한 설명과 방지하는 방법에 대해 말해주세요.
└ 메모리 누수는 더 이상 필요하지 않은 객체가 여전히 참조되어 [1]가비지 컬렉션(GC)에 의해 제거되지 않는 상황으로, Flutter에서는 주로 StatefulWidget의 dispose 메소드를 통해 리소스를 적절히 해제해야 합니다.
 
메모리 누수란 프로그램이 더 이상 사용하지 않는 메모리를 해제하지 않아 지속적으로 메모리 사용량이 증가하는 현상입니다. Dart는 가비지 컬렉션을 사용하지만, 참조가 유지되면 메모리가 해제되지 않습니다.
 
* Flutter에서의 주요 메모리 누수 원인
1. 구독(Subscription)이나 리스너를 취소하지 않는 경우
2. 비동기 작업이 완료되기 전에 위젯이 dispose될 때 [3, 4]컨텍스트 유출
3. 전역 또는 정적 변수에 대한 참조 관리 미흡
4. 대용량 객체를 캐시에 저장하고 해제하지 않는 경우
 
* 방지 방법
1. dispose 메소드에서 모든 리스너, 컨트롤러, 구독을 취소
2. 비동기 작업에 mounted 체크 추가
3. StreamControllor & AnimationController... 등의 자원 해제
4. 필요시 [2]약한 참조(weak reference) 사용
5. DevTools의 메모리 프로파일러를 활용한 정기적인 메모리 사용량 분석
 
이러한 방법을 통해 Dart의 가비지 컬렉션(GC)이 불필요한 객체를 효과적으로 수거할 수 있도록 하여 메모리 누수를 방지할 수 있습니다. 
 
* [1] 가비지 컬렉션(Garbage Collection): 프로그램이 더 이상 사용하지 않는 메모리를 자동으로 식별하고 해제하는 메모리 관리 시스템입니다.
 
동작 원리
 
1. 마킹(Marking): GC는 '루트'객체(ex. 전역 변수 & 스택의 로컬 변수)에서 시작하여 모든 참조를 추적하고 '살아있는'객체로 표시합니다.
2. 스위핑(Sweeping): 표시되지 않은 객체(즉, 더 이상 참조되지 않는 객체)의 메모리를 해제합니다.
 
Dart의 가비지 컬렉션: Dart는 두 세대 가비지 컬렉션 시스템을 사용합니다.

void garbageCollectionExample() {
  // 객체가 생성됨
  var list = List.filled(1000, 'item');
  
  // 함수가 종료되면 list 변수는 스코프를 벗어나고
  // 참조가 없어지므로 가비지 컬렉터가 메모리를 해제함
}
void garbageCollectionExample2() {
  // 객체 생성
  var largeList = List.filled(10000, 'data');
  
  // 다른 작업 수행
  doSomething();
  
  // 이 시점에서 largeList는 더 이상 참조되지 않으므로 
  // 다음 GC 사이클에서 메모리가 해제됨
}

 
1. 새로운 세대(Young Generation): 새로 생성된 객체는 여기에 할당되며, 빠른 가비지 컬레션이 자주 실행됩니다.
2. 오래된 세대(Old Generation): 여러 가비지 컬렉션 사이클 동안 살아남은 객체가 이동하며, 덜 자주 수행되는 더 철저한 GC가 실행됩니다.
 
* [2] 약한 참조(Weak Reference)

import 'dart:core';

void weakReferenceExample() {
  // Expando는 객체에 대한 약한 참조 맵을 제공
  final expando = Expando<String>('데이터 저장소');
  
  // 테스트용 객체
  var obj = Object();
  
  // 객체에 데이터 연결 (약한 참조)
  expando[obj] = '중요한 데이터';
  
  // 데이터 접근
  print(expando[obj]); // '중요한 데이터' 출력
  
  // obj에 대한 강한 참조를 제거하면
  obj = null;
  
  // 다음 GC 사이클 후에 expando[obj]는 접근할 수 없게 됨
  // 원래 obj 객체와 연결된 데이터가 메모리에서 해제됨
}

 
약한 참조는 객체에 대한 참조이지만, GC가 메모리를 회수하는 것을 방해하지 않는 참조 유형입니다.
Dart에서는 'dart:core'의 'Expando'클래스를 사용하여 약한 참조를 구현할 수 있습니다.
 
참조의 종류와 특징
 
1. 강한 참조(Strong Reference): 일반적인 참조로, 객체가 이러한 참조로 접근 가능한 동안에는 GC가 메모리를 회수하지 않습니다.
2. 약한 참조(Weak Reference): GC가 객체의 메모리를 회수할 수 있으며, 다른 강한 참조가 없는 경우 약한 참조가 있어도 객체가 수거됩니다.
 
* [3]컨텍스트(Context): Flutter에서 'Context'는 일반적으로 BuildContext 객체를 의미하며, 위젯 트리에서 현재 위젯의 위치를 나타냅니다. 이는 부모 위젯에 접근하고, 테마 & 미디어 쿼리 & 라우팅... 등과 같은 상위 수준의 기능에 접근하는 데 사용됩니다.
 
◎ BuildContext 특징

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // context를 사용하여 테마 데이터 접근
    final theme = Theme.of(context);
    
    // context를 사용하여 화면 크기 접근
    final screenSize = MediaQuery.of(context).size;
    
    // context를 사용하여 다른 화면으로 이동
    return ElevatedButton(
      child: Text('다음 화면'),
      onPressed: () {
        Navigator.of(context).push(
          MaterialPageRoute(builder: (context) => NextScreen())
        );
      },
    );
  }
}

 
● 위젯의 위치 정보 포함
● 상위 위젯 찾기(dependOnInheritedWidgetOfExactType)를 위한 방법 제공
● 테마 & 네비게이션 & 스낵바 & 모달... 등을 위한 접근점 역할
 
* [4]컨텍스트 유출(Context Leak)

class LeakingWidget extends StatefulWidget {
  @override
  _LeakingWidgetState createState() => _LeakingWidgetState();
}

class _LeakingWidgetState extends State<LeakingWidget> {
  @override
  void initState() {
    super.initState();
    
    // 비동기 작업 시작
    fetchDataWithDelay().then((data) {
      // 위젯이 이미 트리에서 제거된 후에도 setState 시도 가능
      // -> 컨텍스트 유출 및 "setState() called after dispose()" 에러
      setState(() {
        // 데이터 업데이트
      });
    });
  }
  
  Future<String> fetchDataWithDelay() async {
    // 3초 후 데이터 반환
    await Future.delayed(Duration(seconds: 3));
    return "데이터";
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

 
컨텍스트 유출은 위젯이 dispose된 후에도 해당 위젯의 BuildContext를 계속 사용하려고 시도하는 상황을 말합니다. 이는 메모리 누수와 예기치 않은 앱 크래시를 일으킬 수 있습니다.
 
발생 원인
 
1. 비동기 작업 후 상태 업데이트 시도: 위젯이 삭제된 후 완료되는 비동기 작업에서 setState 호출
2. 이벤트 리스너/콜백에 컨텍스트 캡처: 위젯이 삭제된 후에도 활성 상태로 유지되는 리스너에서 컨텍스트 사용
3. 전역 싱글톤에 컨텍스트 저장: 애플리케이션 생명주기 동안 컨텍스트를 유지하는 전역 객체
 
컨텍스트 유출(Context Leak) 방지 방법
 
1. 취소 가능한 작업 사용

class CancellableWidget extends StatefulWidget {
  @override
  _CancellableWidgetState createState() => _CancellableWidgetState();
}

class _CancellableWidgetState extends State<CancellableWidget> {
  // 취소 토큰
  CancelToken? _cancelToken;
  
  @override
  void initState() {
    super.initState();
    _cancelToken = CancelToken();
    
    // 취소 가능한 작업 시작
    fetchDataWithCancellation(_cancelToken!).then((data) {
      if (mounted) {
        setState(() {
          // 데이터 업데이트
        });
      }
    }).catchError((e) {
      // 취소된 경우 처리
      if (e is CancelledException) {
        print('작업이 취소되었습니다');
      }
    });
  }
  
  @override
  void dispose() {
    // 작업 취소
    _cancelToken?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

// 취소 토큰 클래스
class CancelToken {
  bool _isCancelled = false;
  bool get isCancelled => _isCancelled;
  
  void cancel() {
    _isCancelled = true;
  }
}

class CancelledException implements Exception {}

Future<String> fetchDataWithCancellation(CancelToken token) async {
  for (int i = 0; i < 3; i++) {
    // 각 단계에서 취소 여부 확인
    if (token.isCancelled) {
      throw CancelledException();
    }
    await Future.delayed(Duration(seconds: 1));
  }
  return "데이터";
}

 
2. [5]mounted 체크

class SafeWidget extends StatefulWidget {
  @override
  _SafeWidgetState createState() => _SafeWidgetState();
}

class _SafeWidgetState extends State<SafeWidget> {
  @override
  void initState() {
    super.initState();
    
    fetchDataWithDelay().then((data) {
      // mounted 체크로 안전하게 상태 업데이트
      if (mounted) {
        setState(() {
          // 데이터 업데이트
        });
      }
    });
  }
  
  Future<String> fetchDataWithDelay() async {
    await Future.delayed(Duration(seconds: 3));
    return "데이터";
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

 
* [5]mounted: Flutter의 State 객체에서 제공하는 Boolean 속성입니다. 이 속성은 현재 State 객체가 위젯 트리에 연결되어 있는지를 나타냅니다.
● true: State 객체가 현재 위젯 트리에 연결되어 있고, setState를 호출할 수 있는 상태
● false: State 객체가 위젯 트리에서 제거되었으며(위젯이 dispose된 후), setState를 호출하면 안 되는 상태
 
생명주기와 관련된 mounted 상태 변화

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String _data = 'Loading...';

  @override
  void initState() {
    super.initState();
    // 비동기 작업 시작
    fetchData().then((result) {
      // 비동기 작업이 완료된 시점에 위젯이 여전히 트리에 있는지 확인
      if (mounted) {
        setState(() {
          _data = result;
        });
      } else {
        // 이미 dispose된 상태라면 setState를 호출하지 않음
        print('위젯이 이미 dispose되었습니다.');
      }
    });
  }

  Future<String> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return '데이터 로드 완료!';
  }

  @override
  Widget build(BuildContext context) {
    return Text(_data);
  }
}

 
1. createState() 호출: State 객체가 생성됩니다.
2. initState() 호출: 이 시점에 'mounted = true'가 됩니다.
3. 위젯이 화면에서 제거
4. dispose() 호출: 이후 'mounted = false'가 됩니다.
 
 


18. BDD & TDD의 차이점에 대해 말해주세요.
└ TDD(Test-Driven Development)는 개발자가 구현 전 테스트를 먼저 작성하는 개발 방법론이고, BDD(Behavior-Driven Development)는 TDD를 확장하여 비즈니스 요구사항과 행동에 초점을 맞춘 접근법입니다.
 
TDD(Test-Driven Development)
● 테스트 코드를 먼저 작성한 후 실제 코드를 구현하는 방식입니다.
● 레드(실패) => 그린(성공) => 리팩터(개선)의 사이클로 진행합니다.
● 개발자 중심적이며 기술적 관점에서 코드의 정확성을 검증합니다.
● Flutter에서는 'flutter_test' 라이브러리를 사용하여 단위 테스트와 위젯 테스트를 작성합니다.
 
BDD(Behavior-Driven Development)
● TDD의 확장으로, 사용자 스토리와 비즈니스 요구사항 중심입니다.
● Given(상황) => When(행동) => Then(결과) 형식으로 테스트를 작성합니다.
● 개발자뿐만 아니라 비개발자(기획자 & 디자이너 & 사용자)도 이해할 수 있는 자연어로 명세합니다.
● Flutter에서는 'flutter_gherkin' 라이브러리를 통해 BDD 스타일의 테스트 작성이 가능합니다.
 
실무 적용 차이
● TDD는 주로 단위 테스트에 집중되어 있고 기술적 세부 사항을 다룹니다.
● BDD는 전체 기능의 동작과 사용자 스토리에 초점을 맞추며 통합 테스트나 인수 테스트와 더 관련이 있습니다.
● Flutter 앱 개발에서는 두 방법론을 혼합하여 사용하는 것이 효과적입니다. 위젯 테스트는 BDD 스타일로, 비즈니스 로직은 TDD로 진행합니다.
 
 


19. SDK 개발과 서비스 개발의 차이점에 대해 말해주세요.
└ SDK 개발은 다른 개발자가 사용할 도구와 라이브러리를 만드는 것으로 안정적인 API와 호환성에 중점을 두며, 서비스 개발은 최종 사용자를 위한 애플리케이션을 구축하는 것으로 사용자 경험과 기능 구현에 초점을 맞춥니다.
 
SDK(Software Development Kit) 개발은 다른 개발자들이 자신의 애플리케이션에 통합할 수 있는 도구 & API & 라이브러리 세트를 개발합니다.
● SDK는 "한 번 작성하고 어디서나 사용"을 목표로 하며, 호환성과 안전성이 최우선입니다.
● SDK는 다른 개발자를 대상으로 하므로 API 설계와 문서화에 더 많은 시간을 투자합니다.
 
SDK 개발 시, 고려 사항
 
1. 안정적이고 직관적인 API 설계
2. 철저한 문서화와 샘플 코드 제공
3. 버전 간 호환성과 명확한 버전 관리
4. 다양한 환경에서 일관된 동작
5. 불필요한 종속성 최소화
 
서비스(Application) 개발은 최종 사용자를 위한 애플리케이션 구축을 말합니다.
● 서비스는 특정 사용자 니즈 충족을 목표로 하며, 사용자 경험과 차별화된 기능이 중요합니다.
● 서비스는 최종 사용자를 대상으로 하므로 UI/UX와 비즈니스 로직에 집중합니다.
 
서비스 개발 시, 고려 사항
 
1. 사용자 경험(UX)과 인터페이스(UI) 디자인
2. 비즈니스 로직과 기능 구현
3. 성능 최적화와 사용자 피드백 반영
4. 배포 및 업데이트 전략
 
 


20. HTTP와 HTTPS의 차이점에 대해 말해주세요.
└ HTTP는 암호화되지 않은 평문 통신 프로토콜이지만, HTTPS는 SSL/TLS를 통해 데이터를 암호화하여 안전한 통신을 제공합니다.
 
HTTP(HyperText Transfer Protocol)
● 웹 브라우저와 서버 간의 통신을 위한 기본 프로토콜
● 텍스트 기반의 평문(암호화되지 않은) 통신
● 기본 포트: 80
● 장점: 단순하고 빠름, 적은 오버헤드
● 단점: 데이터 노출 위험, 중간자 공격에 취약
 
HTTPS(HTTP Secure)
● HTTP에 SSL/TLS(보안 소켓 계층/전송 계층 보안) 프로토콜을 추가한 것
● 암호화된 통신으로 데이터 보호
● 동작 방식: 공개키/개인키 암호화 및 디지털 인증서 사용
● 기본 포트: 443
● 장점
1. 데이터 암호화로 중요 정보 보호
2. 서버 인증을 통한 신뢰성 확보
3. 데이터 무결성 검증 가능
 
* Flutter 앱 개발 시, 고려 사항
● Android 9(API 레벨 28) 이상부터는 기본적으로 HTTP 통신을 차단합니다. HTTPS 사용이 필수입니다.
● IOS에서는 ATS(App Transport Security)로 HTTPS 사용이 필수입니다.
● Flutter에서 HTTP 요청 시, 'http' 라이브러리를 사용합니다.
● HTTP 사용 시, 안드로이드 Manifest에 cleartext 통신 허용 설정이 필요합니다.
● 프로덕션 앱에서는 HTTPS 사용이 필수입니다.
 
 


21. HTTPS의 SSL Handshaking에 대해 말해주세요.
└ SSL/TLS Handshaking은 클라이언트와 서버가 안전한 통신을 위해 서로를 인증하고 암호화 방식을 협상하여 공유 세션 키를 생성하는 과정입니다.
 
SSL/TLS Handshaking 과정
 
1. 클라이언트 헬로(Client Hello): 클라이언트가 지원하는 암호화 스위트 & 랜덤 데이터 & 프로토콜 버전... 등을 서버에 전송합니다.
2. 서버 헬로(Server Hello): 서버가 선택한 암호화 스위트 & 랜덤 데이터 & SSL 인증서를 클라이언트에 전송합니다.
3. 인증서 검증: 클라이언트가 서버의 인증서를 신뢰할 수 있는지 확인합니다. (CA 검증)
4. 키 교환: 클라이언트가 임시 대칭 키(세션 키) 생성 후, 서버의 공개키로 암호화하여 전송합니다.
5. 세션 키 생성 완료: 양측이 동일한 세션 키를 보유하게 됩니다.
6. 암호화 통신 시작: 이후 모든 통신은 협상된 세션 키를 사용한 대칭 암호화로 통신합니다.
 
* 하이브리드 암호화로 HTTPS 이해하기
이해를 돕기 위해, 위의 9번 "대칭 키와 비대칭 키"에 이어서 추가 설명을 하겠습니다.
HTTPS는 가장 대표적인 하이브리드 암호화 사용 사례입니다.
HTTPS의 하이브리드 암호화는 두 가지 단계로 구분할 수 있습니다.
 
1. 핸드셰이크 단계
● 클라이언트가 서버에 연결 요청
● 서버가 인증서(공개 키 포함)를 클라이언트에 전송
● 클라이언트가 임시 대칭 키(세션 키)를 생성하고 서버의 공개 키로 암호화
● 암호화된 세션 키를 서버에 전송
● 서버가 자신의 개인 키로 세션 키를 복호화
 
2. 데이터 교환 단계
● 이후 모든 데이터 통신은 합의된 세션 키를 사용한 대칭 암호화(주로 AES)로 진행
● 이 방식은 비대칭 암호화의 보안성과 대칭 암호화의 효율성을 모두 제공
 
이러한 SSL/TLS 핸드셰이킹 과정을 통해 HTTPS는 안전한 데이터 통신을 보장하며, 중요한 정보가 오가는 모바일 앱에서는 필수적인 보안 요소입니다.
 


 
긴 글을 끝까지 읽어주셔서 감사합니다!

개념을 정리하면서, 특히 메모리 관리나 비동기 프로그래밍과 같은 기초적이지만 중요한 개념들을 다시 한번 깊이 생각해 볼 수 있었습니다. 

그럼, 다음 포스트에서 인사드리겠습니다~!