기술 면접 Q&A 심화편
1. SNS 로그인을 구현하셨는데, OAuth 2.0의 동작 단계를 Google과 Apple을 포함하여 자세히 설명해주세요.
- OAuth 2.0은 이 '카드 키'처럼, 사용자가 자신의 아이디/비밀번호(마스터키)를 다른 서비스(제3자 앱)에 직접 알려주지 않고도, 자신의 데이터에 접근할 수 있는 제한된 권한(Access Token)을 부여하는 표준 방식입니다.
- 네, OAuth 2.0은 사용자의 비밀번호를 직접 받지 않고도, 특정 서비스의 리소스 접근 권한을 제3자 앱에 위임해주는 표준 프로토콜입니다. Google은 표준적인 흐름을 따르지만, Apple은 고유한 특징이 있습니다.
(1) Google 로그인 (표준 Authorization Code Grant 흐름)
Google 로그인은 표준적인 '인가 코드 승인 방식'을 따릅니다.
- 권한 요청: 사용자가 앱에서 'Google로 로그인' 버튼을 누릅니다.
- 인가 코드 요청: 앱은 브라우저를 통해 Google의 인가 서버로 사용자를 이동시킵니다. 이때 저희 앱의
Client ID, 요청할 권한 범위(Scope), 인증 후 돌아올 Redirect URI를 함께 보냅니다.
- 사용자 동의: 사용자는 Google 페이지에서 로그인하고, 제 앱이 특정 정보에 접근하는 것을 허용할지 동의합니다.
- 인가 코드 발급: Google은 지정된
Redirect URI로 사용자를 돌려보내며, 일회성 '인가 코드(Authorization Code)'를 발급합니다.
- 액세스 토큰 요청: 저희 앱의 백엔드 서버는 받은 '인가 코드'와
Client ID, Client Secret을 Google 인가 서버에 보내 액세스 토큰을 요청합니다.
- 액세스 토큰 발급: Google은 모든 정보가 유효하면, 리소스 서버에 접근할 수 있는 '액세스 토큰(Access Token)'을 발급합니다.
- 리소스 요청 및 로그인 완료: 백엔드 서버는 이 토큰을 이용해 Google의 리소스 서버에서 사용자 프로필을 가져온 후, 검증이 완료되면 저희 앱의 자체 토큰(예: JWT)을 발급하여 로그인을 최종 완료시킵니다.
(2) Apple 로그인 (고유한 특징)
- 정의: Apple이 위 표준 기술들을 활용하여 만든 실제 소셜 로그인 서비스입니다.
- 기반 기술: OAuth 2.0 (인가 코드 방식) + OpenID Connect (OIDC)
- 특징:
- OAuth 2.0의 Access Token 뿐만 아니라, OIDC 표준에 따라 사용자의 신원 정보가 담긴 'ID Token'을 추가로
발급합니다. 이 ID Token으로 실제 로그인 처리를 합니다.
- '비공개 이메일 릴레이' 같은 강력한 개인정보 보호 기능을 추가했습니다.
- 핵심: 인가(Authorization)를 넘어 인증(Authentication)까지 책임집니다. 즉, "누구인지 증명"하는 역할까지
수행합니다.
-
네이티브 UI 및 자격 증명(Credential) 사용: Flutter에서 sign_in_with_apple 같은 라이브러리를 사용하면, 앱은 시스템에서 제공하는 네이티브 로그인 UI를 호출합니다. 인증 성공 시, 라이브러리는 ASAuthorizationAppleIDCredential이라는 자격 증명 객체를 반환합니다.
-
최초 로그인 시 정보 제공: 가장 중요한 특징으로, Apple은 사용자의 이름(fullName)과 이메일(email)을 최초 로그인 시에만 제공합니다. 따라서 저희 백엔드 서버는 이 정보를 첫 번째 요청에서 반드시 저장해야 합니다. 이후의 로그인에서는 사용자를 식별할 수 있는 고유한 userIdentifier만 반환됩니다.
-
토큰 종류: Apple은 인증 성공 시, 클라이언트에게 identityToken (JWT 형식)과 authorizationCode를 함께 전달합니다. identityToken에는 이미 사용자의 고유 식별자, 이메일 등의 정보가 포함되어 있습니다.
-
서버 검증: 클라이언트는 이 identityToken이나 authorizationCode를 저희 백엔드 서버로 보냅니다. 서버는 다음 두 가지 방법으로 검증할 수 있습니다.
identityToken 검증: Apple의 공개 키(Public Key)를 이용해 identityToken의 서명을 직접 검증합니다. 검증에 성공하면 토큰에 담긴 사용자 정보가 신뢰할 수 있음을 확인하고 로그인을 처리합니다.
authorizationCode 교환: authorizationCode를 Apple의 인가 서버에 보내 액세스 토큰으로 교환합니다. 이 방식은 Google의 흐름과 더 유사합니다.
결론적으로, 두 방식 모두 사용자의 비밀번호를 직접 다루지 않고 안전하게 인증을 위임하는 핵심 원칙은 동일하지만, Apple은 네이티브 UI 연동, 최초 정보 제공 정책, identityToken 활용 등에서 차별점을 가집니다.
2. Flutter로 UI를 구현하면서 겪었던 가장 큰 어려움은 무엇이었나요?
Flutter로 UI를 구현하면서 겪었던 가장 큰 어려움은 다양한 화면 크기에 대응하는 반응형 레이아웃을 구축하는 것과, 복잡한 목록(List)의 스크롤 성능을 최적화하는 것이었습니다.
-
반응형 레이아웃 구축의 어려움:
- 문제점: 하나의 레이아웃이 일반적인 스마트폰에서는 잘 보이지만, 화면이 작은 보급형 기기나 가로 모드, 태블릿에서는 UI가 깨지거나 공간이 비는 문제가 있었습니다.
- 해결 과정: 이 문제를 해결하기 위해
MediaQuery를 사용하여 현재 기기의 화면 크기와 방향 정보를 얻고, LayoutBuilder를 통해 부모 위젯의 제약 조건을 파악했습니다. 이를 바탕으로 화면 너비에 따라 레이아웃이 동적으로 변경되는 적응형(Adaptive) 위젯을 만들었습니다. 예를 들어, 너비가 600px 미만일 때는 Column으로 위젯을 수직 배치하고, 그 이상일 때는 Row로 수평 배치하는 식으로 구현했습니다. 이를 통해 다양한 환경에서도 일관되고 안정적인 UI를 제공할 수 있었습니다.
-
긴 목록(List)의 스크롤 성능 최적화:
- 문제점: 네트워크 이미지를 포함한 복잡한 아이템이 수백 개 포함된 목록을
ListView로 구현했을 때, 스크롤 시 버벅거림(Jank) 현상이 발생했습니다.
- 해결 과정: 성능 저하의 원인을 분석하고 다음과 같이 최적화를 진행했습니다.
- 가장 먼저,
ListView 대신 ListView.builder를 사용하여 화면에 보이는 아이템만 동적으로 빌드하도록 변경했습니다. 이것만으로도 큰 성능 향상이 있었습니다.
- 리스트 아이템 위젯 내부에서 불필요한 리빌드를 막기 위해, 변경되지 않는 위젯에는
const 키워드를 적극적으로 사용했습니다.
- 네트워크 이미지 로딩이 성능 저하의 주된 원인 중 하나였기 때문에,
cached_network_image 라이브러리를 도입하여 이미지를 캐싱하고, 로딩 중에는 플레이스홀더를 보여주어 사용자 경험과 성능을 모두 개선했습니다.
3. Isolate와 스레드(Thread)의 차이점을 설명해주세요.
Isolate(아이솔레이트)는 Flutter(Dart)에서 코드를 병렬로 실행할 수 있게 해주는 독립적인 실행 단위입니다.
- 독립된 메모리: 각 Isolate는 자신만의 메모리 공간을 가지며 다른 Isolate와 메모리를 공유하지 않습니다. 이로 인해 동시성 문제(race condition 등)가 원천적으로 방지됩니다.
- 단일 스레드 실행: 하나의 Isolate는 단일 스레드 이벤트 루프에서 코드를 실행합니다. Flutter 앱은 기본적으로 '메인 Isolate'에서 실행되며, 이 Isolate가 UI 렌더링을 포함한 모든 작업을 처리합니다.
- 메시지 기반 통신: Isolate 간의 통신은 메모리를 공유하는 대신 포트(Port)를 통해 메시지를 주고받는 방식으로 이루어집니다.
Isolate와 스레드의 가장 근본적인 차이점은 '메모리 공유 여부' 입니다.
-
스레드(Thread):
- 전통적인 운영체제의 개념으로, 하나의 프로세스 내에서 여러 스레드가 동일한 메모리 공간(Heap)을 공유합니다.
- 장점: 메모리를 공유하기 때문에 데이터 접근이 빠르고 통신이 간단합니다.
- 단점: 여러 스레드가 동시에 같은 데이터에 접근하려 할 때 경쟁 상태(Race Condition)나 교착 상태(Deadlock)가 발생할 수 있습니다. 이를 막기 위해 락(Lock), 뮤텍스(Mutex) 같은 복잡한 동기화 기법이 반드시 필요하며, 이는 동시성 프로그래밍을 매우 어렵고 오류 발생 가능성이 높게 만듭니다.
-
Isolate(아이솔레이트):
- Dart의 동시성 모델로, 각 Isolate는 완전히 독립된 자신만의 메모리 공간(Heap)과 이벤트 루프(Event Loop)를 가집니다. 즉, 메모리를 절대 공유하지 않습니다.
- 장점: 메모리 공유가 없기 때문에 경쟁 상태나 교착 상태의 위험이 원천적으로 차단됩니다. 따라서 훨씬 안전하고 예측 가능한 동시성 프로그래밍이 가능합니다.
- 통신 방식: 메모리를 공유하는 대신, Isolate들은 포트(Port)를 통해 서로 메시지를 주고받으며 통신합니다. 한 Isolate가
SendPort로 메시지(객체의 복사본)를 보내면, 다른 Isolate는 ReceivePort로 그 메시지를 받는 방식입니다. 이는 마치 각자 독립된 주방을 가진 요리사들이 배달 창구를 통해서만 음식을 주고받는 것과 같습니다.
핵심 요약: 스레드가 '하나의 주방에서 여러 요리사가 부딪히며 일하는 것'이라면, Isolate는 '각자의 주방을 가진 요리사들이 안전하게 소통하며 일하는 것'과 같습니다. Flutter에서 이미지 처리나 JSON 파싱, 암호화 같은 무거운 작업을 UI 스레드에서 분리하여 앱의 버벅임을 막기 위해 Isolate를 사용하며, 간단한 작업은 compute() 함수를 통해 쉽게 처리할 수 있습니다.
4. RDBMS와 NoSQL의 차이점, 그리고 CRUD 시 주의해야 할 점은 무엇인가요?
RDBMS와 NoSQL의 차이점
| 구분 | RDBMS (관계형 데이터베이스) | NoSQL (비관계형 데이터베이스) |
|---|
| 데이터 구조 | 정해진 스키마(테이블, 행, 열)에 따라 데이터 저장 | 스키마 없이 자유로운 형태(Document, Key-Value 등)로 저장 |
| 스키마 | 스키마가 엄격하여 데이터 무결성 보장 | 스키마가 유연하여 데이터 모델을 쉽게 변경 가능 |
| 확장성 | 주로 수직적 확장(Scale-up, 서버 성능 향상) | 주로 수평적 확장(Scale-out, 서버 수 증가)에 용이 |
| 일관성 | ACID 트랜잭션을 통해 데이터의 강력한 일관성 보장 | BASE 모델을 통해 분산 환경에서의 가용성에 집중 (결과적 일관성) |
| 대표 예시 | MySQL, PostgreSQL | MongoDB, Firestore, Redis |
CRUD 시 (백엔드와 협업 관점에서) 주의해야 할 점
- Create (생성): 핵심 데이터 유효성 검사(Validation)는 반드시 백엔드에서 처리하도록 협의해야 합니다. 프론트엔드 검사는 사용자 경험을 위한 보조 수단일 뿐입니다.
- Read (조회): 한 번에 모든 데이터를 가져오지 않도록 페이지네이션(Pagination)을 API에 필수로 구현해야 합니다. 또한, 화면에 필요한 데이터 필드만 선택적으로 받을 수 있도록 API를 설계하여 네트워크 부하를 줄여야 합니다.
- Update (수정): 사용자의 이름만 바꾸는데 모든 정보를 보내는 것은 비효율적이므로, 변경이 필요한 필드만 수정할 수 있도록
PATCH 메서드를 지원하는 API를 백엔드와 협의하는 것이 좋습니다.
- Delete (삭제): 실제 데이터를 바로 지우는 Hard Delete 대신, '삭제됨' 상태로 표시만 하는 Soft Delete 방식을 사용할지 정책을 정해야 합니다. 이는 데이터 복구나 분석에 유리합니다.
5. Flutter에서 Heap과 Stack, 값 형식과 참조 형식의 차이는 무엇인가요?
Stack과 Heap
- Stack(스택): 정적 메모리 영역입니다. 함수가 호출될 때 지역 변수, 매개변수, 값 형식 데이터가 저장됩니다. 속도가 매우 빠르지만 크기가 작습니다.
- int, double, bool, null 등 크기가 정해진 값 타입 데이터가 저장
- 데이터가 차곡차곡 쌓이는(Push) 구조이며, 사용할 때는 가장 위에서부터 빼내는(Pop)
LIFO(Last-In, First-Out) 방식을 따릅니다.
- Heap(힙): 동적 메모리 영역입니다. 클래스의 인스턴스(객체),
List, Map 같은 참조 형식 데이터가 저장됩니다. Dart의 가비지 컬렉터(GC)가 관리하며, 크기가 크고 유연합니다.
- 필요한 공간을 찾아서 할당해야 하므로 속도가 느림
- 스택보다 훨씬 크고, 프로그램 실행 중에 동적으로 크기가 변할 수 있습니다.
- 참조 타입 (Reference Types) 객체: List, Map, Set 등
값 형식(Value Type)과 참조 형식(Reference Type)
- 값 형식 (Value Type): 변수에 실제 데이터 값이 직접 저장됩니다 (
int, double, bool). 변수를 다른 변수에 할당하면 값이 복사되므로, 복사본을 변경해도 원본은 영향을 받지 않습니다.
- 참조 형식 (Reference Type): 변수에 데이터가 있는 메모리 주소(참조)가 저장됩니다 (
List, Map, 클래스 인스턴스). 변수를 다른 변수에 할당하면 메모리 주소가 복사되어, 두 변수가 동일한 객체를 가리킵니다. 따라서 한쪽에서 객체를 변경하면 다른 쪽에도 그대로 반영됩니다.
6. JWT가 무엇이고, 프로젝트에서 어떻게 사용되었는가?
JWT(JSON Web Token)는 JSON 형식의 데이터를 담아 안전하게 전송하기 위한 웹 토큰으로, 특히 Stateless한 서버 환경에서 사용자 인증에 널리 쓰입니다.
구조: Header, Payload, Signature 세 부분으로 구성되며, .으로 구분됩니다. 가장 중요한 Signature는 서버만 아는 비밀 키로 암호화되어, 토큰의 위변조 여부를 검증하는 데 사용됩니다.
프로젝트에서의 사용 방식:
- 로그인 및 발급: 사용자가 로그인에 성공하면, 백엔드 서버는 사용자의 고유 ID 등을 Payload에 담아 서명한 후 JWT를 생성하여 클라이언트(앱)에 전달합니다.
- 저장: 앱은 받은 JWT를
flutter_secure_storage와 같은 안전한 저장소에 보관합니다.
- API 요청: 로그인이 필요한 API를 요청할 때마다, 저장된 JWT를 HTTP
Authorization 헤더에 Bearer <JWT> 형태로 포함하여 서버에 보냅니다.
- 서버 검증: 서버는 요청받은 JWT의 서명을 자신의 비밀 키로 검증합니다. 서명이 유효하면 사용자를 신뢰하고 요청을 처리합니다. 이를 통해 서버는 사용자의 로그인 상태를 세션에 저장할 필요 없이(Stateless) 확장성 있는 구조를 만들 수 있었습니다.
JWT 사용 이유:
JWT를 사용하는 가장 큰 이유는 Stateless(무상태) 인증 시스템을 구축하기 위함입니다. 이는 서버의 확장성과 유지보수성을 크게 향상시킵니다.
- Stateless 서버 구현 (서버의 확장성):
- 전통적인 세션 방식의 문제점: 과거에는 서버가 사용자의 로그인 상태를 메모리나 데이터베이스의 '세션'에 저장했습니다. 이 방식은 사용자가 많아져 서버를 여러 대로 늘릴 경우(Scale-out), 특정 사용자의 세션이 저장된 서버로만 요청을 보내야 하는 '스티키 세션(Sticky Session)' 문제가 발생합니다. 이는 로드 밸런싱을 복잡하게 만들고 서버 확장성을 저해합니다.
- JWT의 해결책: JWT는 사용자 인증에 필요한 모든 정보를 토큰 자체에 담고 있습니다. 서버는 클라이언트가 보낸 토큰의 서명만 검증하면 사용자를 신뢰할 수 있으므로, 서버에 사용자의 상태를 저장할 필요가 없습니다(Stateless). 따라서 어떤 서버로 요청이 들어와도 상관없이 인증을 처리할 수 있어, 서버를 자유롭게 확장할 수 있습니다.
- 간결함과 독립성 (Self-contained):
- JWT는 사용자 식별에 필요한 정보(예: 사용자 ID)를 Payload에 직접 포함하고 있습니다. 따라서 서버는 토큰을 받은 후, 사용자 정보를 확인하기 위해 데이터베이스를 추가로 조회할 필요가 없습니다. 이로 인해 시스템의 부하가 줄어들고 응답 속도가 빨라집니다.
- 보안 및 신뢰성:
- JWT는 비밀 키로 서명되어 있으므로, 제3자가 토큰의 내용을 임의로 변경(위조)하더라도 서버는 서명 검증을 통해 이를 즉시 알아챌 수 있습니다. 이를 통해 데이터의 무결성(Integrity)이 보장됩니다.
- 다양한 플랫폼 및 도메인 간의 호환성:
- JWT는 웹 표준이므로, 웹, 모바일 앱(Flutter, iOS, Android), 데스크톱 등 다양한 클라이언트 환경에서 쉽게 사용할 수 있습니다. 또한, *.example.com과 같이 여러 서브도메인을 사용하는 환경에서도 쿠키(Cookie) 방식보다 훨씬 유연하게 인증 정보를 공유할 수 있습니다.
결론적으로, JWT는 서버의 확장성을 확보하고, 시스템 간의 의존성을 낮추며, 안전한 데이터 전송을 가능하게 하기 때문에 현대적인 분산 시스템 환경에서 인증을 구현하는 사실상의 표준으로 사용되고 있습니다.