프로젝트 개요

EBS 초등온은 EBS에서 제공하는 초등학생을 위한 종합 교육 플랫폼으로, 이모티브의 인지능력평가 서비스와 강화 서비스를 통합하여 제공하기로 결정되었습니다. 그러나 두 플랫폼 간 원활한 연동을 위해서는 다음과 같은 여러 기술적 과제들이 존재했습니다:

  • 플랫폼 호환성 문제: 이모티브 서비스와 EBS 초등온 간의 데이터 구조 및 인증 체계 불일치
  • 사용자 인증 통합 이슈: 각 플랫폼의 사용자 인증 체계를 통합적으로 관리할 수 있는 방안 필요
  • 보안 요구사항: EBS 측에서 요구하는 높은 수준의 보안 요구사항 충족 필요
  • 서비스 이용권 관리: 플랫폼 간 이용권 정보를 안전하게 교환하고 관리할 수 있는 시스템 필요

이러한 문제들을 해결하고, 두 서비스 간의 원활한 연동을 위해 전용 API 인터페이스 서버를 개발하게 되었습니다.

프로젝트 소개

  • 서비스명: EBS 초등온 연동 API 인터페이스 서버
  • 설명: 이모티브의 인지능력 측정 및 강화 서비스를 EBS 초등온 플랫폼에 통합하기 위한 API 인터페이스 서버
  • 인원: BE(2), FE(협력사 2)
  • 기술 스택: Java, Spring Boot, OAuth 2.0, AWS LoadBalancer, WebGL, Unity, MySQL, Redis, JWT
  • 기간: 2023.10 - 2024.01
  • 서버 스팩: AWS EC2 t3.medium (CPU 2 Core, RAM 4GB), RDS(MySQL)

기술 상세

안정적인 연결 환경 구축: AWS LoadBalancer를 활용한 고정 IP 네트워크

  • 도입 배경
    • EBS 초등온 플랫폼과의 연동을 위해서는 보안상의 이유로 고정 IP 기반의 안정적인 네트워크 연결이 필수적이었습니다.
    • 기존 이모티브 서비스는 확장성을 고려한 유동적인 IP 구조로 설계되어 있어, EBS 측의 요구사항인 화이트리스트 기반 IP 접근 제한과 충돌이 발생했습니다.
    • 서비스 간 통신 시 안정성과 보안성을 동시에 확보할 수 있는 해결책이 필요했습니다.
  • 사용 이유
    • AWS LoadBalancer는 고정된 엔드포인트를 제공하면서도 내부적으로 여러 인스턴스에 트래픽을 분산시킬 수 있어, 안정성과 확장성을 동시에 확보할 수 있습니다.
    • 보안 그룹 및 네트워크 ACL을 통한 세밀한 접근 제어가 가능하여 EBS 측의 높은 보안 요구사항을 충족할 수 있었습니다.
    • 헬스 체크 기능을 통해 장애 발생 시 자동으로 정상 인스턴스로 트래픽을 라우팅하여 서비스 중단 위험을 최소화할 수 있었습니다.
  • 구현 방식
    [EBS 초등온 서버] <---> [AWS LoadBalancer] <---> [이모티브 API 서버]
                                    |
                                    v
                            [이모티브 백업 서버]
    
  • 성과
    • 서비스 간 통신 안정성이 99.9%로 향상되어 사용자 경험 개선 및 서비스 신뢰도 향상
    • 부하 분산을 통해 트래픽 급증 시에도 안정적인 서비스 제공 가능
    • 장애 발생 시 평균 복구 시간을 3분 이내로 단축하여 서비스 연속성 확보
    • EBS 측의 보안 요구사항을 100% 충족하여 원활한 협업 관계 구축

