본문으로 건너뛰기

📨 사내 메시지 발송 관리 서비스 마이그레이션

레거시 SMS/알림톡 관리자 웹서비스 마이그레이션 및 기능 보강 수기로 운영되던 조직 데이터 관리를 시스템화하고, 멀티 데이터소스 환경에서 발송 이력 조회를 구현한 프로젝트


목차​

  1. 프로젝트 개요
  2. 시스템 아키텍처
  3. 기술 스택
  4. 담당 역할
  5. 핵심 기술 도전
  6. 주요 성과
  7. 배운 점 & 회고

📋 프로젝트 개요​

항목내용
기간2026.03.03 ~ 2026.06.02 (약 3개월)
소속웅진 · 그룹IT혁신팀 (백엔드 인턴)
역할Java 백엔드 개발 및 관리자 화면 구현
대상SMS/알림톡 발송 내역을 관리하는 사내 웹서비스

핵심 문제 인식​

기존 서비스의 일부 코드와 운영 화면, 검증용 데이터베이스를 바탕으로 기능과 데이터 구조를 분석했습니다. 과제의 목표는 기술 스택만 교체하는 것이 아니라 기존 운영 과정의 문제를 찾아 더 유지하기 쉬운 구조로 개선하는 것이었습니다.

  • ❌ XML 설정 과다, 뷰 로직과 비즈니스 로직 혼재로 유지보수성 저하
  • ❌ 회사/부서/사용자 데이터를 운영자가 SQL로 직접 수기 관리 → 휴먼 에러로 인한 정합성 오류 누적
  • ❌ 기존 인증 데이터가 신규 저장 정책과 다른 형식으로 관리됨
  • ❌ 발송 이력(Oracle)과 메타데이터(MariaDB)가 분리되어 통합 조회 어려움
  • ❌ 신규 발송 이력이 계속 유입되어 offset 페이지 이동 중 조회 기준이 변동

해결 방향​

  • ✅ Spring Boot + Thymeleaf + JPA/QueryDSL로 마이그레이션 (JSON API 기반 뷰 분리)
  • ✅ 웹 UI 기반 조직 데이터 CRUD 시스템화 + 계단식 soft-delete 자동화
  • ✅ 기존 인증 데이터를 신규 저장 정책에 맞게 해시 기반으로 이관
  • ✅ 멀티 데이터소스 인메모리 조인으로 발송 이력 통합 조회
  • ✅ 최초 조회 시점의 앵커를 고정하고 신규 데이터 건수를 별도로 안내

🏗️ 시스템 아키텍처​

┌──────────────────────────────────────────────┐
│ Spring Boot Admin (msg_manage) │
│ Thymeleaf(껍데기) + Alpine.js + Axios │
└───────────────┬───────────────────────────────┘
│ JSON API
┌────────┴─────────┐
│ Service / QueryDSL │
└───┬───────────┬────┘
메타데이터 │ │ 발송 이력
┌───────▼────┐ ┌───▼─────────┐
│ MariaDB │ │ Oracle │
│ 회사/부서 │ │ 발송 내역 │
│ 채널/사용자 │ │ │
└────────────┘ └─────────────┘
▲ ▲
└──── LEGACY_ID 로 ─┘
애플리케이션 레벨 IN절 인메모리 조인
  • 핵심 설계 원칙
    • 뷰-서버 분리: Thymeleaf는 HTML 껍데기, 데이터는 JSON API로 전달 → 추후 CSR 전환 가능
    • 메타데이터 중앙화: 저장 트랜잭션 복잡도를 줄이기 위해 메타데이터를 단일 DB에서 관리
    • 레거시 호환: 외부 발송 시스템이 참조하는 기존 ID를 LEGACY_ID로 보존

🔧 기술 스택​

Backend​

기술선택 이유
Java + Spring Boot프로젝트 표준 기술 스택
Thymeleaf기술 제약사항으로 지정, 레이아웃/프래그먼트 역할
JPA + QueryDSL기본 CRUD 자동화 + 타입 안전한 동적 쿼리
MariaDB조직/사용자 메타데이터 (중앙 관리)
Oracle기존 SMS 발송 이력 (레거시 운영 DB)

Frontend​

  • Alpine.js: DOM 직접 제어 시 늘어나는 보일러플레이트를 선언적 방식으로 축소
  • Axios: 인터셉터로 인증 만료/커스텀 예외 처리 중앙화

🎯 담당 역할​

1. Thymeleaf 화면과 JSON API의 책임 분리​

Thymeleaf는 레이아웃과 화면 골격에 사용하고 목록·상세 데이터는 JSON API로 제공했습니다. Alpine.js로 모달, 선택 상태, 이벤트를 선언적으로 처리해 반복적인 DOM 조작을 줄였고, 추후 클라이언트 렌더링 방식을 도입하더라도 API를 재사용할 수 있도록 화면과 데이터 전달 책임을 분리했습니다.

2. 메타데이터 일원화와 서비스 레벨 조인​

기존에는 동일한 메타데이터를 Oracle과 MariaDB에 각각 등록했습니다. 이를 그대로 시스템화하면 두 DB의 부분 실패와 동기화를 관리해야 하므로 등록·수정은 MariaDB로 일원화하고, 발송 이력 조회 시 두 DB의 결과를 서비스 계층에서 연결했습니다.

검색 조건 사용 시,​

// 1) MariaDB: 회사·사용자 조건에 맞는 후보 식별자 조회
List<String> candidateIds = userRepository.findLegacyIdsByCondition(condition);

// 2) Oracle: 후보 식별자로 발송 이력 페이징
Page<SendHistory> histories = sendHistoryRepository
.findByLegacyIdIn(candidateIds, pageable);

