💼interviewnhn gameenvil console backend 직무 인터뷰 준비약 4분GitHub에서 보기nhn gameenvil console backend 직무 인터뷰 준비 자기소개 안녕하세요 김병태입니다. 소셜카지노 슬롯게임, 블록체인을 활용한 메타버스 게임 등 다양한 도메인을 경험해 왔습니다. NHN에서는 슬롯 백엔드 개발을 담당하여, 슬롯 게임 제작 및 아키텍처 고도화, 테스트 가능한 환경을 만들었으며 품질과 구조 개선에 강점을 갖고 있습니다. 새로운 기술과 개발 프로세스 최적화에도 관심이 많아, AI기반 슬롯게임개발 TF에도 참여하여 생산성을 향상하기 위해 노력했습니다. 고성능 게임엔진 백엔드 환경과 분산 시스템을 경험해보고자 지원하게 되었습니다. RabbitMQ 혹은 Message Queue에 대해서 경험이 있는가? 슬롯 게임 데이터의 변경을 관리하기 위해 RabbitMQ를 사용해본 경험이 있습니다. 게임 데이터는 자주 변하지 않는 데이터로 애플리케이션 메모리에 올려두고 사용했는데요, 이 때 DB 변경시점과 여러 서버에서 캐쉬 동기화 시점을 맞추기 위해 RabbitMQ를 사용한 경험이 있습니다. 이 문제를 해결하기 위해 Hibernate Post-Commit Listener를 활용해 데이터 변경을 감지했고, 변경된 엔티티 ID를 RabbitMQ를 통해 fanout 방식 브로드캐스트로 전달하도록하는 구조를 사용했습니다. 각 서버는 메시지를 수신하면, 자체적으로 유지하던 캐시 메모리를 갱신하도록 처리하여, DB와 메모리 캐시간 정합성을 유지하도록 했습니다. Hibernate Post-Commit Listener는 어떻게 동작하나요? Hibernate는 flush 단계에서 Dirty Checking을 수행합니다. 트랜잭션 커밋 직전, Hibernate Event System이 해당 엔티티 변경 이벤트를 기록합니다. DB 커밋이 실제로 성공하면, 그 때 이벤트 리스너가 실행됩니다. 리스너 내부에서는 변경된 엔티티 정보, 변경된 필드, 엔티티 ID등을 받아 후처리 로직을 자유롭게 실행할 수 있습니다. 캐시 갱신 시 데이터 불일치와 동기화 문제는 어떻게 해결했나요? 게임 서버 특성상 캐시에 대한 Read 요청이 많고, Write는 메시지 수신 시에만 간헐적으로 발생했습니다. 이런 read-heavy 환경에서는 일반적인 ReentrantReadWriteLock보다 StampedLock이 optimistic read와 write lock이 훨씬 효율적입니다. 평소에는 Optimistic Read Lock으로 캐시를 읽습니다. 대부분 Read 요청이기 때문에 lock이 거의 필요 없고, 스레드 경합도 없어서 매우 빠르게 응답 가능합니다. 캐시 갱신이 필요한 타이밍에만 Write Lock을 획득했습니다 Write Lock은 단일 스레드가 캐시 값을 변경할 때 잠깐만 획득됩니다. Read 요청은 Write 요청을 막지 않고, Write는 Read를 대기 시킵니다. 짧은 불일치에 대해서는 허용하는 수준으로 동작하게 했습니다. read는 잠깐 이전 캐시를 볼 수 있지만, 슬롯 게임 특성상 게임의 정보가 변경되는 일은 빈도가 낮은 편이었습니다. StampedLock vs ReentrantReadWriteLock은 어떻게 다른가요? 둘 다 읽기-쓰기를 분리하는 락이라는 점은 같습니다. 읽기 동시성은 높이고, 쓰기 구간에서는 배타적으로 보호하는 목적을 갖고 있습니다. ReentrantReadWriteLock은 이름 그대로 재진입성을 갖고 있어, 같은 스레드가 여러번 같은 락을 획득할 수 있습니다. 다만 읽기/쓰기는 내부적으로 락 상태를 갱신해야 하기 때문에, 읽기가 많은 상황에서는 오버헤드가 커질 수 있습니다. StampedLock은 낙관적 읽기를 제공하며 쓰기가 드문 시나리오에서는 거의 락 오버헤드가 없는 것과 비슷한 성능을 낼 수 있습니다. 또, 락을 잡을 떄 long 타입의 stamp를 반환하고, stamp를 넘겨줘야 unlock이 됩니다. 이 떄문에 특정 스레드에 종속되지 않는 대신, 코드를 잘못짜면 unlock 누락 등으로 이어지기 쉽습니다. Kafka 경험도 있나요? 슬롯 게임에서 Spin 요청에서 즉시 응답과 비동기 후 처리를 분리하기 위해, 스핀 과정에서 발생하는 미션 달성, 로그 처리 등은 ApplicationEvent로 발행하여 트랜잭션 이후에 처리되도록 했습니다. 트랜잭션 커밋된 이후 Kafka에 메시지를 발행하고, 발행 시 오류가 발생하면 Outbox 패턴을 통해, 일단 DB에 적재한 후 별도 재처리 배치 작업으로 카프카로 재전송하는 구조를 사용했습니다. 이렇게 설계함으로써 Spin API의 응답 속도는 유지하면서도 후처리 작업의 신뢰성과 재시도 가능성을 확보했습니다. 왜 RabbitMQ가 아닌 Kafka를 사용했나요? Spin 후처리 데이터는, 로그 기반 스토리지와 재처리가 가능한 Kafka를 사용했다고 생각됩니다. RabbitMQ는 실시간의 단기 메시징 중심으로 동작되지만, Kafka를 사용함으로 써 Consumer에 의해 Commit되지 않으면 메시지가 계속 남아 있어, 필요하면 과거 메시지를 재처리할 수 있도록 하기 때문입니다. 장기 보관 / 재처리 / 리플레이 구조에 더 강한 Kafka를 사용함으로써, 결과적으로 일관성을 유지할 수 있는 구조를 만들기 위해 Kafka를 사용한 것으로 판단됩니다. 또한 userId 기반으로 파티션 키를 user account uuid로 사용함으로 써, 해당 메시지에 대한 순서가 보장되도록 처리할 수 있습니다. Kafka에서 메시지는 어떤 모델을 사용했나요? (At Most Once, At Least Once, Exactly Once) 메시지는 반드시 한 번 publish 되도록 처리 Outbox 패턴을 사용하여, 전송 실패 시 재전송 할 수 있도록 처리했습니다. Consumer에대해서는 중복 발생 가능성이 열려있는 상태인 것 같은데, 이는 매 스핀마다 spinId를 부여했고, 해당 id를 통해서 중복된 메시지인지 구분할 수 있도록은 처리해두었습니다. 데이터베이스 샤딩을 해본적이 있나요? 유저 ID기반, mod 방식 샤딩을 적용해본 경험이 있습니다. 공통 DB에서 account 테이블을 관리하고, 각 유저에 대한 shard 번호를 DB에 관리하도록 했습니다. 이 때, 트랜잭션을 사용하기전에 ThreadLocal 스토리지를 활용하여 shard 번호를 설정하도록 하도록하여 동작하도록 했습니다. 그럼 Spring 트랜잭션에서 DataSource를 선택하는 흐름은 어떻게 되나요? 메서드를 호출하면 AOP Proxy에서 @Transactional를 감지하게 되고 PlatformTransactionManager.begin()을 호출하게 됩니다. 이 때 DataSourceTransactionManager.getConnection()을 호출하여 DataSource를 가져오게 됩니다. DataSource(=RoutingDataSource)에서 determineCurrentLookupKey()를 호출하게 되고 이 때 ThreadLocal에 저장된 shardKey를 활용하여 데이터 소스를 선택할 수 있도록 했습니다. 그럼 샤딩 라우팅을 어떻게 하면 최적화 할 수 있을까요? 샤드 내부 트랜잭션만 허용하고, 샤드 간 정합성은 이벤트 기반 Eventually Consistent 패턴을 활용하여 결과적으로 정합성을 유지하는 방식을 택할 것 같습니다.
nhn gameenvil console backend 직무 인터뷰 준비 자기소개 안녕하세요 김병태입니다. 소셜카지노 슬롯게임, 블록체인을 활용한 메타버스 게임 등 다양한 도메인을 경험해 왔습니다. NHN에서는 슬롯 백엔드 개발을 담당하여, 슬롯 게임 제작 및 아키텍처 고도화, 테스트 가능한 환경을 만들었으며 품질과 구조 개선에 강점을 갖고 있습니다. 새로운 기술과 개발 프로세스 최적화에도 관심이 많아, AI기반 슬롯게임개발 TF에도 참여하여 생산성을 향상하기 위해 노력했습니다. 고성능 게임엔진 백엔드 환경과 분산 시스템을 경험해보고자 지원하게 되었습니다. RabbitMQ 혹은 Message Queue에 대해서 경험이 있는가? 슬롯 게임 데이터의 변경을 관리하기 위해 RabbitMQ를 사용해본 경험이 있습니다. 게임 데이터는 자주 변하지 않는 데이터로 애플리케이션 메모리에 올려두고 사용했는데요, 이 때 DB 변경시점과 여러 서버에서 캐쉬 동기화 시점을 맞추기 위해 RabbitMQ를 사용한 경험이 있습니다. 이 문제를 해결하기 위해 Hibernate Post-Commit Listener를 활용해 데이터 변경을 감지했고, 변경된 엔티티 ID를 RabbitMQ를 통해 fanout 방식 브로드캐스트로 전달하도록하는 구조를 사용했습니다. 각 서버는 메시지를 수신하면, 자체적으로 유지하던 캐시 메모리를 갱신하도록 처리하여, DB와 메모리 캐시간 정합성을 유지하도록 했습니다. Hibernate Post-Commit Listener는 어떻게 동작하나요? Hibernate는 flush 단계에서 Dirty Checking을 수행합니다. 트랜잭션 커밋 직전, Hibernate Event System이 해당 엔티티 변경 이벤트를 기록합니다. DB 커밋이 실제로 성공하면, 그 때 이벤트 리스너가 실행됩니다. 리스너 내부에서는 변경된 엔티티 정보, 변경된 필드, 엔티티 ID등을 받아 후처리 로직을 자유롭게 실행할 수 있습니다. 캐시 갱신 시 데이터 불일치와 동기화 문제는 어떻게 해결했나요? 게임 서버 특성상 캐시에 대한 Read 요청이 많고, Write는 메시지 수신 시에만 간헐적으로 발생했습니다. 이런 read-heavy 환경에서는 일반적인 ReentrantReadWriteLock보다 StampedLock이 optimistic read와 write lock이 훨씬 효율적입니다. 평소에는 Optimistic Read Lock으로 캐시를 읽습니다. 대부분 Read 요청이기 때문에 lock이 거의 필요 없고, 스레드 경합도 없어서 매우 빠르게 응답 가능합니다. 캐시 갱신이 필요한 타이밍에만 Write Lock을 획득했습니다 Write Lock은 단일 스레드가 캐시 값을 변경할 때 잠깐만 획득됩니다. Read 요청은 Write 요청을 막지 않고, Write는 Read를 대기 시킵니다. 짧은 불일치에 대해서는 허용하는 수준으로 동작하게 했습니다. read는 잠깐 이전 캐시를 볼 수 있지만, 슬롯 게임 특성상 게임의 정보가 변경되는 일은 빈도가 낮은 편이었습니다. StampedLock vs ReentrantReadWriteLock은 어떻게 다른가요? 둘 다 읽기-쓰기를 분리하는 락이라는 점은 같습니다. 읽기 동시성은 높이고, 쓰기 구간에서는 배타적으로 보호하는 목적을 갖고 있습니다. ReentrantReadWriteLock은 이름 그대로 재진입성을 갖고 있어, 같은 스레드가 여러번 같은 락을 획득할 수 있습니다. 다만 읽기/쓰기는 내부적으로 락 상태를 갱신해야 하기 때문에, 읽기가 많은 상황에서는 오버헤드가 커질 수 있습니다. StampedLock은 낙관적 읽기를 제공하며 쓰기가 드문 시나리오에서는 거의 락 오버헤드가 없는 것과 비슷한 성능을 낼 수 있습니다. 또, 락을 잡을 떄 long 타입의 stamp를 반환하고, stamp를 넘겨줘야 unlock이 됩니다. 이 떄문에 특정 스레드에 종속되지 않는 대신, 코드를 잘못짜면 unlock 누락 등으로 이어지기 쉽습니다. Kafka 경험도 있나요? 슬롯 게임에서 Spin 요청에서 즉시 응답과 비동기 후 처리를 분리하기 위해, 스핀 과정에서 발생하는 미션 달성, 로그 처리 등은 ApplicationEvent로 발행하여 트랜잭션 이후에 처리되도록 했습니다. 트랜잭션 커밋된 이후 Kafka에 메시지를 발행하고, 발행 시 오류가 발생하면 Outbox 패턴을 통해, 일단 DB에 적재한 후 별도 재처리 배치 작업으로 카프카로 재전송하는 구조를 사용했습니다. 이렇게 설계함으로써 Spin API의 응답 속도는 유지하면서도 후처리 작업의 신뢰성과 재시도 가능성을 확보했습니다. 왜 RabbitMQ가 아닌 Kafka를 사용했나요? Spin 후처리 데이터는, 로그 기반 스토리지와 재처리가 가능한 Kafka를 사용했다고 생각됩니다. RabbitMQ는 실시간의 단기 메시징 중심으로 동작되지만, Kafka를 사용함으로 써 Consumer에 의해 Commit되지 않으면 메시지가 계속 남아 있어, 필요하면 과거 메시지를 재처리할 수 있도록 하기 때문입니다. 장기 보관 / 재처리 / 리플레이 구조에 더 강한 Kafka를 사용함으로써, 결과적으로 일관성을 유지할 수 있는 구조를 만들기 위해 Kafka를 사용한 것으로 판단됩니다. 또한 userId 기반으로 파티션 키를 user account uuid로 사용함으로 써, 해당 메시지에 대한 순서가 보장되도록 처리할 수 있습니다. Kafka에서 메시지는 어떤 모델을 사용했나요? (At Most Once, At Least Once, Exactly Once) 메시지는 반드시 한 번 publish 되도록 처리 Outbox 패턴을 사용하여, 전송 실패 시 재전송 할 수 있도록 처리했습니다. Consumer에대해서는 중복 발생 가능성이 열려있는 상태인 것 같은데, 이는 매 스핀마다 spinId를 부여했고, 해당 id를 통해서 중복된 메시지인지 구분할 수 있도록은 처리해두었습니다. 데이터베이스 샤딩을 해본적이 있나요? 유저 ID기반, mod 방식 샤딩을 적용해본 경험이 있습니다. 공통 DB에서 account 테이블을 관리하고, 각 유저에 대한 shard 번호를 DB에 관리하도록 했습니다. 이 때, 트랜잭션을 사용하기전에 ThreadLocal 스토리지를 활용하여 shard 번호를 설정하도록 하도록하여 동작하도록 했습니다. 그럼 Spring 트랜잭션에서 DataSource를 선택하는 흐름은 어떻게 되나요? 메서드를 호출하면 AOP Proxy에서 @Transactional를 감지하게 되고 PlatformTransactionManager.begin()을 호출하게 됩니다. 이 때 DataSourceTransactionManager.getConnection()을 호출하여 DataSource를 가져오게 됩니다. DataSource(=RoutingDataSource)에서 determineCurrentLookupKey()를 호출하게 되고 이 때 ThreadLocal에 저장된 shardKey를 활용하여 데이터 소스를 선택할 수 있도록 했습니다. 그럼 샤딩 라우팅을 어떻게 하면 최적화 할 수 있을까요? 샤드 내부 트랜잭션만 허용하고, 샤드 간 정합성은 이벤트 기반 Eventually Consistent 패턴을 활용하여 결과적으로 정합성을 유지하는 방식을 택할 것 같습니다.