1. 개요


Spring이 제공하는 핵심 가치와 원리를 이해해야 합니다.
왜 Spring을 만들었고, Spring이 왜 이런 기능들을 제공하는지를 살펴봅니다.

과거 오픈 소스는 ‘사파’라 불렸고, 표준이 ‘정파’ 기술로 불렸습니다. EJB는 ‘정파’였기 때문에 여러 기업들에서 EJB를 많이 도입했습니다. EJB가 이론적으로는 정말 좋았지만, 현실적으로 너무 어렵고, 느렸습니다. 그리고 비쌌습니다.

EJB 엔티티빈 → 하이버네이트 → JPA

지금 시점에 Java로 개발할 때의 메인이 되는 두 축은 Spring과 JPA입니다.

 

Spring 프레임워크의 역사 (로드 존슨이 최초 만들었던 3만줄의 Spring 코드로부터 시작하였습니다.)

  1. 2003년, Spring 프레임워크 1.0 출시 - XML로 설정
  2. 2006년, Spring 프레임워크 2.0 출시 - XML 편의 기능 지원
  3. 2009년, Spring 프레임워크 3.0 출시 - Java 코드로 설정
  4. 2013년, Spring 프레임워크 4.0 출시 - Java8
  5. 2014년, Spring Boot 1.0 출시
  6. 2017년, Spring 프레임워크 5.0과 Spring Boot 2.0 출시 - 리액티브 프로그래밍 지원
  7. 2020년 9월 현재, Spring 프레임워크 5.2.x, Spring Boot 2.3.x

 

2014년에 출시된 Spring Boot는 큰 전환점 중 하나였습니다.
Spring 프레임워크로 주로 웹 애플리케이션을 개발하는데, 이때 두 가지가 크게 어려웠습니다.

  1. 설정
  2. 웹 서버(Tomcat)에다가 Spring 코드 빌드해서 나온 war 파일을 집어넣고 배포하는 작업

[그림 1] Spring Framework 생태계

Spring에는 무수히 많은 서브 프로젝트가 있는데, 당연하게도 가장 중요한 것은 Spring Framework입니다.
Spring Framework 안에 포함된 기술은 다음과 같이 분류할 수 있습니다.

  • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타
  • 웹 기술: Spring MVC, Spring WebFlux
  • 데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원
  • 기술 통합: 캐시, 이메일, 원격접근, 스케줄링
  • 테스트: 스프링 기반 테스트 지원
  • 언어: Kotlin, Grovy
  • 최근에는 Spring Boot를 통해서 Spring Framework의 기술들을 편리하게 사용할 수 있다.

 

‘Spring’ 이라는 단어가 가지는 의미?

  • 스프링 DI 컨테이너 기술
  • Spring Framework
  • Spring Boot, Spring Framework를 포함한 전체 Spring 생태계

 

아무리 복잡한 기술도 처음 컨셉은 단순합니다. 이 단순한 컨셉이 좋으면 기술이 점점 발전해 나가는 것입니다.
Spring이 지금은 엄청나게 크고 복잡한 프로젝트지만, 최초에는 로드 존슨이 만든 3만줄의 코드에서 시작했습니다. 그럼 Spring의 핵심 개념은 무엇일까요?

  • 이 기술은 왜 만들었을까요?
  • 이 기술의 핵심 컨셉은 무엇일까요?

 

Spring의 핵심은 다음과 같습니다.
Spring은 Java 언어 기반의 프레임워크로, Java 언어의 가장 큰 특징은 객체 지향 언어라는 것입니다.
Spring은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크입니다.
Spring은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크입니다.

2. 좋은 객체 지향 설계의 5가지 원칙(SOLID)


클린 코드로 유명한 로버트 마틴이 좋은 객체지향 설계의 5가지 원칙을 정리했습니다.

  • SRP(Single Responsibility Principle) : 단일 책임 원칙
  • OCP(Open/Close Principle) : 개방-폐쇄 원칙
  • LSP(Liskov Substitution Priciple) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존관계 역전 원칙

 

1. SRP(Single Responsibility Principle, 단일 책임 원칙)

