본문 바로가기
CS/기술 면접 대비

[백엔드 기술 면접] #4 Server (2)

by 경험의 가치 2024. 11. 14.

#4 Server (2)

Q. API Rate Limiter를 구현할 수 있는 알고리즘 4가지를 설명해보아라.

A. 첫 번째는 토큰 버킷 알고리즘입니다. 요청을 처리할 때마다 버킷에서 토큰을 꺼내 사용하는 방식으로, 토큰이 남아있으면 요청이 처리되고 없으면 요청이 버려집니다. 짧은 시간에 집중되는 트래픽을 처리할 수 있어 유용하지만, 버킷 크기와 토큰 공급률 설정이 까다로울 수 있습니다. 두 번째는 누출 버킷 알고리즘으로, 고정된 크기의 FIFO 큐를 사용해 요청을 관리합니다. 큐에 자리가 있으면 요청이 추가되고, 큐가 가득 차면 요청이 버려지며, 안정적인 처리율을 보장하지만 트래픽이 몰리면 새로운 요청이 처리되지 않을 수 있습니다. 세 번째는 고정 윈도 카운터 알고리즘입니다. 타임라인을 고정된 간격의 윈도로 나누어 카운터를 붙여, 임계치에 도달하면 새로운 윈도가 열릴 때까지 요청을 버리는 방식입니다. 메모리 효율적이지만 윈도 경계에서 일시적인 트래픽 폭증이 발생할 수 있습니다. 네 번째는 이동 윈도 로깅 알고리즘으로, 요청의 타임스탬프를 기반으로 로그를 관리하며, 임계치에 도달하면 요청을 버립니다. 모든 순간에서 윈도의 허용 요청 수를 관리할 수 있지만, 메모리 사용량이 많아질 수 있습니다. 마지막으로 이동 윈도 카운터 알고리즘은 현재 1분과 직전 1분의 요청 수를 이용해 현재 윈도의 요청 수를 계산합니다. 버스트 트래픽에 대응하기 좋지만, 직전 시간대 요청이 균등하게 분포되어 있다고 가정하기에 느슨할 수 있습니다. 이 다섯 가지 알고리즘은 각각의 특성과 장단점을 가지고 있어, 상황에 맞게 선택해 Rate Limiting을 구현할 수 있습니다.

Q. API Gateway의 필요성에 대해서 설명해보아라.

A. API Gateway는 클라이언트와 서버 간의 중간 매개체로서 역할을 합니다. 보안, 인증, 로깅, 모니터링, 로드 밸런싱, 캐싱 등을 제공하여 시스템의 확장성과 관리 효율성을 높입니다. 이를 통해 서비스 간의 통신을 중앙에서 제어하고, 클라이언트가 여러 서비스에 일관된 방식으로 접근할 수 있게 합니다. 또한, API Composition을 통해 API Gateway에서 여러 마이크로서비스에서 데이터를 수집하고, 이를 클라이언트에 단일 응답으로 제공할 수 있습니다. 예를 들어, 클라이언트가 하나의 요청을 보냈을 때, API Gateway는 필요한 여러 마이크로서비스에서 데이터를 가져와 이를 조합하여 클라이언트에게 전달합니다. 이 방식은 클라이언트가 여러 마이크로서비스에 각각 요청을 보내는 번거로움을 없애고, 시스템의 복잡성을 클라이언트로부터 감추며, 성능 최적화에도 기여합니다.

 

Q. 캐싱의 읽기 전략과 쓰기 전략들에 대해서 각각 설명해보아라. 어떤 조합을 이용하는 것이 제일 좋을까?

