Search
Duplicate
🌱

[요약] 스프링 핵심 원리 - 기본편

스프링 탄생 배경

기존에 있던 EJB(Enterprise Java Beans)라는 기술이 배우기 힘들고, 사용하기도 복잡하다보니 개발자들이 Plan Old Java 라고 다시 옜날 자바로 돌아가자는 주장도 했다고 한다.
개빈 킹(Gavin King)과 로드존슨(Rod Johnson)이라는 개발자가 나타나 오픈소스 프로젝트를 시작했는데 이게 JPA와 Spring이다.
좀 더 정확하게는 유겐 휠러(Juergen Hoeller), 얀 카로프(Yann Caroff)가 로드존슨에게 오픈소스 프로젝트를 제안하는데 이게 스프링 프레임워크이다.

스프링이란?

문맥별로 다르게 해석될 수 있다.
스프링에서 다루는 기술을 주 관점으로 말하면 DI컨테이너 기술이라 볼 수 있고, 스프링 프레임워크라고 볼 수도 있다. 혹은 스프링 부트, 스프링 프레임워크를 포함한 스프링 생태계 전체를 의미하기도 한다.

스프링 생태계

스프링 데이터, 스프링 세션, 스프링 시큐리티, 스프링 배치 등 스프링 생태계 내에서 수많은 프로젝트들을 통해 기능이 제공된다.
이러한 여러 프로젝트들을 모아서 쉽게 사용할 수 있도록 해주는게 스프링 부트이다.

스프링 프레임워크

이런 스프링 생태계에서 여러 기술을 합쳐 만든 프레임워크이다.
스프링 프레임워크 내에서는 웹 기술, 데이터 접근, 기술 통합, 테스트같은 기능들이 모두 지원된다.(코틀린이나 그루비 역시 지원된다.)
Spring DI Container, AOP, Event같은 핵심 기술부터 Spring MVC, DB 접근 기술은 트랜잭션, JDBC, ORM, XML도 지원한다.
결국 여러 스프링 생태계에서 생성 배포된 여러 기술들을 한데 뭉쳐 사용할 수 있게 해주는 프레임워크를 스프링 프레임워크라 할 수 있다.

스프링 부트

기존 스프링 프레임워크는 많은 기술들을 지원하지만, 이 모든 기술들을 사용하기 위해 설정을 하거나 프로젝트를 배포하는 과정에 대한 학습난이도가 높고 번거로웠다.
이런 번거로운 과정들을 은닉해서 개발자가 쉽게 사용할 수 있도록 지원한다.
내장 톰캣을 사용해 별도의 웹서버를 설치하고 설정하지 않아도 된다.
스프링 생태계에서 제공하는여러 기술들을 starter로 종속성을 제공한다.
Ex: org.springframework.boot:spring-boot-starter-data-jpa
스프링 서드파티 외부라이브러리 자동 구성
관례에 의한 간결한 설정을 할 수 있다.
대부분의 공통 설정들은 기본 값으로 미리 설정되어 있다.
명시적으로 변경이 필요한 경우 Application.properties(or yaml, yml)같은 설정 파일을 통해 손 쉽게 변경이 가능하다.

결국 스프링은 자바 기반 프레임워크이다.

물론 코틀린으로도 할 수 있지만, 기본적으로는 자바 진영의 대표 프레임워크이다.
객체지향 패러다임을 따르는 프레임워크라 볼 수 있다.
객체지향 패러다임이 추구하는 방향을 지킬 수 있도록 도와주는 프레임워크로 객체지향을 추구하는 애플리케이션 개발을 할 수 있도록 도와준다.

객체지향을 추구한다는건 무슨 의미인가?

추상화, 캡슐화, 상속, 다형성이라는 특징을 잘 살리는 프로그램
명령어의 목록(절차 지향)이 아닌 독립적인 객체들 간의 메세지를 주고받음으로써 데이터를 처리하는 협력관계 프로그램
프로그램을 유연하고 변경에 용이하게 만드는 방법 → 다형성

다형성은...

역할과 구현을 구분하여 각각의 역할이 맡은 책임만 알면 되도록 하는 방식
즉, 책임주도설계의 핵심은 다형성이다.
자바에서는 이러한 역할을 인터페이스가 담당하고, 구현을 인터페이스를 구현한 클래스가 담당한다.