// 3) MariaDB: 필요한 메타데이터를 Map으로 구성
Map<String, User> userMap = userRepository.findByLegacyIdIn(candidateIds).stream()
.collect(Collectors.toMap(User::getLegacyId, Function.identity()));

// 4) 서비스 계층에서 결합
List<SendHistoryView> result = histories.getContent().stream()
.map(h -> SendHistoryView.of(h, userMap.get(h.getLegacyId())))
.toList();

기본 조회 시,​

// 1) Oracle: 발송 이력 조회 후, 등장하는 사용자 식별자만 추출
List<String> legacyIds = histories.stream()
.map(SendHistory::getLegacyId)
.distinct()
.toList();

// 2) MariaDB: IN 절로 한 번에 조회 → Map 변환
Map<String, User> userMap = userRepository.findByLegacyIdIn(legacyIds).stream()
.collect(Collectors.toMap(User::getLegacyId, Function.identity()));

// 3) 메모리에서 매핑
List<SendHistoryView> result = histories.stream()
.map(h -> SendHistoryView.of(h, userMap.get(h.getLegacyId())))
.toList();

3. 조직 데이터 CRUD 시스템화 + 계단식 soft-delete​

비활성화는 UPDATE 연산이라 DB FK cascade로 처리할 수 없어, 애플리케이션 레벨에서 상위 → 하위 계단식 비활성화를 자동 처리했습니다 (QueryDSL bulk UPDATE).

4. 앵커 기반 발송 이력 조회​

최초 조회 시점의 정렬 기준을 앵커로 저장하고, 같은 조회 흐름에서는 앵커 이전 데이터만 페이지 대상으로 제한했습니다. 앵커 이후 신규 데이터는 별도 count API로 표시하고 사용자가 갱신할 때 현재 시점으로 기준을 다시 설정했습니다.

5. 데이터 마이그레이션 (보안 / 운영 규칙)​

신규 테이블 이관 과정에서 기존 인증 데이터를 신규 저장 정책에 맞게 해시 기반으로 변환했습니다. 외부 시스템이 참조하는 식별자는 별도 LEGACY_ID로 유지해 데이터 연결 관계를 보존했습니다.


🔥 핵심 기술 도전​

1️⃣ 서로 다른 두 DB를 조인할 수 없을 때​

문제: 메타데이터(MariaDB)와 발송 이력(Oracle)이 물리적으로 분리되어 단일 쿼리 조인 불가

해결: 메타데이터를 단일 DB에 중앙화하고, 조회 시 IN절 기반 인메모리 조인 채택. 데이터 규모(회사당 사용자 최대 300건, 전체 1,000건 미만)를 근거로 성능 부담이 허용 가능하다고 판단

상세 분석 보기 →

2️⃣ 수기 운영 데이터의 정합성 복구​

문제: 부서 245건 중 상·하위 상태 불일치 87건과 잘못된 상위 데이터 참조 2건 확인

해결: 웹 기반 CRUD와 계단식 soft-delete를 적용해 직접 SQL 수정으로 발생할 수 있는 입력 오류 가능성을 줄임

3️⃣ 계속 추가되는 발송 이력의 페이지 기준 유지​

문제: 최신순 offset 페이징 중 신규 데이터가 들어오면 기존 데이터가 다음 페이지로 밀려 중복되거나 누락된 것처럼 보일 수 있음

해결: 최초 조회 시점의 정렬 기준을 앵커로 고정하고 앵커 이후 데이터는 신규 건수로 별도 표시

상세 분석 보기 →

4️⃣ 화면과 데이터 전달 책임 분리​

문제: Model과 Fragment 중심 데이터 전달 및 바닐라 JavaScript DOM 처리의 결합도와 반복 코드 증가

해결: Thymeleaf는 화면 골격, 데이터는 JSON API, UI 상태와 이벤트는 Alpine.js가 담당하도록 분리

5️⃣ 레거시 호환성과 비밀번호 보안​

문제: 외부 시스템이 기존 식별자를 참조하는 상황에서 인증 데이터를 신규 저장 정책에 맞게 이관해야 함

해결: LEGACY_ID로 연결을 유지하고 기존 인증 데이터를 해시 기반으로 이관


📊 주요 성과​

  • ✅ 레거시 기능을 분석해 화면 골격과 JSON API의 책임을 분리
  • ✅ 멀티 DB 발송 이력 통합 조회 구현 (저장 트랜잭션 복잡도 최소화)
  • ✅ 부서 245건 중 상태 불일치 87건과 잘못된 상위 데이터 참조 2건 정비
  • ✅ 기존 인증 데이터를 신규 저장 정책에 맞게 해시 기반으로 이관
  • ✅ 앵커 기반 조회로 신규 데이터 유입 중에도 페이지 조회 기준 유지

💡 배운 점 & 회고​

기술적 배움​

  • 트레이드오프 기반 설계: "인메모리 조인이 좋다"가 아니라 "이 데이터 규모에서는 이게 맞다"를 근거로 설명하는 경험
  • 레거시 호환성: 최신 프레임워크가 생성하는 SQL이 구버전 DB에서 항상 통하지 않는다는 점
  • soft-delete와 cascade: UPDATE 기반 비활성화는 DB cascade로 처리 불가 → 애플리케이션 레벨 설계 필요

회고 (아쉬웠던 점)​

  • 권한 설계 시 역할(Role) 중심으로 구현했으나, 기존 시스템은 메뉴별 세부 권한(Permission) 할당 방식이었음. 기존 시스템 분석이 부족했던 점이 회고 포인트이며, Role + Permission 조합 설계가 더 적합했을 것
  • SSE 기반 이상 탐지 알림은 일정상 미구현으로 남김

사내 프로젝트 특성상 소스 코드 및 일부 상세 구성은 비공개입니다.