A. 읽기 전략에는 Look-Aside와 Read-Through가 있습니다. Look-Aside는 클라이언트가 먼저 캐시를 조회하고, 캐시에 데이터가 없다면 직접 데이터 소스에서 데이터를 가져온 뒤 필요에 따라 캐시에 저장하는 방식입니다. 이 방식은 유연성이 높지만, 캐시에 데이터가 없을 경우 성능 이점이 줄어들 수 있습니다. 반면 Read-Through는 클라이언트가 요청을 보낼 때 캐시를 먼저 조회하며, 캐시에 데이터가 없는 경우 캐시가 직접 데이터 소스에서 데이터를 가져와 캐시에 저장하고 클라이언트에게 반환하는 방식입니다. 이 방식은 캐시의 일관성을 유지하는 데 유리하며 클라이언트가 데이터 소스에 직접 접근하지 않아 코드가 단순해집니다.

쓰기 전략에는 Write-Through, Write-Back, Write-Around가 있습니다. Write-Through는 데이터를 쓸 때 캐시와 데이터 소스에 동시에 쓰는 방식으로, 데이터 일관성이 보장되지만 성능이 다소 낮아질 수 있습니다. Write-Back은 데이터를 먼저 캐시에만 쓰고, 일정 시점에 데이터 소스에 반영하는 방식으로 성능이 우수하지만, 캐시 장애 시 데이터 손실 위험이 있습니다. Write-Around는 데이터를 쓸 때 캐시를 건너뛰고 데이터 소스에만 쓰는 방식으로, 자주 참조되지 않는 데이터가 캐시를 오염시키는 것을 방지할 수 있지만, 이후 해당 데이터를 조회할 때 캐시에 없을 가능성이 큽니다.

일반적으로 가장 많이 쓰이는 조합은 Look Aside + Write Around 조합입니다. 해당 조합을 사용하면, 쓸 때 마다 Cache를 무효화 하고, 자연스럽게 Cache Miss를 유도해서 Database에서 읽어올 수 있고, 읽어올 때 우선적으로 Cache로 부터 읽어올 수 있습니다.

 

Q. 캐싱은 어떤 데이터에 대해서 해야 효과적인지 예시와 함께 서술하여라.

A. 캐싱은 자주 조회되지만 자주 변경되지 않는 데이터에 대해 효과적입니다. 예를 들어, 인기 상품 목록, 자주 조회되는 블로그 게시물, 사용자 프로필 정보 등이 캐싱에 적합합니다. 이런 데이터는 읽기 요청이 많아 성능 향상을 기대할 수 있지만, 빈번한 변경이 없어 캐시 일관성 문제를 줄일 수 있습니다.

 

Q. 캐시의 TTL 설정과 Cache Stampede 현상에 대해서 서술해보아라.

A. 캐시의 TTL(Time To Live) 설정은 캐시의 유효 기간을 정하는 것으로, 데이터 정합성 문제와 메모리 사용량을 관리하기 위해 필요합니다. TTL이 너무 길면 오래된 데이터가 남아 정합성 문제가 생기고, 너무 짧으면 캐싱의 효과가 떨어집니다. 또한, TTL이 짧아 Cache Stampede 현상이 발생할 수 있는데, 이는 많은 요청이 동시에 만료된 캐시를 갱신하려 할 때 발생하여 서비스 장애로 이어질 수 있습니다. 적절한 TTL 설정은 데이터 특성과 사용 패턴에 따라 신중히 결정해야 합니다.

 

Q. Redis와 Memcached의 차이는 무엇인가?

A. Redis와 Memcached는 모두 인메모리 데이터 저장소이지만, 여러 가지 차이점이 있습니다.

첫째, Redis는 싱글 스레드 기반으로 동작하는 반면, Memcached는 멀티스레드를 지원하여 멀티 프로세싱이 가능합니다. 이를 통해 Memcached는 여러 CPU 코어를 활용할 수 있어 높은 동시성을 제공합니다.

둘째, Redis는 다양한 데이터 구조를 지원합니다. 문자열(String), 리스트(List), 셋(Set), 해시(Hash), 정렬된 셋(Sorted Set) 등 여러 자료구조를 통해 복잡한 데이터 모델을 구현할 수 있습니다. 반면, Memcached는 단순한 키-값 저장소로, 모든 데이터를 문자열 형태로만 저장합니다.