사용자 인증 프로세스 개선: OAuth 2.0 인증 고도화

  • 도입 배경
    • 기존 이모티브 앱 서비스의 OAuth 2.0 (Apple, Google) 기반 로그인 프로세스가 EBS 초등온 웹 환경에서 호환성 문제를 일으켰습니다.
    • 로그인 실패율이 높아 사용자 이탈률이 증가하고 있었습니다.
    • 각 플랫폼 간 사용자 인증 정보를 안전하게 교환하고 관리할 수 있는 체계가 필요했습니다.
  • 사용 이유
    • OAuth 2.0은 업계 표준 프로토콜로서 다양한 플랫폼에서의 호환성이 높습니다.
    • 토큰 기반 인증 방식을 사용하여 서버 간 안전한 통신이 가능합니다.
    • 사용자 정보를 안전하게 공유하면서도 각 서비스의 독립성을 유지할 수 있습니다.
  • 구현 방식
    @Service
    class OAuthService(
        private val appleAuthProvider: AppleAuthProvider,
        private val googleAuthProvider: GoogleAuthProvider,
        private val userRepository: UserRepository,
        private val tokenService: TokenService
    ) {
        fun authenticateUser(authRequest: AuthRequest): AuthResponse {
            val oauthInfo = when (authRequest.provider) {
                AuthProvider.APPLE -> appleAuthProvider.validateToken(authRequest.token)
                AuthProvider.GOOGLE -> googleAuthProvider.validateToken(authRequest.token)
                else -> throw InvalidAuthProviderException()
            }
              
            // 사용자 정보 조회 또는 생성
            val user = userRepository.findByProviderAndProviderId(
                authRequest.provider, 
                oauthInfo.providerId
            ) ?: createNewUser(authRequest.provider, oauthInfo)
              
            // 플랫폼 간 연동을 위한, 통합 토큰 생성 및 반환
            return AuthResponse(
                accessToken = tokenService.generateAccessToken(user),
                refreshToken = tokenService.generateRefreshToken(user),
                expiresIn = tokenService.getAccessTokenExpiration()
            )
        }
          
        // 사용자 정보가 없을 경우 신규 생성
        private fun createNewUser(provider: AuthProvider, oauthInfo: OAuthInfo): User {
            val newUser = User(
                provider = provider,
                providerId = oauthInfo.providerId,
                email = oauthInfo.email,
                name = oauthInfo.name
            )
            return userRepository.save(newUser)
        }
    }
    
  • 성과
    • 로그인 실패율을 78% 이상 감소시켜 사용자 경험 대폭 개선
    • 인증 프로세스 평균 처리 시간을 2.3초에서 0.8초로 단축
    • 사용자 인증 데이터 보안성 강화로 개인정보 보호 수준 향상
    • 플랫폼 간 원활한 사용자 전환으로 서비스 이용률 증가

비네트워크 기반 이용권 정보 처리 자동화: 엑셀 기반 데이터 처리 시스템

  • 도입 배경
    • EBS 초등온 서비스와의 연동 과정에서 보안 정책상 일부 민감한 사용자 이용권 정보는 네트워크를 통한 직접 전송이 불가능했습니다.
    • 이용권 정보 처리를 위해 운영팀이 수작업으로 엑셀 파일을 통해 데이터를 입력하고 검증하는 과정이 필요했으며, 이 과정에서 많은 시간과 인력이 소요되었습니다.
    • 데이터 처리 과정에서 발생하는 휴먼 에러로 인한 서비스 불안정성 문제가 존재했습니다.
  • 사용 이유
    • 엑셀 파일은 비개발자인 운영팀도 쉽게 사용할 수 있는 친숙한 인터페이스를 제공합니다.
    • Java의 Apache POI 라이브러리를 활용하여 엑셀 파일 처리를 자동화할 수 있었습니다.
    • 데이터 검증 및 가공 로직을 서버에 구현함으로써 휴먼 에러를 최소화할 수 있었습니다.
  • 구현 방식
    @Service
    public class LicenseImportService {
        
        private final LicenseRepository licenseRepository;
        private final UserRepository userRepository;
          
        public LicenseImportResult importLicensesFromExcel(MultipartFile excelFile) throws IOException {
            Workbook workbook = WorkbookFactory.create(excelFile.getInputStream());
            Sheet sheet = workbook.getSheetAt(0);
              
            LicenseImportResult result = new LicenseImportResult();
            List<License> validLicenses = new ArrayList<>();
              
            // 엑셀 데이터 처리 및 검증
            for (int i = 1; i <= sheet.getLastRowNum(); i++) {
                Row row = sheet.getRow(i);
                try {
                    License license = extractLicenseFromRow(row);
                    if (validateLicense(license)) {
                        validLicenses.add(license);
                    } else {
                        result.addInvalidRow(i, "유효하지 않은 라이센스 정보");
                    }
                } catch (Exception e) {
                    result.addInvalidRow(i, e.getMessage());
                }
            }
              
            // 유효한 라이센스 정보만 저장
            if (!validLicenses.isEmpty()) {
                licenseRepository.saveAll(validLicenses);
                result.setSuccessCount(validLicenses.size());
            }
              
            return result;
        }
          
        private License extractLicenseFromRow(Row row) {
            // 엑셀 행에서 라이센스 정보 추출 및 객체 생성
            String userIdentifier = getStringCellValue(row.getCell(0));
            String productCode = getStringCellValue(row.getCell(1));
            Date startDate = row.getCell(2).getDateCellValue();
            Date endDate = row.getCell(3).getDateCellValue();
              
            User user = userRepository.findByIdentifier(userIdentifier)
                .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + userIdentifier));
              
            return new License(user, productCode, startDate, endDate);
        }
          
        private boolean validateLicense(License license) {
            // 라이센스 유효성 검증 로직
            return license.getStartDate().before(license.getEndDate()) 
                   && license.getUser() != null 
                   && StringUtils.hasText(license.getProductCode());
        }
    }
    
  • 성과
    • 이용권 정보 처리 시간을 1시간에서 10분 이내로 단축하여 운영 효율성 85% 향상
    • 데이터 처리 과정에서의 오류율을 95% 감소시켜 서비스 안정성 제고
    • 운영팀의 업무 부담 경감 및 인력 리소스의 효율적 재배치 가능
    • 대량의 이용권 데이터도 빠르게 처리할 수 있어 서비스 확장성 확보