하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다. 하지만, 실무를 하다보면 이 하나의 책임이라는 것이 상당히 모호하게 다가올 수 있습니다. 하나의 책임이 엄청나게 큰 책임일 수도 있고, 아주 조그마한 책임일 수도 있는 것이죠. 이때, 중요한 기준은 ‘변경’입니다. 변경이 있을 때 파급 효과가 적으면 SRP를 잘 따른 것입니다. 예를 들어, UI 변경, 객체의 생성과 사용을 분리하는 것입니다.

 

2. OCP(Open/Close Principle, 개방-폐쇄 원칙) - 5가지 중 가장 중요한 원칙

소프트웨어 요소(컴포넌트)는 확장에는 열려있으나, 변경에는 닫혀있어야 한다는 원칙입니다.
다형성만을 활용하면 이 원칙을 지킬 수 있을까요? (인터페이스는 그대로 놔두고 기능을 추가한 구현체만 새로 생성)
다형성만을 사용하게되면 한 가지 문제가 생깁니다.
객체의 생성을 담당하는 클라이언트 코드가 구현체를 직접 선택해줘야 하기 때문에, 변경된 구현체로 새로 코드를 변경해줘야 합니다. 즉, 다형성만을 활용한다고 해서 제대로된 OCP를 지킬 수는 없습니다.
이를 해결하기 위해 객체를 생성하고 연관관계를 맺어주는 별도의 조립자(DI/IoC 컨테이너)가 필요합니다.
바로 이 역할을 Spring이 담당합니다.

 

3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙) - 쉬운 원칙

객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙입니다.
‘자동차’ 라는 인터페이스가 있고 ‘엑셀’, ‘브레이크’ 라는 기능이 있다고 가정해보겠습니다. 이때, 당연히 ‘엑셀’이라는 기능은 차를 앞으로 가게끔 하는 기능이어야하는데, 어떤 구현체가 ‘엑셀’ 기능에 차를 뒤로가게끔 로직을 짰다면 어떻게 될까요? 컴파일 시점에서는 당연히 문제가 없겠지만, 프로그램의 정확성은 깨지게 됩니다. 즉, 인터페이스 규약에 맞게 구현체가 기능을 만들어야한다는 원칙입니다.

 

4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)

범용적인 인터페이스 하나보다 구체적인 인터페이스 여러 개로 분리하라는 원칙입니다.
’자동차’ 인터페이스 → ‘운전’ 인터페이스, ‘정비’ 인터페이스와 같이 분리합니다.
위와 같이 구체적인 기능 하나하나로 인터페이스를 쪼개면 클라이언트 코드도 분리할 수 있습니다.
’정비’ 인터페이스 자체가 변해도, ‘운전자’ 클라이언트에 전혀 영향이 없습니다.
따라서, 인터페이스가 명확해지고, 대체 가능성이 높아집니다.

 

5. DIP(Dependency Inversion Principle, 의존관계 역전 원칙) - 중요한 원칙(OCP와 연관있음)

”구현체가 아니라 추상화에 의존해야 한다” 의존성 주입은 이 원칙을 따르는 방법 중 하나입니다.
의존성을 내가 직접 적어주는게 아니라, 누군가로부터 주입받는다면 나는 추상화에만 의존할 수 있습니다.
즉, 객체를 생성하는 부분을 분리해야 합니다.

 

정리

객체 지향의 핵심은 다형성입니다. 하지만, 다형성만을 가지고는 쉽게 부품을 갈아 끼우듯이 개발할 수 없습니다.
구현 객체를 변경하기 위해 클라이언트 코드도 함께 변경해줘야하기 때문입니다. 즉, 다형성만으로는 OCP, DIP를 지킬 수 없습니다. 구현체를 주입해주는 DI/IoC 컨테이너가 필요합니다.

2. Spring Framework


객체지향 설계의 5가지 원칙에서 다형성만으로는 OCP, DIP를 지킬 수 없다는 것을 알았습니다.
Spring은 DI 컨테이너를 제공하여 OCP, DIP를 가능하게 지원합니다.
이로 인해, 클라이언트 코드의 변경없이 기능을 확장할 수 있습니다. (쉽게 부품을 교체하듯이 개발)

프로젝트 생성시 적어주는 ‘Artifact’가 빌드시 이름이 됩니다.

 