셋째, Redis는 다양한 용도로 사용할 수 있도록 여러 기능을 지원합니다. 예를 들어, Pub/Sub 메시징, 트랜잭션, Lua 스크립트 실행, TTL 설정 등을 통해 복잡한 애플리케이션 요구 사항을 충족시킬 수 있습니다.

넷째, Redis는 데이터 지속성을 지원합니다. 스냅샷(RDB)과 AOF(Append-Only File) 로그를 통해 데이터를 디스크에 저장하여, 시스템 장애 시에도 데이터를 복구할 수 있습니다. 반면, Memcached는 데이터 지속성을 지원하지 않으며, 서버 재시작 시 모든 데이터가 삭제됩니다.

결론적으로, Redis는 다양한 데이터 구조와 고급 기능, 데이터 지속성을 제공하여 복잡한 애플리케이션에 적합합니다. 반면, Memcached는 단순한 키-값 저장소로서 높은 성능과 멀티스레딩을 필요로 하는 캐싱 용도에 적합합니다.

 

Q. Redis의 데이터 구조(예: String, List, Set, Hash, Sorted Set)와 각 구조의 사용 사례를 설명하시오.

A. Redis의 String은 가장 기본적인 데이터 타입으로, 캐싱, 세션 저장, 카운터 등에 사용됩니다. List는 순서가 있는 컬렉션으로, 대기열, 로그 저장 등에 유용합니다. Set은 중복 없는 집합으로, 태그 저장, 유니크 아이템 관리 등에 적합합니다. Hash는 필드와 값의 맵으로, 사용자 프로필과 같은 객체 저장에 사용됩니다. Sorted Set은 값에 점수를 매겨 정렬된 집합으로, 리더보드, 순위 저장 등에 활용됩니다.

 

Q. 동시성을 예방할 수 있는 방안을 아는만큼 서술해보아라.

A. 동시성을 예방할 수 있는 방안으로는 여러 가지가 있습니다.

첫째, 낙관적 락(Optimistic Locking)은 트랜잭션이 끝날 때까지 다른 트랜잭션이 데이터를 수정하지 않았는지 검증합니다. 주로 버전 번호나 타임스탬프를 사용하여 데이터가 변경되지 않았음을 확인하고, 변경되었다면 롤백하거나 재시도합니다.

둘째, 비관적 락(Pessimistic Locking)은 데이터에 접근할 때마다 락을 걸어 다른 트랜잭션이 접근하지 못하도록 합니다. 이는 충돌 가능성이 높을 때 사용하며, 데이터 일관성을 강하게 보장합니다.

셋째, Named Lock은 특정 이름을 가진 락을 획득하여 동시성을 제어하는 방식입니다. 주로 데이터베이스에서 제공하며, 동일한 이름의 락을 획득하려는 다른 트랜잭션을 대기시킵니다.

넷째, 트랜잭션 Isolation Level 조정은 트랜잭션이 서로에게 미치는 영향을 제어하는 방법입니다. Isolation Level은 Read Uncommitted, Read Committed, Repeatable Read, Serializable이 있으며, 높은 수준의 Isolation Level일수록 동시성 문제가 적지만 성능이 저하될 수 있습니다.

다섯째, 분산락(Distributed Locking)은 여러 노드에 걸쳐 락을 분산시키는 방식입니다. Redis와 같은 인메모리 데이터 저장소를 이용하여 구현할 수 있으며, Redlock 알고리즘을 사용하여 높은 신뢰성을 보장합니다.

여섯째, Redis Transaction은 Redis의 MULTI, EXEC 명령어를 사용하여 일련의 명령을 원자적으로 실행하는 방식입니다. 이를 통해 동시성 문제를 예방할 수 있습니다.

일곱째, Redis Lua Script는 Redis 서버에서 직접 실행되는 Lua 스크립트를 사용하여 복잡한 작업을 원자적으로 처리하는 방법입니다. 이는 네트워크 왕복을 줄이고, 더 높은 성능과 일관성을 보장합니다.

 

