- dependency-injectionejbiocjavajava-reflectionspring-framework
Java 엔터프라이즈 개발의 문제와 Spring Framework의 등장
초기의 자바 애플리케이션에서는 객체를 코드 내부에서 직접 생성하는 방식이 일반적이었다.
이 방식은 로그 저장 방식 변경, 데이터베이스 교체, 결제 모듈 교체, 암호화 알고리즘 변경과 같은 요구사항이 발생할 때마다 소스 코드를 수정하고 다시 빌드해야 하는 문제를 만들었다.
또한 당시 엔터프라이즈 애플리케이션 개발에서 사용되던 EJB 역시 높은 복잡성과 무거운 실행 환경이라는 문제를 가지고 있었다.
이러한 문제를 해결하기 위한 여러 시도가 있었고, 그 과정에서 설정 파일과 리플렉션 기반 객체 생성 방식이 사용되기도 했다. 그러나 애플리케이션이 커질수록 이러한 방식 역시 관리가 어려워졌다.
이러한 배경 속에서 IoC(Inversion of Control)와 DI(Dependency Injection) 개념을 중심으로 한 Spring Framework가 등장하게 되었다.
Spring Framework 이전의 코드 수정 문제
자바의 철학 중 하나는 Write Once, Run Anywhere이다.
즉 JVM(Java Virtual Machine)을 통해 CPU나 운영체제에 관계없이 동일한 바이트코드를 실행할 수 있다는 의미이다.
하지만 애플리케이션 내부에서 객체를 직접 생성하는 구조에서는 환경이나 요구사항이 변경될 때 코드 수정이 필요해지는 문제가 있었다.
로그 저장 방식 변경
예를 들어 애플리케이션이 처음에는 로그를 파일로 저장하도록 구현되었다고 가정해 보자.
Logger logger = new FileLogger();이후 로그를 데이터베이스에 저장하도록 변경하려면 다음과 같이 코드를 수정해야 한다.
Logger logger = new DbLogger();이 경우 애플리케이션을 다시 빌드하고 배포해야 한다.
문제의 핵심은 JVM이 아니라 구체 클래스에 직접 의존하는 구조에 있다.
개발 환경과 운영 환경의 DB 차이
또 다른 사례는 데이터베이스 환경의 차이다.
-
개발 환경: H2 같은 메모리 DB
-
운영 환경: Oracle 또는 MySQL
하지만 코드에 다음과 같은 객체 생성 코드가 있다면
new OracleDriver()환경이 바뀔 때마다 코드를 수정하고 다시 빌드해야 한다.
결제 모듈(PG사) 교체
결제 시스템을 운영하다 보면 PG사를 변경해야 하는 경우가 있다.
예를 들어 코드 곳곳에서 다음과 같은 객체를 직접 생성하고 있다면
new APaymentService()B사로 변경할 때 애플리케이션 전체 코드를 수정해야 한다.
이 과정에서 코드 수정 범위가 넓어지고 시스템 안정성에도 문제가 생길 수 있다.
암호화 알고리즘 변경
보안 정책이 강화되면서 기존 MD5 방식 대신 SHA-256 같은 더 강력한 알고리즘을 사용해야 하는 경우도 있다.
하지만 암호화 객체를 코드 곳곳에서 직접 생성하고 있다면 시스템 전체를 수정해야 할 수도 있다.
문제 해결을 위한 초기 시도
이러한 문제를 해결하기 위해 외부 설정 파일을 이용해 객체 생성 방식을 분리하는 방법이 사용되기 시작했다.
설정 파일 기반 객체 생성
예를 들어 다음과 같은 설정 파일이 있다고 가정하자.
service.payment=com.pg.BPayment애플리케이션은 이 설정 값을 읽어 해당 클래스를 생성하여 사용한다.
이 방식의 장점은 애플리케이션 코드를 수정하지 않고도 기능을 교체할 수 있다는 점이다.
하지만 설정 파일에 정의된 클래스 이름을 기반으로 객체를 생성하려면 자바 리플렉션이 필요하다.
자바 리플렉션(Reflection)
자바 리플렉션은 런타임에 클래스 정보를 조회하고 객체를 동적으로 생성할 수 있는 기능이다.
예를 들어 설정 파일에서 읽은 클래스 이름을 이용해 다음과 같이 객체를 생성할 수 있다.
Class clazz = Class.forName("com.example.PaymentService");
Object obj = clazz.getDeclaredConstructor().newInstance();이 방식은 객체 생성의 유연성을 높여 주지만 애플리케이션 규모가 커질수록 다음과 같은 문제가 발생했다.
-
설정 오류를 컴파일 시점에 검증할 수 없다.
-
싱글톤 객체 관리가 어렵다.
-
객체 간 의존성이 많아질수록 초기화 코드가 매우 복잡해진다.
즉 객체 생성과 의존성 관리 자체가 또 하나의 복잡한 문제가 되었다.
EJB(Enterprise Java Bean)의 한계
당시 엔터프라이즈 자바의 표준 기술이었던 EJB 역시 개발자에게 여러 부담을 주었다.
프레임워크 종속적인 객체 모델
EJB 컴포넌트를 작성하려면 SessionBean, EJBObject 등 인터페이스를 구현해야 했다. 이로 인해 비즈니스 로직이 프레임워크에 강하게 의존하게 되었고 코드의 재사용성과 테스트가 어려워졌다.
무거운 실행 환경
EJB 애플리케이션을 실행하려면 EJB 컨테이너가 포함된 WAS가 필요했다.
따라서 단순한 기능 테스트를 위해서도 전체 애플리케이션 서버를 실행해야 하는 경우가 많았고 개발 생산성이 떨어졌다.
Spring Framework의 등장
이러한 문제를 해결하기 위해 등장한 것이 Spring Framework이다.
Spring은 다음과 같은 핵심 개념을 중심으로 설계되었다.
-
IoC(Inversion of Control)
-
DI(Dependency Injection)
-
POJO 기반 개발 모델
Spring의 IoC 컨테이너는 객체 생성과 의존성 관리를 담당하며, 개발자는 객체를 직접 생성하지 않고 필요한 의존성을 주입받아 사용할 수 있다. 또한, 기술적으로는 자바 리플렉션, Annotation, 동적 Proxy 기술을 적극적으로 활용하여 설정이나 애노테이션에 정의된 클래스 정보를 런타임에 분석하고 객체를 동적으로 생성한다.
이 방식은 다음과 같은 장점을 제공한다.
-
객체 간 결합도 감소
-
설정 기반 모듈 교체 가능
-
테스트 환경 단순화
-
프레임워크 비종속적인 코드 작성
Spring은 객체 생성과 의존성 관리라는 인프라 문제를 프레임워크 내부로 숨기고, 개발자가 POJO(Plain Old Java Object) 중심으로 애플리케이션을 개발할 수 있도록 한다.
Spring 프로젝트의 목표였던 "엔터프라이즈 개발을 단순하게 만든다”는 철학은 이후 자바 생태계 전반에 큰 영향을 미쳤고, Spring은 사실상의 표준 프레임워크로 자리 잡게 되었다.
(끝)