IoC(Inversion of Control, 제어의 역전)

우선 MemberServiceImpl은 MemberRepository를 사용하는 클라이언트 입장이고, MemberRepository는 서버 입장입니다. 다음과 같이 클라이언트가 직접 구현체를 생성하는 코드는 클라이언트가 서버 코드에 대한 제어권을 직접 가지고 있는 것과 같습니다.

public class MemberServiceImpl {
    private MemberRepository memberRepository = new MemoryMemberRepository();

    public void save(Member member) {
        memberRepository.save(member);
    }
}

위 코드는 객체지향 설계 원칙인 OCP, DIP를 위반합니다. 기능을 확장하기 위해 새로운 구현체를 만들어 갈아끼우기 위해선 클라이언트 코드를 변경해야하기 때문입니다. 그리고 SRP 원칙도 위반합니다. 한 클래스는 하나의 책임만을 가져야하는데 위 코드는 ‘생성’, ‘기능의 사용’ 두 가지 책임을 가지기 때문입니다. 따라서, 아래와 같이 ‘생성’을 담당하는 부분을 분리해야 합니다.

public class AppConfig {
    public static MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
public class MemberServiceImpl {
    private MemberRepository memberRepository; // 구현체를 직접 생성하지 않습니다.

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    // ... 생략 
}

MemberServiceImpl을 사용하는 클라이언트가 AppConfig를 이용해서 MemberServiceImpl을 가져옵니다.

public class Main {
    public static void main(String[] args) {
        MemberService memberService = AppConfig.memberService();
        memberService.save( ... );
    }
}

MemberServiceImpl 입장에서는 MemberRepository에 대한 제어권을 자신이 가지고 있는게 아니라, AppConfig가 가지고 있습니다. 즉, 제어권이 역전되었는데 이를 IoC(제어의 역전)이라고 합니다.

 

프레임워크 vs 라이브러리

내가 작성한 코드를 제어하고 대신 실행한다면 프레임워크입니다. (Spring MVC에서 컨트롤러의 메서드를 개발자가 직접 호출하지 않고, 멤버변수에 구현체를 직접 할당하지 않습니다. 따라서 프레임워크입니다.)
반면, 내가 작성한 코드가 서버 코드의 제어권을 가지고 있고, 실행을 직접한다면 라이브러리입니다.

위의 AppConfig를 IoC 컨테이너 또는 DI 컨테이너라 합니다.

 

Spring 컨테이너 생성

// 애너테이션 기반의 Java 설정 클래스를 이용해서 Spring 컨테이너를 만듭니다.
// XML 기반으로 만들려면 XML~ApplicationContext 와 같은 이름의 구현체를 사용합니다.
ApplicationContext ctx = 
                    new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext 를 Spring 컨테이너라 합니다.
  • ApplicationContext 는 인터페이스입니다.
  • Spring 컨테이너는 XML 기반으로 만들 수 있고, 애너테이션 기반의 Java 설정 클래스로 만들 수 있습니다.

 

사실 더 정확하게는 Spring 컨테이너를 얘기할 때 ‘BeanFactory’, ‘ApplicationContext’ 둘을 구분해서 이야기합니다. 하지만, ‘BeanFactory’를 직접 사용하는 경우는 거의 없으므로 일반적으로 ‘ApplicationContext’를 Spring 컨테이너라고 합니다.

[그림 2] Spring 최상위 인터페이스인 BeanFactory와 상속 관계도

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스입니다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당합니다.
  • ‘getBean()’ 과 같은 메서드가 다 BeanFactory에 들어있는 기능입니다.

ApplicationContext

  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요합니다.
  • BeanFatory뿐만 아니라 여러가지 인터페이스를 상속받습니다.
  • EnvironmentCapable, MessageSource, ResourceLoader 등을 추가로 상속받습니다.
  • MessageSource: 국제화 기능을 제공합니다. 영어권이면 영어 출력, 한국이면 한국어 출력
  • EnvironmentCapble: 로컬, 개발, 운영 환경을 구분하는 기능을 제공합니다.
  • ApplicationEventPublisher: 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 기능입니다.
  • ResourceLoader: 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회하는 기능을 제공합니다.

BeanFactory만을 직접 사용할 일은 거의 없습니다. 부가기능이 더해진 ApplicationContext를 사용합니다.

3. Spring Framework 설정 형식 (Java 코드, XML, Groovy, 기타)


요즘에는 XML 기반으로 Spring 컨테이너를 만드는 경우는 거의 없습니다. Spring Boot 자체가 애너테이션 기반의 Java 설정 클래스로 편리하게 Spring 컨테이너를 생성하도록 많은 지원을 해주고 있습니다. 하지만, Spring은 유연하게 설계되어 있기 때문에 XML, Java 코드뿐만 아니라 Groovy, 심지어 개인이 스스로 만들어 사용할 수 있도록 다양한 설정 형식을 지원합니다.

[그림 3] 다양한 설정 형식을 지원

 

위와 같이 ApplicationContext 인터페이스를 구현하면 개발자 스스로 설정 형식을 새로 만들수도 있습니다.

 

XML 설정

최근에는 Spring Boot를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않습니다. 그러나 아직 많은 레거시 프로젝트들이 XML로 되어있고, 또 XML을 사용하면 컴파일없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한 번쯤 배워두는 것도 괜찮습니다.

그런데 Spring은 어떻게 이런 다양한 설정 형식을 지원할 수 있는 걸까요?
Spring이 다양한 설정 형식을 지원하는데의 중심에는 ‘BeanDefinition’ 이라는 추상화가 있습니다.
쉽게 이야기해서 ‘역할과 구현을 개념적으로 나눈 것’입니다.