Q. Java의 Synchronized 매서드 자체를 Transaction을 걸더라도 동시성은 예방되지 않는다. 이유가 무엇인가? 또한, Transaction 시작 전에 synchronized를 묶어버어도 해결되지 않는다. 그 이유가 무엇인가?

A. Java의 Synchronized 메서드는 객체 수준의 락을 사용하여 동시 접근을 제어하지만, 트랜잭션은 데이터베이스의 ACID 속성을 보장하는 논리적 단위입니다. Synchronized와 트랜잭션은 다른 레벨에서 동작하므로 단순히 Synchronized를 사용한다고 트랜잭션의 일관성을 보장할 수 없습니다. 트랜잭션 시작 전에 Synchronized를 묶어도 데이터베이스 락과 자바 객체 락이 일치하지 않아 동시성 문제가 발생할 수 있습니다.

 

Q. Named Lock 같은 경우는 스케일 아웃 하더라도 성능이 늘어나지 않는다. 반면에 Redis 분산락은 스케일 아웃하면 성능이 좋아진다. 이유가 무엇인가? Multiplexing I/O의 epoll 매커니즘과 연관지어 설명해보아라.

A. Named Lock은 주로 데이터베이스를 통해 구현되며, 데이터베이스 락은 단일 노드에 의존하기 때문에 스케일 아웃 시 성능 향상이 제한적입니다. 반면, Redis 분산락은 여러 노드에 분산되어 락을 관리할 수 있어 스케일 아웃 시 성능이 향상됩니다. Redis는 epoll을 사용하여 비동기 I/O 처리를 최적화하고, 다중 클라이언트 연결을 효율적으로 관리하여 성능을 극대화합니다.

 

Q. Spring의 Lettuce와 Redission 두 Redis Client 라이브러리의 차이는 무엇인가? Lock의 관점에서 설명해보아라.

A. Spring의 Lettuce와 Redisson은 두 가지 주요 Redis 클라이언트 라이브러리로, 각기 다른 방식으로 Redis와 상호작용합니다. Lettuce는 고성능 비동기 및 반응형 API를 제공하며, SpinLock을 이용한 락 메커니즘을 지원합니다. SpinLock은 락을 획득하려는 스레드가 반복적으로 시도하여 락을 얻을 때까지 대기하는 방식입니다. 이는 간단하고 빠르지만, 높은 CPU 사용률을 초래할 수 있습니다. 반면에 Redisson은 보다 고수준의 추상화를 제공하며, Pub/Sub 메커니즘을 이용하여 락을 구현합니다. Pub/Sub은 락을 획득하려는 클라이언트가 메시지를 수신하고 처리함으로써 락을 획득할 수 있게 합니다. 이는 CPU 사용률을 줄이고, 보다 효율적인 자원 관리를 가능하게 합니다.

또한, Redisson은 Redlock 알고리즘을 지원합니다. Redlock은 분산 환경에서 락의 신뢰성을 보장하는 알고리즘으로, 여러 Redis 인스턴스에 걸쳐 락을 분산시키고, 다수의 인스턴스에서 락을 획득함으로써 단일 장애 지점을 줄입니다. Lettuce는 기본적으로 단일 인스턴스 락을 지원하며, Redlock을 직접적으로 지원하지는 않습니다.

결론적으로, Lettuce는 간단하고 고성능이지만 CPU 사용률이 높은 SpinLock을 주로 사용하며, Redlock 알고리즘 지원이 제한적입니다. 반면에 Redisson은 효율적인 Pub/Sub 메커니즘과 Redlock 알고리즘을 통해 보다 견고한 분산 락을 제공하지만, 설정이 복잡하여 초기 구성이 어렵다는 단점이 있습니다. 이를 통해 사용자는 필요한 기능과 설정의 난이도를 고려하여 적절한 라이브러리를 선택할 수 있습니다.