SOLID - 좋은 객체지향 설계의 5가지 원칙

SRP(Single Responsibility principle) 단일 책임 원칙

: 한 클래스는 하나의 책임만 가져야 한다는 원칙으로 핵심은 변경이 있을때 파급효과가 적을수록 단일 책임 원칙을 잘 따른 것이라 할 수 있다.

OCP(Open-Closed Principle) 개방-폐쇄 원칙

: 확장에는 열려있으나 변경에는 닫혀 있어야 한다는 원칙으로 얼핏 생각하면 확장을 하려면 당연히 코드를 변경해야 하기에 말이 안되는 것 같지만, 다형성을 이용하여 인터페이스를 구현하는 새로운 구현 클래스를 만들어 기능의 확장을 도모하면 개방-폐쇄 원칙을 지킨다고 할 수 있다.

LSP(Liskov substitution Principle) 리스코프 치환 원칙

: 프로그램의 객체가 프로그램의 정확성을 해치지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙으로, 다형성을 제대로 사용했다면 하위 인스턴스는 인터페이스의 규약을 모두 지켰기 때문에 문제가 없이 되야 한다. 대표적인 예로는 정사각형 - 직사각형 문제를 예로 들 수 있다.

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

역할들이 합쳐진 범용 인터페이스를 설계하기보다는 특정 목적만을 책임지는 인터페이스가 여러개 있는것이 좋다는 의미다.
예시 코드
public interface Car { void driving(DirectionType direction); void fix(FixType type); }
Java
복사
public interface Car { void go(); void left(); void stop(); void right(); void fixHandle(); void fixEngine(); }
Java
복사
public interface Car implements DrivingHandler, FixSupplier { } public interface DrivingHandler { void go(); void left(); void stop(); void right(); } public interface FixSupplier { void fixHandle(); void fixEngine(); }
Java
복사

DIP(Dependency Inversion Principle): 의존관계 역전 원칙

: 구체화가 아닌 추상화에 의존해야한다는 원칙으로 스프링의 의존성 주입(Dependency Injection)이 이 원칙을 따르는 대표적인 방법이라 생각하면 된다. 이 원칙은 구현보다 역할에 집중하라는 것으로 역할에 집중함으로써 내부 구현이 어떻게 바뀌든 인터페이스(역할)만 의존하게 되면 기능의 변경및 확장이 유연해질 수 있다.

스프링을 쓰지 않고 MVC Pattern을 구현할 시 문제점

