2026-03-15
Java 8에서 25까지 — 단계적 마이그레이션으로 보일러플레이트 900줄 제거한 이야기
출발점: Java 8 + Spring Boot 2.x
에이블테라퓨틱스에 합류했을 때 기존 시스템은 Java 8 + Spring Boot 2.x 기반이었다. Java 8은 2019년에 공식 지원이 종료됐고, Spring Boot 2.x도 EOL에 가까웠다. 보안 패치가 더 이상 나오지 않는 상태에서 헬스케어 플랫폼을 운영하는 건 리스크였다.
하지만 더 큰 문제는 코드 품질이었다. Java 8 시절의 관행대로 작성된 VO(Value Object) 클래스들이 있었는데:
// Before: 전형적인 Java 8 스타일 VO
public class UserResponse {
private String id;
private String name;
private String email;
private String phone;
private String role;
private LocalDateTime createdAt;
public UserResponse() {}
public UserResponse(String id, String name, String email,
String phone, String role, LocalDateTime createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.role = role;
this.createdAt = createdAt;
}
// getter 6개, setter 6개, equals, hashCode, toString...
// 한 클래스에 60~80줄
}이런 클래스가 수십 개. 거기에 다중 상속 구조로 VO끼리 extends하고 있어서 필드가 어디서 오는지 추적하기도 어려웠다.
마이그레이션 전략: 한 번에 하지 않기
"Java 25로 한 번에 올리자"는 위험한 접근이다. 호환성 이슈가 어디서 터질지 모르기 때문이다. 단계적으로 진행했다:
1단계: Java 8 → 17 (LTS)
가장 큰 변화가 여기서 일어났다:
javax → jakarta 네임스페이스 변경 준비 (아직 Spring Boot 2.x라서 실제 전환은 아님)
텍스트 블록, switch 표현식 같은 문법 적용
record 클래스 도입 시작 — 새로 만드는 DTO부터 record로
// After: Java 17+ record
public record UserResponse(
String id,
String name,
String email,
String phone,
String role,
LocalDateTime createdAt
) {}
// 끝. 6줄. getter, equals, hashCode, toString 자동 생성.2단계: Spring Boot 2.x → 3.5 (Java 17 → 21)
이게 가장 큰 고비였다. Spring Boot 3.x부터 Jakarta EE 전환이 필수다:
// Before
import javax.persistence.*;
import javax.validation.constraints.*;
import javax.servlet.http.*;
// After
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import jakarta.servlet.http.*;단순 패키지명 변경이지만 영향 범위가 넓다. IDE의 전체 치환 기능으로 처리하되, 서드파티 라이브러리가 jakarta를 지원하는지 하나하나 확인해야 했다.
보안 설정도 완전히 바뀌었다:
// Before: WebSecurityConfigurerAdapter 상속 (deprecated)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
// After: SecurityFilterChain 빈 등록
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.build();
}
}3단계: Java 21 → 25
여기서는 주로 최신 문법 적용:
패턴 매칭 강화 — instanceof 후 바로 캐스팅
sealed classes — 도메인 모델 타입 안전성
virtual threads — 검토 중 (아직 적용 전)
VO → record 전환: 900줄 제거
가장 임팩트가 컸던 작업이다. 기존 VO 클래스들을 전수 조사했다:
Request DTO: 15개 → 전부 record로 전환
Response DTO: 22개 → 전부 record로 전환
다중 상속 구조: 공통 필드를 composition으로 변경
Before: 평균 60줄/클래스 × 37개 = ~2,220줄
After: 평균 8줄/클래스 × 37개 = ~296줄
제거된 보일러플레이트: ~1,900줄 (그 중 명확히 측정 가능한 것만 900줄 이상)
record는 불변(immutable)이니까 setter로 인한 사이드 이펙트도 원천 차단된다.
겪은 이슈들
Jackson 직렬화 문제
record와 Jackson(JSON 라이브러리) 조합에서 기본 생성자가 없어서 역직렬화가 안 되는 경우가 있었다. Spring Boot 3.x에서는 jackson-module-parameter-names가 기본 포함되어 해결되지만, 커스텀 직렬화가 필요한 경우 @JsonCreator를 명시해야 했다.
서드파티 라이브러리 호환성
Java 17+에서 리플렉션 제한이 강화되면서 일부 라이브러리가 InaccessibleObjectException을 던졌다. --add-opens JVM 옵션으로 임시 대응 후, 라이브러리 업데이트로 근본 해결했다.
결과
| 항목 | Before | After |
|---|---|---|
| Java 버전 | 8 (EOL) | 25 (최신) |
| Spring Boot | 2.x (EOL 근접) | 3.5.x |
| DTO 보일러플레이트 | ~2,220줄 | ~296줄 |
| 보안 설정 | deprecated API | 현행 API |
마이그레이션은 한 번에 하면 안 된다. 단계별로 올리면서 각 단계에서 테스트를 통과시키고, 동료에게 리뷰를 받아야 한다. 특히 Spring Boot 2→3 전환은 Jakarta EE 네임스페이스 변경 때문에 영향 범위가 크니까, 충분한 테스트 기간을 잡아야 한다.
관련 글
벡터 유사도 기반비동기와 병렬 처리
모던 백엔드 애플리케이션에서 지연 시간을 줄이고 리소스 활용을 극대화하기 위한 비동기 및 병렬 처리 기법을 다룹니다. Spring @Async, CompletableFuture, Java Concurrency, WebFlux를 살펴봅니다.
68% 일치Flix 프로젝트: 아키텍처 소개 & 개발 기록 - 1
넷플릭스를 모방한 Flix 스트리밍 플랫폼의 전체 아키텍처 소개와 초기 구현 내용을 다룬 개발 기록입니다.
67% 일치스프링 내부 구조 정리
스프링의 핵심 내부 구조를 실제 코드와 테스트를 통해 정리합니다. IoC 컨테이너, Bean 생명주기, AOP, 의존성 주입 방식을 다룹니다.