  • XML을 읽어서 BeanDefinition을 만들면 된다.
  • Java 코드를 읽어서 BeanDefinition을 만들면 된다.
  • Spring 컨테이너는 설정 형식이 Java 코드인지 XML인지 몰라도 됩니다. 오직 BeanDefinition만 알면 된다.

 

‘BeanDefinition’ 을 빈 설정 메타정보라고 합니다.
’@Bean’, ‘’ 당 각각 하나씩 메타 정보가 생성됩니다.
Spring 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성합니다.

4. Singleton 컨테이너


웹 애플리케이션과 싱글톤

  • Spring은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 대부분의 Spring 애플리케이션은 웹 애플리케이션이다. (물론 웹이 아닌 애플리케이션도 개발 가능)
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청한다.

 

웹의 특성상 여러 고객이 동시에 요청을 하는데, 만약 그때마다 MemberService와 같은 객체를 new로 생성한다면 JVM 메모리 낭비뿐만 아니라 객체를 매번 생성하고 GC가 객체를 다시 소멸시키는 과정이 무수히 일어날텐데 이때의 오버헤드가 엄청 심할 것입니다. 이를 해결하기 위해 객체를 1개만 생성하고 요청마다 공유하는 방식(싱글톤)으로 사용합니다.

싱글톤 패턴: 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.

싱글톤 패턴은 웹 애플리케이션에서 필수적이지만, 동시에 무수히 많은 문제점을 야기합니다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. → DIP를 위반합니다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

 

그런데 Spring 컨테이너는 싱글톤이 가지는 무수히 많은 문제점들을 극복하고 사용할 수 있게 해줍니다.
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리합니다.
싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 합니다.
즉, 개발자가 MemberService 코드를 만들면 어떠한 싱글톤 관련 코드가 없지만, 싱글톤으로 사용합니다.

참고: Spring의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아닙니다. 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공합니다. (하지만 거의 99%는 싱글톤으로 사용합니다.)

참고: Spring의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아닙니다. 
요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공합니다. (하지만 거의 99%는 싱글톤으로 사용합니다.)

 

싱글톤 방식의 주의점⭐️

싱글톤 패턴이든 Spring과 같은 싱글톤 컨테이너를 사용하든 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안됩니다.
무상태(stateless)로 설계해야 합니다!

  • 특정 클라이언트에 의존적인 필드가 있으면 안됩니다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됩니다!
  • 가급적 읽기만 가능해야 합니다.
  • 필드 대신에 Java에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 합니다.

 

스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있습니다!!

 

@Configuration과 싱글톤

@Configuration은 사실 싱글톤을 위해 존재하는 것입니다.