WebGL(Unity) 도메인 인증 및 만료 처리 프로세스 설계

  • 도입 배경
    • 이모티브의 인지능력 강화 서비스는 Unity 기반의 WebGL 환경에서 제공되는데, EBS 초등온 도메인에서 이를 안전하게 호스팅하고 실행할 수 있는 체계가 필요했습니다.
    • Unity WebGL 콘텐츠는 도메인 제한이 필요한데, 이를 자동화할 수 있는 시스템이 없었습니다.
    • 이용권 만료 시 자동으로 서비스 접근을 제한할 수 있는 메커니즘이 필요했습니다.
  • 사용 이유
    • Unity의 WebGL 빌드는 높은 수준의 인터랙티브 콘텐츠를 웹 환경에서 제공할 수 있어, 교육용 콘텐츠로 적합합니다.
    • 서버 측에서 도메인 인증 로직을 구현하여 허가된 도메인에서만 콘텐츠 접근이 가능하도록 통제할 수 있습니다.
    • JWT 기반의 토큰 시스템을 활용하여 사용자의 이용권 상태를 실시간으로 검증할 수 있습니다.
  • 구현 방식
    @RestController
    @RequestMapping("/api/webgl")
    public class WebGLAuthController {
          
        private final DomainService domainService;
        private final LicenseService licenseService;
        private final JwtTokenProvider tokenProvider;
          
        @PostMapping("/authenticate")
        public ResponseEntity<AuthResponse> authenticateDomain(
            @RequestHeader("Origin") String origin,
            @RequestBody WebGLAuthRequest request
        ) {
            // 도메인 검증
            if (!domainService.isAllowedDomain(origin)) {
                throw new UnauthorizedDomainException("허가되지 않은 도메인에서의 접근입니다.");
            }
              
            // 사용자 이용권 검증
            User user = tokenProvider.getUserFromToken(request.getAccessToken());
            boolean hasValidLicense = licenseService.hasValidLicense(user.getId(), request.getProductCode());
              
            if (!hasValidLicense) {
                throw new LicenseExpiredException("유효한 이용권이 없습니다.");
            }
              
            // 인증 성공 시 WebGL 접근 토큰 발급
            String webglToken = tokenProvider.generateWebGLToken(user, request.getProductCode());
              
            return ResponseEntity.ok(new AuthResponse(webglToken));
        }
          
        @PostMapping("/verify-session")
        public ResponseEntity<VerificationResponse> verifySession(
            @RequestHeader("Authorization") String bearerToken
        ) {
            // WebGL 세션 유효성 검증 (주기적으로 클라이언트에서 호출)
            String token = bearerToken.substring(7);
              
            if (!tokenProvider.validateToken(token)) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new VerificationResponse(false, "만료된 세션입니다."));
            }
              
            User user = tokenProvider.getUserFromToken(token);
            String productCode = tokenProvider.getProductCodeFromToken(token);
              
            boolean isValid = licenseService.hasValidLicense(user.getId(), productCode);
              
            return ResponseEntity.ok(new VerificationResponse(isValid, isValid ? "유효한 세션입니다." : "이용권이 만료되었습니다."));
        }
    }
    
  • 성과
    • 도메인 인증 프로세스 자동화로 보안성 강화 및 불법 접근 시도 100% 차단
    • 이용권 만료 시 자동 접근 제한으로 라이센스 관리 효율화
    • Unity WebGL 콘텐츠의 안정적 제공으로 사용자 만족도 향상
    • 인증 과정의 자동화로 운영 부담 감소 및 관리 효율성 증대

