무인 바둑/카드방 관리 시스템 개요 및 기술 과제
최근 무인 운영 방식의 바둑/카드방은 자동화된 예약, 원격 출입 통제, 정산 시스템을 통해 운영 효율성을 높이고 사용자 편의를 제공하는 새로운 형태의 오프라인 엔터테인먼트로 자리 잡고 있습니다. 이러한 시스템을 안정적이고 효율적으로 구축하기 위해서는 몇 가지 기술적 도전 과제를 해결해야 합니다.
- 기기 및 서비스 간 실시간 연동: 도어락, 카메라와 같은 기기와 중앙 시스템 간의 실시간 통신 및 연동이 필수적입니다. 이는 출입 관리 및 요금 정산의 정확성을 보장하는 핵심 요소입니다.
- 고동시성 예약 및 결제 처리: 피크 시간대에는 수많은 예약 및 결제 요청이 동시에 발생할 수 있습니다. 시스템은 이러한 고동시성 트랜잭션을 안정적으로 처리할 수 있는 능력을 갖춰야 합니다.
- 예외 처리 및 데이터 일관성: 결제 성공 후 출입이 불가능하거나 그 반대의 상황이 발생하지 않도록 결제와 출입 프로세스 간의 데이터 일관성을 철저히 유지해야 합니다.
본 시스템은 다음과 같은 기술 스택을 기반으로 설계 및 구현되었습니다:
- 백엔드 서비스: SpringBoot + MyBatis-Plus + MySQL
- 사용자 클라이언트: UniApp (Vue 문법 기반)
- 관리자/유지보수 클라이언트: UniApp (Vue 문법 기반)
- 웹 관리자 페이지: Vue + ElementUI
- 캐시: Redis (기기 상태 및 예약 정보 조회 성능 향상)
- 메시지 큐: RabbitMQ (예약, 결제, 출입 요청 등의 비동기 처리로 시스템 안정성 확보)
시스템 아키텍처 및 기술 스택 선정
시스템 아키텍처 설계
무인 바둑/카드방 시스템은 분산 계층형 아키텍처를 채택하여 다음과 같은 핵심 모듈로 구성됩니다.
- 사용자 클라이언트 (UniApp)
- 기능: 사용자가 앱을 통해 공간 예약, 출입 제어, 결제, 정산을 수행합니다.
- 기술 스택: UniApp (Vue 문법)을 사용하여 iOS 및 Android 크로스 플랫폼 호환성을 제공합니다.
- 관리자/유지보수 클라이언트 (UniApp)
- 기능: 관리자나 유지보수 담당자가 기기 점검, 장애 처리, 공간 상태 모니터링을 수행합니다.
- 기술 스택: UniApp 개발로 다양한 플랫폼을 지원합니다.
- 백엔드 서비스 (SpringBoot + MyBatis-Plus + MySQL)
- 기능: 사용자 예약, 공간 상태 관리, 출입 기록, 결제 처리, 기기 연동 등의 비즈니스 로직을 담당합니다.
- 기술 스택:
- SpringBoot: RESTful API를 제공합니다.
- MyBatis-Plus: 데이터베이스 작업을 간소화합니다.
- Redis: 공간 상태 및 예약 정보를 캐시하여 데이터베이스 부하를 줄입니다.
- RabbitMQ: 결제 및 출입 명령을 비동기적으로 처리하여 데이터 일관성 및 시스템 안정성을 보장합니다.
- 웹 관리자 페이지 (Vue + ElementUI)
- 기능: 공간, 예약, 사용자, 결제 정보 관리 및 데이터 통계 및 시각화 기능을 제공합니다.
- 기술 스택: Vue 프레임워크와 ElementUI를 사용하여 관리자 인터페이스를 구축합니다.
아키텍처 흐름 설명:
- 클라이언트 상호작용: 사용자 및 관리자 클라이언트는 UniApp을 통해 백엔드 서비스 API를 호출합니다.
- 백엔드 서비스: SpringBoot는 예약, 결제, 출입 요청을 처리하는 API를 제공하며, Redis는 공간 상태를 캐시하여 데이터베이스 조회 부담을 줄입니다.
- 비동기 처리: RabbitMQ는 비동기 예약 처리, 결제 및 출입 명령의 신뢰성 있는 전달을 보장합니다.
- 관리 시스템: Vue + ElementUI 기반의 웹 관리자 페이지는 예약 및 기기 관리를 수행합니다.
기술 스택 선정
| 모듈 | 기술 스택 | 주요 기능 |
|---|---|---|
| 사용자 클라이언트 | UniApp (Vue 문법) | 크로스 플랫폼 앱을 통한 공간 예약 및 출입 제어 |
| 관리자/유지보수 클라이언트 | UniApp (Vue 문법) | 기기 유지보수 및 공간 관리 |
| 웹 관리자 페이지 | Vue + ElementUI | 예약, 기기 관리 및 데이터 통계 |
| 백엔드 서비스 | SpringBoot + MyBatis-Plus | 예약, 결제, 출입 비즈니스 로직 처리 |
| 데이터 저장소 | MySQL | 회원, 예약, 공간 데이터 저장 |
| 캐시 | Redis | 공간 상태 및 예약 정보 캐싱 |
| 메시지 큐 | RabbitMQ | 비동기 예약 및 출입 처리 |
| 로그 및 모니터링 | ELK (Elasticsearch + Logstash + Kibana) | 시스템 로그 모니터링 및 검색 |
핵심 기능 구현: 사용자 예약 및 자동 출입
데이터베이스 설계
MySQL에 다음과 같은 핵심 테이블을 생성합니다:
-- 회원 정보 테이블
CREATE TABLE `member_info` (
`member_id` INT PRIMARY KEY AUTO_INCREMENT,
`member_name` VARCHAR(100) NOT NULL,
`contact_number` VARCHAR(20) UNIQUE NOT NULL,
`registration_date` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 공간 정보 테이블
CREATE TABLE `play_space` (
`space_id` INT PRIMARY KEY AUTO_INCREMENT,
`space_name` VARCHAR(100) NOT NULL,
`current_status` ENUM('AVAILABLE', 'OCCUPIED') DEFAULT 'AVAILABLE',
`hourly_rate` DECIMAL(12,2) NOT NULL,
`last_occupied_time` DATETIME
);
-- 예약 정보 테이블
CREATE TABLE `space_reservation` (
`reservation_id` INT PRIMARY KEY AUTO_INCREMENT,
`fk_member_id` INT NOT NULL,
`fk_space_id` INT NOT NULL,
`reservation_status` ENUM('REQUESTED', 'ACTIVE', 'FINISHED', 'CANCELLED') DEFAULT 'REQUESTED',
`total_amount` DECIMAL(12,2) NOT NULL,
`reserved_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`fk_member_id`) REFERENCES `member_info`(`member_id`),
FOREIGN KEY (`fk_space_id`) REFERENCES `play_space`(`space_id`)
);
백엔드 서비스 구현
엔티티 클래스 및 MyBatis-Plus 리포지토리
PlaySpace.java
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("play_space")
public class PlaySpace {
@TableId // 자동 증가 ID로 가정
private Integer spaceId;
private String spaceName;
private String currentStatus; // DB의 ENUM과 매핑
private BigDecimal hourlyRate;
private LocalDateTime lastOccupiedTime;
}
PlaySpaceRepository.java
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@Mapper
public interface PlaySpaceRepository extends BaseMapper<PlaySpace> {
// 필요한 경우 추가적인 쿼리 메소드를 정의할 수 있습니다.
}
SpaceReservation.java
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("space_reservation")
public class SpaceReservation {
@TableId
private Integer reservationId;
private Integer fkMemberId;
private Integer fkSpaceId;
private String reservationStatus;
private BigDecimal totalAmount;
private LocalDateTime reservedAt;
private LocalDateTime updatedAt;
}
SpaceReservationRepository.java
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@Mapper
public interface SpaceReservationRepository extends BaseMapper<SpaceReservation> {
}
서비스 계층
SpaceBookingService.java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class SpaceBookingService {
private final SpaceReservationRepository reservationRepository;
private final PlaySpaceRepository playSpaceRepository;
@Autowired
public SpaceBookingService(SpaceReservationRepository reservationRepository, PlaySpaceRepository playSpaceRepository) {
this.reservationRepository = reservationRepository;
this.playSpaceRepository = playSpaceRepository;
}
@Transactional
public String bookSpace(Integer memberId, Integer spaceId, BigDecimal initialAmount) {
Optional<PlaySpace> optionalSpace = playSpaceRepository.selectById(spaceId);
if (optionalSpace.isEmpty()) {
throw new IllegalArgumentException("요청된 공간을 찾을 수 없습니다.");
}
PlaySpace space = optionalSpace.get();
if (!"AVAILABLE".equals(space.getCurrentStatus())) {
throw new IllegalStateException("해당 공간은 현재 예약할 수 없습니다.");
}
SpaceReservation newReservation = new SpaceReservation();
newReservation.setFkMemberId(memberId);
newReservation.setFkSpaceId(spaceId);
newReservation.setReservationStatus("REQUESTED"); // 초기 상태
newReservation.setTotalAmount(initialAmount);
newReservation.setReservedAt(LocalDateTime.now());
reservationRepository.insert(newReservation);
space.setCurrentStatus("OCCUPIED"); // 예약 요청 후 즉시 점유 상태로 변경
space.setLastOccupiedTime(LocalDateTime.now());
playSpaceRepository.updateById(space);
return "공간 예약 요청이 성공적으로 접수되었습니다.";
}
}
컨트롤러 계층
SpaceReservationController.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
@RestController
@RequestMapping("/api/space/reservations") // 변경된 API 경로
public class SpaceReservationController {
private final SpaceBookingService spaceBookingService;
@Autowired
public SpaceReservationController(SpaceBookingService spaceBookingService) {
this.spaceBookingService = spaceBookingService;
}
@PostMapping("/book") // 변경된 엔드포인트
public ResponseEntity<String> createSpaceReservation(
@RequestParam("memberId") Integer memberId,
@RequestParam("spaceId") Integer spaceId,
@RequestParam("amount") BigDecimal amount) { // 예약 금액 추가
try {
String result = spaceBookingService.bookSpace(memberId, spaceId, amount);
return new ResponseEntity<>(result, HttpStatus.CREATED);
} catch (IllegalArgumentException | IllegalStateException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<>("예약 처리 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
사용자 클라이언트 (UniApp)
// 페이지 로드 시 예약 요청 예시
onLoad() {
const memberIdentifier = 5; // 예시 회원 ID
const targetSpaceId = 205; // 예시 공간 ID
const reservationAmount = 50.00; // 예시 금액 (실제 결제 로직은 더 복잡함)
uni.request({
url: 'http://localhost:8080/api/space/reservations/book', // 변경된 API 경로
method: 'POST',
data: {
memberId: memberIdentifier,
spaceId: targetSpaceId,
amount: reservationAmount
},
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
success: (response) => {
if (response.statusCode === 201) { // 201 Created
console.log('공간 예약 요청 성공:', response.data);
// 성공 시 추가 로직 (예: 결제 페이지로 이동, 예약 목록 업데이트 등)
} else {
console.error('공간 예약 실패:', response.data);
// 실패 시 에러 메시지 표시
}
},
fail: (error) => {
console.error('네트워크 또는 서버 오류:', error);
// 네트워크 오류 처리
}
});
}
성능 최적화 전략
무인 시스템의 안정적인 운영을 위해 다음과 같은 성능 최적화 전략을 적용할 수 있습니다.
| 최적화 방안 | 장점 | 고려사항 |
|---|---|---|
| Redis를 활용한 공간 상태 캐싱 | 데이터베이스 조회 부하를 크게 감소시킵니다. | 캐시와 데이터베이스 간의 데이터 일관성 유지 전략이 필요합니다. |
| RabbitMQ를 통한 비동기 출입 처리 | 백엔드 서비스의 응답 속도를 향상시키고, 동시성 처리 능력을 높입니다. | 메시지 큐 시스템의 장애 발생 시 데이터 손실 가능성을 대비해야 합니다. |