솔직히 고백하자면, 나는 2-Tier 아키텍처를 3년이나 방치했다. '돌아가니까 괜찮지'라는 안일한 생각이었다. 그러다 어느 날 동시접속 200명을 넘기자 서버가 뻗었고, 대표이사한테 직접 전화가 왔다. '시스템 왜 이래?' 그날 밤, 3-Tier 전환 기획서를 썼다.

왜 3-Tier 아키텍처인가?

기존 2-Tier 아키텍처에서는 클라이언트가 데이터베이스에 직접 연결하는 구조였습니다. 이 방식의 문제점은 명확합니다:

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. 1단계: API Gateway 도입 - 기존 시스템과 신규 API 공존
  2. 2단계: 핵심 모듈부터 마이그레이션
  3. 3단계: 데이터 동기화 구간 운영
  4. 4단계: 전체 전환 및 레거시 제거
실무 경험 공유: C# WinForm + MSSQL 직접 연결 방식의 2-Tier 시스템을 Spring Boot 기반 3-Tier로 전환하는 프로젝트에 참여한 적이 있습니다. 기존 방식에 익숙한 팀원들을 설득하는 게 기술 구현보다 어려웠습니다. 결국 기존 화면 하나를 함께 Spring Boot + REST API로 전환해보면서 장점을 직접 느끼게 하는 방법이 가장 효과적이었습니다.

아키텍처 전환, 기술보다 중요한 것

아키텍처 마이그레이션은 단순한 기술 전환이 아닙니다. 기존 비즈니스 로직의 이해, 데이터 정합성 보장, 그리고 팀의 학습 곡선까지 고려해야 합니다. 특히 팀원들의 동의와 참여 없이는 아무리 좋은 기술을 도입해도 성공하기 어렵습니다.

돌이켜보면 마이그레이션 프로젝트에서 코드를 작성하는 시간보다 팀원들을 설득하고 교육하는 시간이 더 많았습니다. 하지만 그 과정이 있었기에 전환 후 팀 전체의 기술 수준이 한 단계 올라갈 수 있었습니다. 비슷한 프로젝트를 진행하시는 분들께 "기술 선택만큼 변화 관리에도 시간을 투자하라"고 조언드리고 싶습니다.

Jaeseong
Jaeseong

10년차 풀스택 개발자. Spring Boot, Flutter, AI 등 실무 경험을 기록합니다.

GitHub →

💬 댓글