머신 러닝 데이터 추출 구조 개선

  • 도입 배경
    • 이모티브 서비스는 사용자의 인지능력 데이터를 분석하여 개인화된 강화 프로그램을 제공하는데, 이 과정에서 대량의 데이터 처리가 필요했습니다.
    • 기존 데이터 추출 방식은 단일 쿼리로 모든 데이터를 가져와 메모리에서 처리하는 방식이었으며, 이는 데이터 증가에 따른 성능 저하 문제를 야기했습니다.
    • EBS 초등온과의 연동으로 인해 데이터 처리량이 대폭 증가할 것으로 예상되어, 확장성 있는 구조로의 개선이 필요했습니다.
  • 사용 이유
    • 페이징 처리와 비동기 처리를 결합하여 대용량 데이터도 효율적으로 처리할 수 있는 구조를 구현할 수 있습니다.
    • 스트림 기반 데이터 처리는 메모리 사용량을 최적화하여 시스템 안정성을 높일 수 있습니다.
    • 분산 처리 방식을 도입하여 처리 속도를 향상시킬 수 있습니다.
  • 구현 방식
    @Service
    public class MachineLearningDataService {
          
        private final UserActivityRepository userActivityRepository;
        private final DataProcessingService dataProcessingService;
        private final ExecutorService executorService;
          
        public MachineLearningDataService() {
            // 데이터 처리를 위한 스레드 풀 생성
            this.executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
            );
        }
          
        public CompletableFuture<ProcessingResult> extractAndProcessData(DataExtractionRequest request) {
            // 전체 데이터 건수 조회
            long totalCount = userActivityRepository.countByDateBetween(
                request.getStartDate(), 
                request.getEndDate()
            );
              
            // 페이징 처리를 위한 설정
            int pageSize = 1000;
            int totalPages = (int) Math.ceil((double) totalCount / pageSize);
              
            List<CompletableFuture<List<ProcessedData>>> futures = new ArrayList<>();
              
            // 페이지별로 비동기 처리
            for (int page = 0; page < totalPages; page++) {
                final int currentPage = page;
                CompletableFuture<List<ProcessedData>> future = CompletableFuture.supplyAsync(() -> {
                    PageRequest pageRequest = PageRequest.of(currentPage, pageSize);
                    List<UserActivity> activities = userActivityRepository.findByDateBetween(
                        request.getStartDate(), 
                        request.getEndDate(), 
                        pageRequest
                    );
                      
                    return dataProcessingService.processActivities(activities);
                }, executorService);
                  
                futures.add(future);
            }
              
            // 모든 비동기 작업이 완료되면 결과 취합
            return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                .thenApply(v -> {
                    List<ProcessedData> allProcessedData = futures.stream()
                        .map(CompletableFuture::join)
                        .flatMap(List::stream)
                        .collect(Collectors.toList());
                      
                    return new ProcessingResult(allProcessedData, totalCount);
                });
        }
          
        // 리소스 해제
        @PreDestroy
        public void destroy() {
            if (executorService != null) {
                executorService.shutdown();
            }
        }
    }
    
  • 성과
    • 데이터 처리 속도를 56.8% 향상시켜 분석 결과 제공 시간 단축
    • 메모리 사용량을 최대 70% 절감하여 서버 안정성 강화
    • 동시 처리 가능한 사용자 수를 3배 이상 증가시켜 서비스 확장성 확보
    • 시스템 장애 발생률을 85% 감소시켜 서비스 신뢰성 제고

프로젝트 결과 및 성과

  • EBS 초등온과의 성공적인 서비스 통합: 이모티브의 인지능력 측정 및 강화 서비스가 EBS 초등온 플랫폼에 성공적으로 통합되어 서비스 중
  • 사용자 경험 개선: 로그인 실패율 78% 감소, 서비스 응답 시간 평균 56.8% 단축으로 인한 사용자 만족도 향상
  • 운영 효율성 제고: 이용권 정보 처리 시간 83% 단축, 인증 프로세스 자동화를 통한 운영 부담 경감
  • 시스템 안정성 강화: 서비스 장애 발생률 85% 감소, 고가용성 아키텍처 구축으로 서비스 연속성 확보
  • 협업 프로세스 개선: EBS 측과의 효율적인 협업 체계 구축으로 추가 기능 개발 및 유지보수 효율성 제고

프로젝트를 통해 배운 점

이 프로젝트를 통해 서로 다른 플랫폼 간의 연동에서 발생할 수 있는 다양한 기술적 과제들을 해결하며, 특히 다음과 같은 부분에서 깊은 이해와 경험을 얻을 수 있었습니다:

  1. 분산 시스템 설계: 여러 서비스 간의 효율적인 통신 및 데이터 일관성 유지 방법
  2. 보안 인증 체계: OAuth 2.0 기반의 견고한 인증 시스템 구축 및 최적화 방법
  3. 대용량 데이터 처리: 페이징 및 비동기 처리를 활용한 대용량 데이터 효율적 처리 기법
  4. 자동화 시스템 구축: 반복적인 운영 작업을 자동화하여 효율성을 높이는 방법

이러한 경험은 향후 다양한 서비스 간 연동 및 대규모 시스템 설계에 있어 귀중한 자산이 될 것입니다.