Auto-Cert: 사내 해커톤
1. 프로젝트 개요
| 항목 | 내용 |
|---|---|
| 프로젝트명 | Auto-Cert (SSL/TLS 인증서 관리 자동화 솔루션) |
| 기간 | 2025.10 ~ 2025.11 (약 2개월) |
| 인원 | 프론트엔드 1명 백엔드 2명 |
| 역할 | 백엔드 개발 (인증서 생명주기 관리, 보안 아키텍처, AI 채팅 통합) |
| 저장소 | GitHub 멀티 모듈 모노레포 (총 커밋 88건, Java 소스 129개 파일, 테스트 18개 파일) |
목표
기업 환경에서 수십~수백 개의 SSL/TLS 인증서를 수동으로 관리하면 만료 사고, 배포 누락, 갱신 지연 등의 운영 리스크가 발생합니다. Auto-Cert는 ACME 프로토콜(Let’s Encrypt)을 통한 인증서 발급/갱신, 대상 서버로의 SSH 배포, 웹서버 재기동, 이메일 알림까지 인증서의 전체 생명주기를 하나의 플랫폼에서 자동화하는 것을 목표로 합니다. 추가로 LLM 기반 AI 채팅 인터페이스를 통해 비전문가도 자연어로 인증서를 관리할 수 있도록 접근성을 높였습니다.
2. 사용 기술 · 지식 · 도구
핵심 기술 스택
| 계층 | 기술 |
|---|---|
| Language / Runtime | Java 21 |
| Framework | Spring Boot 3.5.7 |
| Build | Gradle 8.14.3 (Groovy DSL), 멀티 모듈 구성 |
| Database | PostgreSQL 16, Flyway 11.17 (마이그레이션 11건) |
| Security | Spring Security, OAuth2 Resource Server (JWT), BCrypt |
| ACME | acme4j 3.3.1, Bouncy Castle 1.78 (Let’s Encrypt 연동) |
| SSH 배포 | sshj 0.38.0 (SFTP 파일 전송, sudo 원격 명령) |
| AI 채팅 | LangChain4j 0.35.0 + OpenAI GPT (Function Calling) |
| 이메일 알림 | Spring Mail + Thymeleaf 템플릿 |
| API 문서 | springdoc-openapi 2.8.4 (Swagger UI) |
| 인프라 | Docker Compose (PostgreSQL, pgAdmin, ACME challenge volume) |
| CI/CD | GitHub Actions (PR 테스트 필수 실행, Docker 이미지 빌드) |
| 암호화 | AES-256-GCM (인증서 개인키 암호화 저장) |
설계 원칙 및 도구
-
멀티 모듈 모노레포: 7개 Gradle 서브모듈로 관심사 분리 (common → domain → certificate-manager / server-manager / notification-manager / ai-assistant → api)
-
이벤트 기반 모듈 통신: Spring ApplicationEvent +
@TransactionalEventListener로 모듈 간 느슨한 결합
-
Flyway 버전 관리: V1~V11까지 11개의 마이그레이션 스크립트로 스키마 진화 이력 관리
3. 아키텍처 설계 및 주요 내용
3.1 시스템 아키텍처

