스프링 프레임워크가 자바 엔터프라이즈 시장의 표준으로 자리 잡은 이유는 단순히 기능이 많아서가 아니다. 스프링의 본질은 '자바 언어가 가진 객체 지향의 특징을 극대화할 수 있도록 돕는 것'에 있다. 그 시작점인 자바 진영의 역사와 객체 지향 설계 원칙에 대해 정리한다.
1. 자바 진영의 추운 겨울과 스프링의 탄생
스프링이 등장하기 전, 자바 엔터프라이즈 개발의 중심에는 EJB(Enterprise JavaBeans)가 있었다. EJB는 당시 기술적으로 권위 있는 표준이었으나, 다음과 같은 치명적인 단점이 존재했다.
- 높은 복잡도와 의존성: EJB 표준을 따르기 위해 비즈니스 로직보다 프레임워크에 종속적인 코드를 더 많이 작성해야 했다.
- 어려운 테스트: 프레임워크 없이는 단독으로 테스트하기가 매우 까다로웠다.
- 비싼 비용과 느린 속도: 고가의 WAS가 필요했으며 개발 속도 또한 저하되었다.
이러한 상황을 두고 개발자들은 "차라리 과거의 순수한 자바 객체로 돌아가자"는 의미의 POJO(Plain Old Java Object)라는 용어를 만들어낼 정도로 절망적이었다. 이때 로드 존슨(Rod Johnson)이 EJB의 문제점을 비판하며 출간한 책에서 소개한 30,000줄의 예제 코드가 바로 스프링 프레임워크의 모태가 되었다. 스프링이라는 이름은 'EJB라는 추운 겨울'이 지나고 '봄'이 왔다는 의미를 담고 있다.
2. 좋은 객체 지향 프로그래밍이란?
스프링은 객체 지향 언어인 자바를 기반으로 한다. 따라서 스프링을 이해하려면 '좋은 객체 지향'이 무엇인지 정의해야 한다. 객체 지향의 핵심은 다형성(Polymorphism)에 있으며, 이를 실현하기 위해서는 역할(Interface)과 구현(Class)을 분리해야 한다.
2.1. 역할과 구현의 분리
- 역할: 인터페이스(Interface)
- 구현: 인터페이스를 구현한 클래스, 구현 객체(Concrete Class)
세상을 '역할'과 '구현'으로 구분하면 단순해진다. 예를 들어 운전자(클라이언트)는 자동차(역할)가 K3이든 아반떼이든 테슬라이든 상관없이 자동차를 운전할 수 있다. 자동차의 내부 구현이 바뀌어도 운전자에게 영향을 주지 않는 것이다.
이처럼 설계 시 역할을 먼저 만들고 그 다음에 구현 객체를 만들면, 클라이언트는 내부 구조를 몰라도 되며 구현 대상 자체를 변경해도 아무런 지장을 받지 않게 된다. 즉, 유연하고 변경이 용이한 설계가 가능해진다.
3. 좋은 객체 지향 설계의 5가지 원칙 (SOLID) ★
객체 지향 설계를 잘하기 위해서는 몇 가지 기본적인 원칙을 이해하고 지키는 것이 중요하다. 로버트 마틴(Robert C. Martin)은 이러한 원칙들을 정리하여 SOLID 원칙이라고 이름 붙였다. SOLID는 좋은 객체 지향 설계를 위한 대표적인 지침으로, 특히 스프링 프레임워크를 이해하고 활용하는 데에도 매우 밀접한 관련이 있다.
SRP(단일 책임 원칙) 은 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 즉, 클래스가 여러 가지 일을 동시에 처리하도록 만들기보다는, 변경의 이유가 하나만 생기도록 역할을 명확히 나누는 것이 중요하다. 이렇게 설계하면 코드가 이해하기 쉬워지고, 수정이나 확장도 훨씬 수월해진다.
OCP(개방-폐쇄 원칙) 은 소프트웨어 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙이다. 새로운 기능이 필요할 때 기존 코드를 직접 수정하기보다는, 다형성을 활용해 새로운 구현 클래스를 추가하는 방식이 바람직하다. 예를 들어 인터페이스를 구현한 새로운 클래스를 만드는 것은 기존 코드를 변경하지 않으면서 기능을 확장하는 대표적인 방법이다.
LSP(리스코프 치환 원칙) 은 상위 타입의 객체를 하위 타입의 객체로 바꾸어도 프로그램이 정상적으로 동작해야 한다는 원칙이다. 즉, 자식 클래스는 언제든지 부모 클래스 자리에 들어가도 문제가 없어야 한다. 이 원칙이 지켜지지 않으면 다형성을 신뢰할 수 없게 되고, 코드의 안정성도 크게 떨어진다.
ISP(인터페이스 분리 원칙) 은 범용 인터페이스 하나를 만드는 것보다, 특정 클라이언트에 맞는 인터페이스 여러 개로 나누는 것이 더 낫다는 원칙이다. 사용하지도 않는 메서드에 의존하게 되는 상황을 피하고, 각 클라이언트가 꼭 필요한 기능만 바라보도록 설계하는 것이 목적이다. 이를 통해 인터페이스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다.
DIP(의존관계 역전 원칙) 은 구체적인 구현 클래스가 아니라, 추상화에 의존해야 한다는 원칙이다. 다시 말해, 인터페이스나 추상 클래스에 의존하고 실제 구현체는 외부에서 주입받는 구조가 바람직하다. 스프링의 DI(의존성 주입) 개념 역시 이 원칙을 기반으로 하고 있으며, 이를 통해 유연하고 테스트하기 쉬운 구조를 만들 수 있다.
4. 객체 지향 설계와 스프링의 관계
순수 자바 코드만으로는 다형성을 활용하더라도 OCP와 DIP 원칙을 완벽히 지키기 어렵다. 인터페이스와 구현체를 분리하더라도, 결국 클라이언트 코드나 별도의 설정 클래스에서 구현 클래스를 직접 생성(new)하여 주입하는 코드를 작성해야 하기 때문이다. 이 과정을 순수 자바에만 의존할 경우 다음과 같은 문제에 직면한다.
- 수동 관리의 한계: 관리해야 할 객체가 100개라면 100번의 new 키워드를 통해 객체를 생성하고 의존 관계를 일일이 연결해야 한다.
- 코드 비대화: 의존 관계가 깊어지고 복잡해질수록 이를 조립하는 코드(AppConfig 등) 자체가 비대해지며, 이는 또 다른 유지보수의 대상이 된다.
스프링은 바로 이 지점을 해결해준다. 스프링은 다음 기술들을 통해 SOLID 원칙을 이론에 그치지 않고 실무에 효율적으로 적용할 수 있는 기반을 제공한다.
- DI(Dependency Injection): 의존관계 주입을 통해 클라이언트 코드를 수정하지 않고도 외부에서 기능을 확장할 수 있게 한다.
- DI 컨테이너: 개발자가 직접 new를 호출하여 객체를 생성하고 연결하는 번거로운 과정을 프레임워크가 대신 수행한다. 설정 정보만 명시하면 컨테이너가 객체를 자동으로 생성하고 필요한 곳에 꽂아준다.
결국 스프링은 개발자가 객체 지향 설계 원칙을 충실히 따르면서도, 객체 관리와 조립이라는 반복적인 업무에서 벗어나 순수 비즈니스 로직에만 집중할 수 있게 도와주는 프레임워크인 것이다.
'Spring > Core' 카테고리의 다른 글
| [Basic-3] 관심사의 분리와 의존관계 주입(DI) (0) | 2025.12.30 |
|---|---|
| [Basic-2] 순수 자바 예제로 이해하는 DIP와 OCP 위반 (0) | 2025.12.30 |
| [Practice-3] Spring Core: 깔끔하고 확장성 있는 예외 처리 (1) | 2025.12.16 |
| [Practice-2] 객체 생성: 빌더 패턴과 정적 팩터리 메서드 (0) | 2025.09.02 |
| [Practice-1] ResponseEntity<> (0) | 2025.08.27 |