스프링을 사용하지 않고 MVC 구조로 코드를 작성하다보면 SOLID 원칙을 모두 지키기가 힘들다는걸 알 수 있다.
MemberService Code 살펴보기
@Service public class MemberService { private MemberRepository memberRepository = new MemoryMemberRepository(); //... logic }
Java
복사
해당 코드를 보면 memberRepository에 할당되는 객체 인스턴스를 생성하기위해 특정 구현체에 의존하는 코드를 작성한 것을 알 수 있다. 여기서 일단 DIP 원칙인 구체화가 아닌 추상화에 의존해야 한다는 원칙을 못 지킨것을 알 수 있다.
또한, 리포지토리 구현체를 InMemory에서 JDBC를 사용하는 리포지토리를 사용하게 하려면, new JdbcMemberRepository()로 변경을 해야하는데 이렇게되면 OCP원칙인 변경에 닫혀있지도 못하게 된다.

즉 다형성만 가지고는 OCP, DIP를 지킬 수 없다.

→ 그래서 스프링을 이용하면 스프링의 다음 기술로 다형성 + OCP, DIP를 지킬 수 있게 된다.
⇒ DI(Dependency Injection): 의존관계, 의존성 주입
⇒ DI Container 제공

관심사의 분리

각각의 계층(Service, Controller, Repository)는 다른 역할의 구현체를 사용하고자 할 때 그 구현체의 생성자를 호출하여 객체 인스턴스를 만들어 해당 인스턴스에 의존을 하게 되는데, 이 과정에서 구현체에 대한 의존성이 생기게 되면서 여러 객체지향 원칙들을 어기게 된다.
각각의 계층의 구현체들은 자기 자신의 책임만 해결하도록 해야하는데, 다른 역할의 구현체까지 알게되는건 과도한 책임이라 할 수 있다.
그래서 관심사를 분리하여 각각의 역할이 자신의 책임에만 관심을 가질 수 있도록 해줘야 한다.

Config 역할

그럼 아예 이런 구현체를 생성하여 연결(주입)해주는 책임만을 담당하는 별도의 역할을 만들어보는건 어떨까? 해당 객체에서는 OCP, DIP 원칙을 위배하게 하는 각각의 구현체에서 다른 역할의 구현체를 의존하게 하는 부분들을 없앨 수 있도록 해줄 수 있다.

AppConfig

public class AppConfig { public MemberService memberService() { return new MemberServiceImpl(new MemoryMemberRepository()); } public OrderService orderService() { return new OrderServiceImpl(new MemoryMemberRepository() ,new FixDiscountPolicy()); } }
Java
복사

클래스 다이어그램

위와같은 구조로 바뀌게 되면서 MemberService의 구현체인 MemberServiceImpl에서는 MemberRepository의 구현체를 알 필요 없이 AppConfig에서 주입해주는 구현체만 받아 사용하면 된다. 그리고 이런 모습을 외부(AppConfig)에서 주입(Injection)받는 것 같다고 하여 의존관계 주입 혹은 의존성 주입이라 부른다.

IOC, DI Container

IoC(Inversion of Control) 제어 역전

: 기존에 구현체에서 필요한 구현체를 직접 생성해 연결 및 사용하던 방식에서
외부에서 주입해주는 역할을 가져와 사용하게 되면서 자기 자신의 책임에만 관심을 가져도 되도록 변경되었는데, 이렇게 제어의 흐름을 자기 자신이 아니라 외부에서 관리되도록 하는 것을 제어의역전(IoC)라 한다.

DI(Dependency Injection) 의존관계 주입

: 런타임 시점에 외부에서 실제 구현체를 생성 및 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
그리고 정적인 클래스 의존관계와 동적인 객체 인스턴스 의존관계에 대해 이해할 필요가 있다.

정적인 클래스 의존관계

: 클래스상단에 작성되있는 import 코드만 보고 판단이 가능한 의존관계. 이는 애플리케이션을 실행하지 않아도 분석이 가능하다. 허나 역할의 실제 구현체가 무엇이 사용될지는 알 수 없다.

동적인 객체 인스턴스 의존 관계

: 애플리케이션 실행 시점에서 실제로 생성된 인스턴스의 참조값이 연결된 실제 의존관계.

DI Container

: AppConfig와 같이 객체를 생성,관리,연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라 한다.

스프링 컨테이너

스프링 컨테이너란?

일반적으로 ApplicationContext 를 스프링 컨테이너라 한다.
스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
스프링 빈은 @Bean 이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다.
@Bean public MemberRepository memberRepository(){ return new MemoryMemberRepository(); }
Java
복사
→ memberRepository가 스프링 빈의 이름이 된다.
개발자가 별도로 AppConfig 같은 DI Container를 만들지 않아도 된다.
ApplicationContext는 인터페이스이고, 다형성을 적극 사용한다.
ApplicationContext의 상위 구조에는 BeanFactory라는 컨테이너가 존재한다. 하지만, 일반적으로 BeanFactory를 직접 사용하는 일은 드믈기에 ApplicationContext를 스프링 컨테이너라 한다.

스프링 빈 등록

AppConfig의 빈 정보를 읽어와 등록해준다.

스프링 빈 의존관계 설정

스프링 빈 의존관계 설정
→ 스프링 컨테이너는 설정정보를 참고해 의존관계를 주입(Dependency Injection)해준다.

특징

동일한 타입이 둘 이상인 스프링 빈을 getBean 메서드를 통해 조회하려 하면 NoUniqueBeanDefinitionException이 발생한다.
메서드 이름을 통해 좀 더 명시적으로 호출을 하거나, getBeans 메서드를 통해 Key/Value 쌍인 Map으로 스프링 빈 컬렉션을 받을 수 있다.
상속관계인 스프링 빈을 조회한다면, 내가 조회하고자 하는 스프링 빈과 해당 빈을 상속하는 하위 빈들까지 전부 조회한다.
이 경우에도 getBean으로 조회할 때 하위 스프링 빈이 둘 이상 있다면 중복 오류가 발생한다.(NoUniqueBeanDefinitionException)
동일한 타입의 스프링 빈이 여러개일때와 동일하게 명시적인 이름이나 getBeansOfType 메서드를 통해 컬렉션으로 반환받을 수 있다.
ApplicationContext라는 스프링 컨테이너는 BeanFactory라는 최상위 스프링 컨테이너 역할에 추가적인 기능을 덧붙혀 제공하는 인터페이스라 볼 수 있다. 그래서 해당 인터페이스를 보면 BeanFactory뿐 아니라 MessageSource, EnvironmentCapable 등 여러 인터페이스를 다중 상속하여 다양한 부가기능을 함께 제공한다.

스프링 빈 설정 메타 정보 - BeanDefinition

스프링 컨테이너 생성시 필요한 설정들은 한 가지의 형식이 아닌 Java, XML, Groovy등 다양한 형식으로 모두 제공이 가능하다. 이게 가능한 이유가 BeanDefinition 인터페이스 덕분이다.
→ 각각의 설정정보에 맞는 BeanDefinitionReader를 사용해 해당 설정정보를 읽어서 BeanDefinition 메타정보를 생성해 전달한다.
→ 이 말은 여기에 없는 다른 형식(JSON, TXT, ...)도 해당 형식을 읽을 수 있는 BeanDefinitionReader만 만들어서 제공하면 설정 정보를 제공할 수 있다.

싱글톤 컨테이너

AppConfig의 문제점

public class AppConfig { public MemberService memberService() { return new MemberServiceImpl(new MemoryMemberRepository()); } public OrderService orderService() { return new OrderServiceImpl(new MemoryMemberRepository() ,new FixDiscountPolicy()); } }
Java
복사
주입(DI)를 책임지는 AppConfig클래스는 메서드를 호출해서 자연스럽게 역할의 구현체를 주입받을 수 있다는 장점이 있지만, 메서드 호출시마다 새롭게 인스턴스를 만드는 문제가 있다. AppConfig의 memberService() 메서드를 통해 MemberController라는 컨트롤러에서 주입받는다고 할 때 만약 기능이 추가되어 OrderController라는 컨트롤러에서도 MemberService 역할의 주입이 필요해 memberService()를 호출해버리면 어떻게 될까?
MemberService는 두 개의 인스턴스가 생성되어 각각 주입될 것이다.
그럼 만약 이게 수십 수백개의 여러 클래스에서 호출을하게 된다면 모두 각각의 인스턴스를 생성해서 주입해줄 것이고 이는 몹시 비효율적이다.
그래서 우리는 싱글톤 패턴을 사용해서 객체를 재활용 할 수 있도록 하여야 한다.

MemberService가 싱글톤 패턴을 사용할 수 있는 이유

: 그럼 모든 도메인이나 클래스가 다 싱글톤 패턴을 사용할 수 있고, 사용해도 될까?
그건 아니다. 싱글톤은 말 그대로 하나의 인스턴스를 모든 클라이언트에서 바라보게하고 사용할 수 있도록 하는 것이기에 지켜야 하는 게 있다. 바로 무상태성(stateless)이다.
싱글톤 패턴을 사용할 객체는 무상태성(stateless)를 보장해야 한다.
public class MemberService { private final String serviceName; private String memberCount; //... logic }
Java
복사
이렇게 MemberService가 서비스명, 멤버수 라는 상태를 가지고 있는 상태에서 싱글톤 패턴을 적용하면 스레드 안전하지 못하기에 문제가 발생할 수 있다.

싱글톤 패턴의 문제점

싱글톤 패턴은 장점만 있는게 아니라 여러 문제점 역시 가지고 있다.
싱글톤 패턴을 적용하기 위한 추가 코드를 작성하는데 드는 비용
클라이언트가 구현체에 의존하면서 DIP를 위반하게 된다.
자연스럽게 OCP도 위반할 수 있다.
테스트가 어려워진다.
내부 속성을 변경하거나 초기화가 어렵다.
private 생성자로 자식 클래스를 만들기 어렵다.
유연성이 떨어진다.
안티패턴으로 불리기도 한다.

스프링 컨테이너로 싱글톤 방식의 문제를 해결한다.

스프링 컨테이너는 싱글톤 컨테이너의 역할을 해준다.
싱글톤 패턴이 가지는 문제점을 모두 해결해준다.
@Configuration 애노테이션을 사용하는 설정 클래스에서 등록되는 스프링 빈들은 모두 스프링 컨테이너에 의해 싱글톤으로 관리가 된다.
클래스 내부에 동일한 객체를 생성하는 코드가 중복 호출되더라도 하나의 인스턴스로 관리되어 싱글톤이 유지된다.

CGLIB를 이용한 싱글톤 컨테이너 관리

CGLIB는 무엇인가?

코드 생성 라이브러리(Code Generator Library)로 런타임시에 동적으로 자바 클래스의 프록시를 만들어주는 기능을 제공한다. 대표적으로 Hibernate에서는 자바빈 객체에 대한 프록시를 생성할 때 사용돠고, Spring에서는 스프링 컨테이너 혹은 프록시 기반 AOP를 구현할 때 주로 사용된다.
CGLIB는 기존 소스코드에 추가기능을 덧붙히는 기존 객체의 하위 객체인 프록시 객체를 만드는데 사용이 되는데, 이를 이용해 스프링은 싱글톤을 유지할 수 있도록 한다.
더하여 AppConfig의 하위 타입이기에 AppConfig 타입으로 조회 역시 가능하다.
내부적으로는 해당 스프링 빈이 등록되어있는지 조회해서 이미 등록되어있으면 등록 된 스프링 빈을 반환하고 그게 아니라면 새로 생성해 반환하는 식으로 기능이 들어가있을 것이다.

주의: @Configuration이 없는 클래스에서 @Bean 등록을 하면?

: @Configuration 애노테이션이 없는 설정 클래스에서도 스프링 빈을 등록할 수는 있다.
하지만, 이는 CGLIB으로 하위 객체가 만들어서 스프링 컨테이너로 관리되지 않기에 싱글톤이 보장되지 않는다. 그렇기에 동일한 인스턴스가 여러번 생성될 수 있다.
그렇기에 스프링 설정 정보는 항상 @Configuration이 있는 클래스에서 하도록 하자.

컴포넌트 스캔 - @ComponentScan

개요

@ComponentScan 애노테이션을 이용하면 해당 애노테이션을 참고해 @Component 애노테이션이붙은 클래스를 스캔하여 스프링 빈으로 등록해 스프링 컨테이너에서 관리한다.
스프링 빈으로 등록시 스프링 빈의 이름은 기본적으로는 클래스명의 맨 앞글자만 소문자로 바꿔 빈 이름으로 등록된다.
MemoryMemberRepository → memoryMemberRepository;
@Component 애노테이션의 속성에 별도의 이름을 지정해서 등록할 수도 있다.
@Component("cumtomService") public class MemberService {... }
Java
복사
customService라는 이름의 스프링 빈으로 등록된다.
탐색이 되는 범위는 사용자가 따로 지정하지 않으면 기본 범위에따라, 지정해주면 해당 범위를 스캔한다.
(기본 관례) 속성을 따로 지정하지 않으면 현재 @ComponentScan 애노테이션 지정된 클래스가 있는 패키지가 시작 위치가 된다.
명시적인 탐색 위치 지정은 basePackages속성을 이용해 할 수 있다.
@ComponentScan(basePackages = "hello.core")
Java
복사
hello.core 부터 컴포넌트 스캔을 하겠다는 의미가 된다.

@Autowired

생성자, 필드, 접근자 메서드 등에 해당 애노테이션(@Autowired)를 지정하면 스프링 컨테이너에서는 자동으로 해당 스프링 빈을 찾아 주입(DI)해준다.
이는 ac.getBean(...)으로 빈을 조회하는것과 유사하다.

@SpringBootApplication

스프링 프로젝트를 만들면 자동으로 생성되는 Main Application 클래스를 보면 해당 애노테이션이 부여되어 있는데, 이 애노테이션은 내부적으로 ComponentScan이 메타 애노테이션으로 부여되어 있으며, 보통 해당 애노테이션이 지정된 클래스는 프로젝트 시작 위치에 있기에 프로젝트 내부의 컴포넌트들이 모두 스캔 대상이 된다.

의존관계는 생성자 주입을 선택하자.

의존관계를 주입하는 방법으로는 생성자, Setter, Field 주입 방법등이 있다.
이 중에서 무엇을 써야할지 고민할 수 있는데 스프링 뿐 아니라 대부분의 DI 프레임워크는 생성자 주입 방식을 권장한다. 어째서일까?

어째서?

1. 불변

최초 스프링 등록 시 1회 주입 후 의존관계가 변경될 일이 없기에 불변성을 확보할 수 있다.
객체 생성시점에서 1회는 무조건 생성자가 호출되야하는데 이 상황에서 의존관계 주입까지 이뤄지면 불변 설계가 가능해진다.

2. 누락

필드 주입 혹은 Setter 주입의 경우 객체 생성시점이나 혹은 해당 스프링 빈이 없는 경우 null인 상태로 존재한다. 그렇기에 추후 로직 수행시 NPE의 위험이 다분하다.
생성자 주입 방식을 선택한다면 final 키워드를 사용하여 무조건 의존관계 주입이 되도록 강제할 수 있다.

동일한 타입의 스프링 빈이 2개 이상인 경우

@Autowired 는 빈을 타입으로 조회한다.
동일한 타입으로 조회된 빈이 2개 이상이면 문제가 발생할 수 있다.
/**FixDiscountPolicy.class**/ @Component public class FixDiscountPolicy implements DiscountPolicy{ ... } /**RateDiscountPolicy.class**/ @Component public class RateDiscountPolicy implements DiscountPolicy{ ... } public class AutoAppConfigTest { @Autowired private DiscountPolicy discountPolicy; ... }
Java
복사
동일한 타입의 빈이 2개 이상인 경우
위 코드에서 DiscountPolicy에는 어떤 빈이 주입될 지 알 수 없기에 다음과 같은 예외가 발생한다.
NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
Java
복사
하지만, 두 정책 모두 사용되는 정책이기에 하나를 없앨 수는 없다고 한다면 어떻게 해야할까?

해결책

@Autowired Field Name

: @Autowired는 타입 매칭을 통해 의존성 주입을 하는데, 빈이 여러개가 검색된다면 필드명으로 빈 이름을 추가적으로 매칭한다.
public class AutoAppConfigTest { //@Autowired //private DiscountPolicy discountPolicy; @Autowired private DiscountPolicy rateDiscountPolicy; ... }
Java
복사

@Qualifier

해당 애노테이션을 활용하여 추가 구분자를 붙혀줄 수 있다.
이 경우 스프링 빈의 이름이 바뀌는 것은아니고 추가적인 매칭 조건을 제공함으로써 해결하는 방식이다.
@Component @Qualifier("myDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy{ ... } @Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService{ private final MemberRepository memberRepository; @Qualifier("myDiscountPolicy") private final DiscountPolicy discountPolicy; ... }
Java
복사

@Primary

애노테이션의 의미 그대로 우선순위를 부여한다.
스프링 빈이 두 개 이상 있는 경우 해당 @Primary 애노테이션이 있는 스프링 빈이 우선권을가지게되어 주입된다. 그리고 이 애노테이션의 경우 @Qualifier와 비교해서 양 쪽에 모두 애노테이션을 붙혀주지 않아도 된다는 장점이 있다.

참고: @Primary, @Qualifier간의 우선순위

그럼 두 애노테이션이 모두 사용되고 있다면 어떻게 주입이 될까?
일반적으로는 조건이 디테일 할 수록 우선순위가 높다. 그렇기에 직접 이름을 지정해 매칭하는 @Qualifier가 우선순위를 가진다.

빈 생명주기 콜백

스프링 빈 라이프 사이클

객체의 생성과 초기화는 분리하자.

스프링 빈 생명주기 콜백 관리 방법

인터페이스(InitializingBean, DisposableBean)

package hello.core.lifecycle; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; public class NetworkClient implements InitializingBean, DisposableBean { private String url; public NetworkClient() { System.out.println("생성자 호출 , url=" + url); connect(); call("초기화 연결 메세지"); } public void call(String msg) { System.out.println("call= " + url + " message= " + msg); } public void connect() { System.out.println("connect= " + url); } public void disconnect() { System.out.println("close= " + url); } public void setUrl(String url) { this.url = url; } @Override public void afterPropertiesSet() throws Exception { connect(); call("초기화 연결 메세지"); } @Override public void destroy() throws Exception { disconnect(); } }
Java
복사
인터페이스를 구현해 초기화(afterPropertiesSet),소멸(destroy) 메서드를 재정의해준다
→ 이러한 방식은 편하지만, 스프링 전용 인터페이스이기에 해당 인터페이스에 종속되게 된다.
→ 외부 라이브러리에는 적용할 수 없다.
→ 이제는 거의 사용하지 않는다.

설정 정보에 초기화 메서드, 종료 메서드 지정(애노테이션 속성 설정)

@Bean 애노테이션에 initMethod, destroyMethod 속성을 통해 초기화, 종료 메서드를 지정하는 방법
public class NetworkClient{ ... public void init() { System.out.println("NetworkClient.init"); connect(); call("초기화 연결 메세지"); } public void close() { System.out.println("NetworkClient.close"); disconnect(); } } @Configuration static class LifeCycleConfig { @Bean(initMethod = "init", destroyMethod = "close") public NetworkClient networkClient() { NetworkClient networkClient = new NetworkClient(); networkClient.setUrl("http://hello-spring.dev"); return networkClient; } }
Java
복사
→ NetworkClient 클래스내의 init 메서드를 초기화, close 메서드를 소멸 메서드로 지정해줬다.

종료 메서드 추론

관례에 따라 close, shutdown 이라는 이름으로 종료 메서드를 지정해두면 destroyMethod 속성을 따로 지정해주지 않아도 자동으로 찾아서 동작시킨다.
그리고, 만약 추론 기능을 사용하기 싫은 경우 공백(””)을 지정하면 된다.

@PostConstruct, @PreDestroy 애노테이션 사용

초기화 메서드에 @PostConstruct, 종료 메서드에 @PreDestroy 애노테이션을 붙혀서 초기화, 종료 메서드를 지정한다.
해당 애노테이션은 스프링이 아닌 JSR-250 자바 표준으로 제공되는 애노테이션이기에 스프링에 종속적이지도 않다.
⇒ javax.annotation.PostConstruct 으로 제공된다.
외부 라이브러리에서는 사용할 수 없기에 이 경우에는 @Bean 애노테이션에 속성 부여로 사용하면 된다.

빈 스코프

개요

: 스프링 빈이 존재할 수 있는 범위. 또는 스프링 컨테이너가 관리를 해 주는 범위라고도 할 수 있다. 기본적으로는 싱글톤 스코프로써 스프링 컨테이너와 라이프 사이클을 같이하지만, 상황에따라 빈 스코프를 변경하여 스프링 빈의 라이프사이클을 임의로 변경할 수 있다.

스코프 범위

웹 관련 스코프

사용법

@Scope 애노테이션을 사용하며 속성으로 스코프를 지정한다.
(default) singleton: 아무 것도 작성하지 않으면 기본적으로 singleton 스코프이다.
prototype: 프로토타입 스코프
@Component, @Bean 모두 스코프를 지정할 수 있다.

싱글톤과 프로토타입 스코프의 차이점

싱글톤은 하나의 스프링 빈을 모든 클라이언트가 같이 사용하지만, 프로토타입은 각각의 클라이언트가 각각의 다른 스프링 빈을 사용한다.
프로토타입 스프링 빈은 소멸 메서드(ex: destroy)가 호출되지 않기에 클라이언트가 직접 호출해줘야 한다.

프로토타입과 싱글톤 스코프가 혼용될 경우 문제점

: 싱글톤 빈에서 프로토타입을 의존하는 경우 첫 호출시에는 새로운 프로토타입 스프링 빈을 생성하여 주입받지만, 싱글톤은 모두가 함께 사용하기에 다른 클라이언트에서 요청하더라도 싱글톤빈은 이미 가지고 있는 프로토타입 스프링 빈을 그대로 사용한다.
즉, 프로토타입 스프링 빈이 싱글톤 스프링 빈처럼 사용된다

참고: DL(Dependency Lookup) 의존관계 조회

: 의존관계를 외부에서 주입(DI)받는게 아닌 필요에 따라 직접 의존관계를 찾는것을 Dependency Lookup이라 한다.

DL 서비스 제공 객체

1. ObjectFactory

: 스프링 컨테이너에서 스프링 빈을 대신 찾아주는 DL 서비스를 제공하는 객체.
스프링에서 제공하기에 스프링에 의존한다.

2. ObjectProvider

: ObjectFactory 에 추가 기능을 확장한 객체로 이 역시 스프링에 의존한다.
static class ClientBean{ @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); prototypeBean.addCount(); int count = prototypeBean.getCount(); return count; } }
Java
복사
ObjectProvider 적용 코드

3. Provider

: JSR-330에서 제공하는 기능으로 스프링이 아닌 자바 표준이기에 스프링에 의존적이지 않다. 하지만, javax.inject:javax.inject:1 라이브러리를 따로 추가해서 사용해야 한다.
dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'javax.inject:javax.inject:1' ... }
Java
복사

프로토타입 빈의 사용시점

실무에서는 거의 대부분 싱글톤 빈으로 문제가 해결되기에 사용되는일은 극히 드물다.
javax.inject패키지의 DL의 사용시점에 대한 문서가 있다.
여러 인스턴스를 검색해야 하는 경우
인스턴스를 지연 혹은 선택적으로 찾아야 하는 경우
순환 종속성을 깨기 위해서
스코프에 포함된 인스턴스로부터 더 작은 범위의 인스턴스를 찾아 추상화 하기 위해서 사용한다.

웹 스코프

개요

웹 환경에서만 동작하는 스코프
스프링에서 해당 스코프의 종료시점까지 관리한다.
기존 프로토타입 스프링 빈과 차이점은 프로토타입 스프링 빈은 생성 및 의존관계 주입까지만 스프링 컨테이너에서 관리하고 그 뒤로는 모두 클라이언트에서 관리를 해줘야 하지만, 웹 스코프의 경우 종류별로 관리의 범위가 다르다.
reuqest: 웹 요청이 들어오고 나갈때까지 유지되는 스코프
session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
application: 웹의 서블릿 컨텍스트와 같은 범위료 유지되는 스코프
websocket: 웹소켓과 동일한 생명주기를 가지는 스코프

주의점

: 내가 웹스코프로 사용하고자 하는 스프링 빈이 다음처럼 작성되었다고 하자.
@Component @Scope(value = "request") public class MyLogger { ... }
Java
복사
스코프의 설정을보면 request니 웹 요청이 들어올 때 생성되어 나갈때까지 유지되는 스프링 빈이다. 여기서 주의할 점은 웹 요청이 들어올 때 생성된다는 점인데, 이 말인즉슨, 앱 구동시점에서는 이 스프링 빈이 필요한 컨트롤러나 서비스등 여러 계층에서는 해당 스프링빈이 아직 주입되지 않았다는 의미이다. 그래서 그냥 실행시키려면 실행도 되지 않을 것이고 빈이 생성되지 않았다는 예외가 발생할 것이다. 그래서 다음 사용법을 보면서 방법에 맞게 사용을 해야 한다.

사용법

: 웹 스코프 사용법은 두 가지가 있다.

1. DL 서비스 객체 사용

Provider, ObjectFactory, ObjectProvider와 같은 DL 서비스를 제공해주는 객체를 이용하는 방식이다.
@Controller @RequiredArgsConstructor public class LogDemoController { private final ObjectProvider<MyLogger> myLoggerProvider; //... }
Java
복사
이처럼 DL 객체에 제네릭으로 사용 할 스프링 빈을 지정해주면 문제없이 동작한다.
그리고 해당 요청별로 스코프 범위에따라 스프링 빈이 생성 혹은 생성된 빈을 조회하여 반환해준다.

2. 프록시 활용

@Scope 애노테이션의 proxyMode라는 속성을 이용하여 스프링 빈을 지연로딩(lazy Loading)해서 해결하는 방법도 있다.
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyLogger { ... }
Java
복사
→ 만약 적용대상이 인터페이스라면 ScopedProxyMode.INTERFACE 를 사용해주면 된다.

스프링 빈의 지연 로딩(Lazy Loading) 원리

그럼 어떻게 지연로딩이 되는 것일까? 의존관계 주입시점에서 아직 생성이 안되었는데 예외가 발생을 안할 수 있는 이유는 무엇일까?

 CGLIB 라이브러리를 이용한 프록시 하위 객체 생성

CGLIB라는 바이트코드 조작 라이브러리를 이용하여 해당 스프링 빈의 객체를 상속받는 프록시 객체를 동적으로 생성하여 의존관계를 주입한다.
그리고 실제로 호출할 때 해당 프로시 객체는 원본객체를 생성해서 위임하도록 하여 문제를 해결한다.