솔직히 고백하자면, 나는 2-Tier 아키텍처를 3년이나 방치했다. '돌아가니까 괜찮지'라는 안일한 생각이었다. 그러다 어느 날 동시접속 200명을 넘기자 서버가 뻗었고, 대표이사한테 직접 전화가 왔다. '시스템 왜 이래?' 그날 밤, 3-Tier 전환 기획서를 썼다.
왜 3-Tier 아키텍처인가?
기존 2-Tier 아키텍처에서는 클라이언트가 데이터베이스에 직접 연결하는 구조였습니다. 이 방식의 문제점은 명확합니다:
- 보안 취약점: DB 연결 정보가 클라이언트에 노출
- 확장성 한계: 동시 접속자 증가 시 DB 커넥션 풀 고갈
- 비즈니스 로직 분산: 클라이언트와 DB에 로직이 혼재
- 유지보수 어려움: 변경 시 클라이언트 재배포 필요
3-Tier 아키텍처로 전환하면 Presentation Layer, Business Logic Layer, Data Access Layer가 명확히 분리되어 이런 문제들을 해결할 수 있습니다.
기술 스택 선정
마이그레이션 프로젝트에서 선정한 기술 스택입니다:
Backend:
- Spring Boot 3.x
- Java 17
- MyBatis (기존 SQL 자산 활용)
- MSSQL Server
Build & Deploy:
- Gradle Kotlin DSL
- WAR 패키징 (Standalone Tomcat)
Security:
- Spring Security
- JWT 인증
MyBatis를 선택한 이유는 기존 시스템의 복잡한 SQL을 그대로 활용하면서 점진적으로 마이그레이션하기 위함입니다. JPA는 추후 신규 모듈부터 도입하는 전략을 취했습니다.
핵심 설계 원칙
1. 비즈니스 로직은 반드시 Service Layer에
Controller에서 직접 비즈니스 로직을 처리하는 코드를 종종 볼 수 있는데, 이는 안티패턴입니다.
// ❌ 잘못된 예시
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody OrderDto dto) {
// Controller에서 비즈니스 로직 처리
if (dto.getQuantity() > inventory.getStock()) {
throw new RuntimeException("재고 부족");
}
// ... 복잡한 로직
}
}
// ✅ 올바른 예시
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@RequestBody @Valid OrderDto dto) {
return ResponseEntity.ok(orderService.createOrder(dto));
}
}
2. API 응답 번들링
화면 하나를 그리기 위해 여러 번의 API 호출이 필요한 경우, 서버 사이드에서 데이터를 번들링하여 한 번에 응답하는 것이 효율적입니다.
@GetMapping("/dashboard")
public ResponseEntity<DashboardResponse> getDashboard() {
return ResponseEntity.ok(
DashboardResponse.builder()
.summary(summaryService.getSummary())
.recentOrders(orderService.getRecentOrders())
.notifications(notificationService.getUnread())
.build()
);
}
3. 트랜잭션 무결성 보장
여러 테이블을 수정하는 작업은 반드시 @Transactional로 묶어야 합니다. 특히 배치 작업에서 이 부분을 놓치면 데이터 불일치 문제가 발생합니다.
실제 마이그레이션 과정에서 발견한 이슈들
JWT 보안 취약점
코드 리뷰 중 발견한 심각한 보안 이슈입니다:
// ❌ 하드코딩된 JWT Secret
private static final String SECRET = "mySecretKey123";
// ✅ 환경변수에서 주입
@Value("${jwt.secret}")
private String jwtSecret;
PasswordEncoder 인자 순서 오류
Spring Security의 passwordEncoder.matches() 메서드 인자 순서가 뒤바뀐 코드를 발견했습니다:
// ❌ 잘못된 순서 (항상 false 반환)
passwordEncoder.matches(encodedPassword, rawPassword);
// ✅ 올바른 순서
passwordEncoder.matches(rawPassword, encodedPassword);
이런 실수는 테스트 코드가 없으면 발견하기 어렵습니다. 인증 관련 로직은 반드시 단위 테스트를 작성하세요.
마이그레이션 단계별 전략
Big Bang 방식보다는 점진적 마이그레이션을 추천합니다:
- 1단계: API Gateway 도입 - 기존 시스템과 신규 API 공존
- 2단계: 핵심 모듈부터 마이그레이션
- 3단계: 데이터 동기화 구간 운영
- 4단계: 전체 전환 및 레거시 제거
아키텍처 전환, 기술보다 중요한 것
아키텍처 마이그레이션은 단순한 기술 전환이 아닙니다. 기존 비즈니스 로직의 이해, 데이터 정합성 보장, 그리고 팀의 학습 곡선까지 고려해야 합니다. 특히 팀원들의 동의와 참여 없이는 아무리 좋은 기술을 도입해도 성공하기 어렵습니다.
돌이켜보면 마이그레이션 프로젝트에서 코드를 작성하는 시간보다 팀원들을 설득하고 교육하는 시간이 더 많았습니다. 하지만 그 과정이 있었기에 전환 후 팀 전체의 기술 수준이 한 단계 올라갈 수 있었습니다. 비슷한 프로젝트를 진행하시는 분들께 "기술 선택만큼 변화 관리에도 시간을 투자하라"고 조언드리고 싶습니다.
10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.
GitHub →
💬 댓글