3.2 핵심 기능별 상세
(1) ACME 프로토콜 기반 인증서 자동 발급 및 갱신
Let’s Encrypt CA와 acme4j 라이브러리를 활용하여, DNS-01 및 HTTP-01 챌린지를 지원하는 인증서 발급 파이프라인을 구현했습니다.
AcmeAccountService: ACME 계정 생성/관리 및 키페어 직렬화AcmeOrderService: CSR 제출 → 챌린지 처리 → 인증서 수령의 전체 흐름 오케스트레이션ChallengeHandlerFactory+DnsProviderFactory: 팩토리 패턴을 적용하여 HTTP-01과 DNS-01 챌린지, 그리고 Cloudflare/수동 DNS 프로바이더를 교체 가능하게 설계- 발급된 인증서의 개인키는 AES-256-GCM으로 암호화 후 DB에 저장하여, 평문 노출 위험을 차단
(2) SSH/SFTP 기반 원격 서버 배포 자동화
인증서 발급 후 대상 서버로의 배포를 완전히 자동화했습니다.
CertificateDistributionService: SSH 연결 → 인증서/개인키/체인 파일 SFTP 업로드 → Nginx 설정 테스트(nginx -t) → Graceful Reload(nginx -s reload)- Exponential Backoff 재시도: SSH 연결 실패 시 최대 3회까지 지수 백오프로 재시도
- 배포 이력 추적: 모든 배포 시도를
Deployment테이블에 기록 (상태, 소요 시간, 에러 메시지) - 배포 실패가 인증서 발급 자체를 롤백하지 않도록 분리하여, 나중에 수동 재배포 가능
(3) 인증서 검증 파이프라인
발급된 인증서의 유효성을 6가지 관점에서 종합 검증합니다.
SignatureValidator: 서명 알고리즘 및 무결성 검증ValidityPeriodValidator: 유효기간 검증 (만료 임박 경고 포함)ChainValidator: 인증서 체인 신뢰성 검증 (Staging 인증서 구분)RevocationValidator: CRL/OCSP 기반 폐기 상태 확인DomainValidator: SAN(Subject Alternative Name) 도메인 매칭KeyUsageValidator: X.509 확장 키 용도(TLS 서버 인증) 검증
(4) JWT 인증 및 보안 체계
Stateless JWT 기반 인증/인가 시스템을 구축했습니다.
- Access Token + Refresh Token 이중 토큰 구조: Access Token의 짧은 수명(분 단위)과 Refresh Token의 안전한 로테이션
- Refresh Token Rotation & Reuse Detection: 토큰 갱신 시 기존 토큰을 즉시 폐기하고, 이미 폐기된 토큰이 재사용되면 해당 사용자의 전체 세션을 무효화 (RFC 6819 기반 토큰 탈취 대응)
- Token Version Validator: 사용자 권한 변경/로그아웃 시
tokenVersion을 증가시켜, 기존 발급 JWT를 즉시 무효화 - Method-level Authorization:
@RequireServerAccess,@RequireCertificateAccess커스텀 어노테이션으로 리소스 단위 접근 제어 - User Soft Delete:
@SQLDelete+@SQLRestriction기반 논리 삭제로 데이터 보존 및 감사 추적 가능 - 만료/폐기 Refresh Token 정리 스케줄러: 매일 새벽 3시에 만료/폐기된 토큰 일괄 삭제
(5) 이벤트 기반 이메일 알림 시스템
인증서 생명주기 이벤트에 연동된 자동 알림 체계입니다.
@TransactionalEventListener(phase = AFTER_COMMIT): 트랜잭션 커밋 후에만 이메일 발송하여, 롤백된 작업에 대한 오알림 방지@Async: 이메일 발송을 비동기로 처리하여 메인 트랜잭션 지연 최소화- Thymeleaf 템플릿: 인증서 생성/갱신/삭제/만료 임박 별 HTML 이메일 템플릿
CertificateExpiryScheduler: 매일 오전 9시(Asia/Seoul) 만료 임박 인증서 일괄 알림
(6) LLM 기반 AI 채팅 인터페이스
LangChain4j와 OpenAI GPT를 연동하여, 자연어로 인증서를 관리할 수 있는 채팅 인터페이스를 구현했습니다.
- Function Calling:
CertificateTools(인증서 조회/생성/갱신/통계)와ServerTools(서버 등록)를 LLM 도구로 등록하여, 사용자의 자연어 요청을 실제 API 호출로 자동 변환 - 세션별 독립 ChatMemory:
MessageWindowChatMemory로 세션별 대화 컨텍스트를 유지하여, 멀티턴 대화 지원 - 의도 감지(Intent Detection): 서버 등록 vs 인증서 등록 등 유사한 요청을 키워드/패턴으로 정확히 구분하는 상세한 System Prompt 설계
- 안전한 작업 제어: 삭제 같은 위험한 작업은 LLM에 도구를 제공하지 않고, 웹 UI 안내 메시지만 반환하도록 제한
4. 과정에서 겪은 기술적 문제와 해결 방법
문제 1: Refresh Token Rotation Race Condition
상황: 동일 사용자가 여러 디바이스에서 동시에 /api/v1/auth/refresh를 호출하면, read-then-write 패턴에서 두 요청 모두 같은 토큰을 유효로 판단하여 2개의 새 Refresh Token이 생성되는 경쟁 조건이 발생했습니다.
분석: 기본 트랜잭션 격리 수준(READ_COMMITTED)에서는 findByTokenHash → setRevokedAt 사이의 시간 창을 보호할 수 없었습니다. 이는 Token Replay Attack으로 이어질 수 있는 보안 취약점이었습니다.
해결: 세 가지 방안(Pessimistic Lock, Optimistic Lock + @Version, Redis 분산 락)을 비교 분석한 후, Pessimistic Locking(@Lock(PESSIMISTIC_WRITE)) 을 선택했습니다.
- Optimistic Lock은 충돌 시 재시도가 필요하지만, refresh token rotation은 “정확히 한 번” 실행되어야 하는 연산이라 부적절
- Redis 분산 락은 현재 단일 인스턴스 아키텍처에서 과잉 엔지니어링
- Refresh Token은 사용자당 수분~수시간 간격으로 갱신되므로, 행 잠금의 contention이 극히 낮아 성능 영향 무시 가능
추가로, Token Reuse Detection 로직을 구현하여 이미 폐기된 토큰이 재사용되면 해당 사용자의 모든 세션을 즉시 무효화하도록 보안을 강화했습니다.
문제 2: ACME 발급 중 장애 시 상태 불일치
상황: 인증서 발급은 DB 저장(PENDING) → ACME 서버 통신(수 초~수 분) → 결과 업데이트(ACTIVE)의 흐름인데, ACME 통신 중 서버가 비정상 종료되면 인증서가 PENDING 상태로 영구 고착되는 문제를 식별했습니다. 더 심각하게는, ACME CA에서는 인증서가 발급되었지만 DB에는 반영되지 않는 인증서 누수(Certificate Leak) 가 가능했습니다.
분석: 이것은 전형적인 분산 트랜잭션 문제로, 외부 CA 서버가 XA 프로토콜을 지원하지 않아 2PC가 불가능하고, 이미 발급된 인증서를 자동 폐기하는 보상 트랜잭션은 서비스 중단 위험이 있어 Saga 패턴도 부적절했습니다.
해결 방향: Detect-and-Recover 전략을 설계했습니다.
- DB 트랜잭션을 짧게 분리하고, 외부 API 호출을 트랜잭션 밖으로 분리
- Stale State Recovery Scheduler가 30분 이상 PENDING 상태인 인증서를 주기적으로 감지하여 ACME 서버에서 실제 상태를 확인 후 자동 복구
- 이를 통해 커넥션 풀 점유 시간도 30초 → 50ms로 개선 가능
문제 3: @Transactional 내부에서의 외부 API 호출로 인한 Connection Pool 고갈
상황: CertificateService.create()가 @Transactional 내에서 ACME 서버 통신(30~60초)을 수행하면서 DB 커넥션을 장시간 점유하여, 동시 발급 10건 초과 시 HikariCP Connection Pool이 고갈되고 다른 모든 API까지 실패하는 Cascading Failure 위험을 분석했습니다.
해결 방향: 트랜잭션 분리 패턴을 설계하여, DB 조작은 짧은 트랜잭션으로, ACME 통신은 @Async 비동기로 처리하도록 개선 방안을 수립했습니다. 이론적으로 처리량이 0.33 TPS → 200 TPS (600배)로 개선됩니다.
문제 4: Soft Delete와 Unique Constraint 충돌
상황: User 엔티티에 @SQLDelete로 soft delete를 구현했으나, soft delete된 레코드가 물리적으로 DB에 남아 있어 동일 username으로 새 사용자를 생성하면 Unique Constraint 위반 에러가 발생했습니다. @SQLRestriction은 JPA 쿼리에만 적용되고, DB 레벨 제약 조건에는 영향이 없었습니다.
해결: PostgreSQL의 Partial Unique Index를 적용하여 WHERE deleted_at IS NULL 조건으로 활성 레코드에만 유니크 제약을 적용했습니다. JPA @UniqueConstraint로 표현할 수 없으므로 Flyway 마이그레이션으로 관리합니다.
5. 본인의 역할 및 기여 내용
담당 영역
- 인증서 생명주기 관리 모듈 (
certificate-manager): ACME 프로토콜 통합, DNS-01/HTTP-01 챌린지 처리, Cloudflare DNS 연동, 인증서 검증 파이프라인(6종 Validator), 개인키 AES-256-GCM 암호화, SSH/SFTP 원격 배포 + Nginx 자동 재기동 - 보안 아키텍처 (
api/security): JWT Access/Refresh Token 이중 인증 체계, Refresh Token Rotation + Reuse Detection, Token Version 기반 즉시 무효화, User Soft Delete, 역할 기반 접근 제어 - AI 채팅 통합 (
ai-assistant): LangChain4j + OpenAI Function Calling 기반 자연어 인증서 관리 인터페이스, System Prompt 설계 및 의도 감지 로직 - 알림 시스템 (
notification-manager): 이벤트 기반 비동기 이메일 알림, 만료 임박 스케줄러 - 아키텍처 설계: 멀티 모듈 구조 설계, 모듈 의존성 규칙 수립, Flyway 마이그레이션 전략, CI/CD 파이프라인(GitHub Actions) 구축
6. 성과와 배운 점
성과
- 인증서 관리 자동화 달성: Let’s Encrypt 연동을 통한 인증서 발급 → 암호화 저장 → SSH 원격 배포 → Nginx Graceful Reload → 이메일 알림까지의 전체 파이프라인을 하나의 플랫폼에서 완성
- 보안 수준 향상: 인증서 개인키 AES-256-GCM 암호화, JWT Refresh Token Rotation + Reuse Detection, Token Version 기반 즉시 무효화 등 실무 수준의 보안 체계 구축
- AI 채팅 인터페이스 제공: 비전문가도 “example.com 인증서 등록해줘”와 같은 자연어로 인증서를 관리할 수 있는 접근성 확보
- 운영 안정성 설계: 트러블슈팅 시나리오 8건을 사전에 분석하고, 각각에 대한 해결 방안과 트레이드오프를 문서화하여 아키텍처 개선 로드맵 수립
배운 점
- 분산 시스템의 일관성 문제: 외부 CA 서버와의 통합에서 트랜잭션 경계 설정, 상태 불일치 복구 전략(Detect-and-Recover),
@Transactional과 외부 API 호출 분리의 중요성을 실감 - 보안과 사용성의 균형: Refresh Token의 Race Condition, Soft Delete와 Unique Constraint 충돌, 메모리 내 Private Key 잔존 등 실무에서 발생할 수 있는 보안 이슈를 직접 분석하며, 트레이드오프 기반 의사결정 역량 강화
- 멀티 모듈 설계의 가치: 7개 서브모듈로 관심사를 명확히 분리함으로써, 3명이 병렬로 개발하면서도 모듈 간 충돌을 최소화할 수 있었고, 이벤트 기반 통신으로 알림 모듈을 독립적으로 추가/제거할 수 있었음
- LLM의 실용적 활용: Function Calling과 System Prompt 엔지니어링을 통해 LLM을 단순 Q&A가 아닌 실제 시스템 조작 인터페이스로 활용하는 경험을 획득. 특히 의도 감지(Intent Detection)의 정확도를 높이기 위한 프롬프트 설계의 중요성을 체감
7. 기술적 의사결정 요약
| 결정 사항 | 선택한 방안 | 대안 및 탈락 사유 |
|---|---|---|
| 인증서 개인키 보관 | AES-256-GCM 앱 레벨 암호화 | HashiCorp Vault (인프라 복잡도 과다), Jasypt (암호화 수준 부족) |
| Token 동시성 제어 | Pessimistic Lock | Optimistic Lock (재시도 부적절), Redis 분산 락 (단일 인스턴스에서 과잉) |
| 상태 불일치 복구 | Detect-and-Recover (Scheduler 폴링) | 2PC (외부 CA 미지원), Saga (보상 트랜잭션 부적절), Outbox (복잡도 과다) |
| 모듈 간 통신 | Spring ApplicationEvent | Kafka/RabbitMQ (현재 규모에서 과잉, 스케일아웃 시 전환 예정) |
| Soft Delete + Unique | PostgreSQL Partial Unique Index | Compound Unique (다중 삭제 시 충돌), username 변경 (감사 추적 곤란) |
| AI 통합 | LangChain4j Function Calling | REST API 직접 호출 (비전문가 사용 불가), Rule-based 챗봇 (유연성 부족) |