Spring을 공부하면서 이래저래 길도 해매고, 공부 할 것도 많고 해서 TIL을 한동안 적지 않았다.
하지만 그럴수록 글로 정리를 해 가며 생각도 정리할 겸 복습의 개념이 될 수도 있다는 생각이 든다.
또한 지금껏 공부한 내용에 대해, 그리고 앞으로 배울 내용에 대해서도 정리를 해 보는 것이 공부에도 도움이 될 뿐더러, 나중에 잊은 개념을 다시 찾아볼 때도 내가 쓴 글이 가장 이해하기 쉬울 것 같아 다시 마음을 다잡고 블로그를 쓰기로 했다.
스프링을 이해하기 위해서는 스프링이 왜 개발된 것인지 알 필요가 있다.
전에 없던 무언가가 개발된 것이든, 이전의 형태에서 발전한 것이든, 결국은 필요에 의해 개발 된 것이고 이를 배우려면 그 필요 라는 것이 무엇인지를 알아야 목표를 명확히 할 수 있기 때문이다.
스프링이 사용되기 이전 자바EE(Java Enterprise Edition)는 강력한 기능을 제공했지만, 지나치게 복잡하고 무거운 구조로 인해 개발 생산성이 떨어지는 문제가 있었다고 한다.
스프링은 이 문제를 해결하고자 탄생하게 되었고 가볍고 유연한 자바 개발이라는 비전을 중심으로 빠르게 성장하게 되었다. 시간이 지나며 웹, 데이터베이스, 메시징, 보안, 클라우드까지 지원 범위를 확장했고, 현재는 자바 개발에서 거의 표준처럼 자리 잡게 되었다.그렇다면 현 시점, 우리는 왜 Spring을 배워야 할까?
답은 그냥 많이 쓰이기 때문이다.
물론 많이 쓰이는 데에는 이유가 있고 그게 사실 우리가 Spring을 배워야 하는 이유이며 써야 하는 이유이다.1. 생산성 향상
-스프링 부트를 사용하면 애플리케이션 개발을 빠르게 시작할 수 있다. 많은 설정이 자동화되어 초보자도 쉽?게 접근할 수 있다.2. 확장성과 유연성
-대규모 애플리케이션 개발에서도 확장성과 유지보수성이 뛰어나다. 특히 IoC와 DI는 이를 크게 돕는 기능라고 할 수 있다.3. 폭넓은 생태계
-스프링은 REST API, 데이터베이스 연동, 보안 등 현대적인 애플리케이션 개발에서 필요한 모든 기능을 지원한다.4. 커뮤니티와 문서
-전 세계의 개발자 커뮤니티와 풍부한 문서 덕분에 학습 리소스가 많아 문제 해결이 쉽다.더 찾아보자면 이런 것들 뿐만이 아니겠지만 사실 3, 4번의 이유만으로도 스프링을 배워야 할 이유로써는 충분하는 생각이 든다.
IoC(Inversion of Control)는 객체의 생성과 생명주기 관리를 개발자가 아닌 컨테이너(스프링 컨테이너)가 대신하도록 하는 설계 원칙으로 Spring에서 가장 중요한 개념이며,
DI(Dependency Injection) 매커니즘과 밀접한 관련이 있다.전통적인 개발 방식에서는 클래스가 직접 필요한 의존성을 생성하거나 관리했어야 했다.
가령 예를 들면 :public class Service { private Repository repository; public Service() { this.repository = new Repository(); }이 방식은 코드를 테스트하기 어렵게 만들고, 다른 구현체로 변경하는 유연성을 떨어뜨린다.
반면, 스프링의 IoC는 객체의 생성과 관리를 개발자가 아닌 스프링 컨테이너가 담당하여 결합도를 낮춘다.@Component class Service { private final Repository repository; @Autowired public Service(Repository repository) { this.repository = repository; // DI를 통한 주입 } }객체가 스스로 의존성을 생성하는 대신, 외부에서 객체의 의존성을 주입하고,
객체 생성, 초기화, 수명 관리 등 애플리케이션의 주요 제어권이 개발자가 아닌 IoC 컨테이너로 이동한다.말그대로 제어권이 역전된 이 상황에서 개발자는 객체의 행위에만 집중할 수 있게 되며, 객체간의 결합도가 낮아져 코드 재사용성과 유지보수성이 높아지고, 이로인해 애플리케이션의 구조 또한 유연하게 변경할 수 있게 된다.
IoC의 개념을 구현하는 방식 중 하나가 DI(Dependency Injection)이다.
의존성 주입은 객체가 필요로 하는 의존관계를 스프링 컨테이너가 제공(주입)하는 방법으로,코드의 결합도를 낮추고, 확장성을 높이고 테스트가 용이하다는 장점이 있다.DI는 주입 방식에 따라 세 가지로 나뉘는데, 이는 다음과 같다.
- 생성자 주입
-객체 생성 시에 필요한 의존관계를 생성자를 통해 주입.@Component public class OrderServiceImpl implements OrderService { private final Repository repository; public OrderService(Repository repository){ this.repository = repository; } }
- Setter 주입
-Setter 메서드를 통해 의존관계를 주입.@Component public class OrderServiceImpl implements OrderService { private Repository repository; @Autowired public void setRepository(Repository repository) { this.repository = repository; } }
- 필드 주입
-필드에 직접 주입하는 방식으로, 스프링 컨테이너가 접근할 수 있도록 필드를@Autowired로 설정.@Component public class OrderServiceImpl implements OrderService { @Autowired private Repository repository; }주로 생성자 주입이 권장되며, 필드 주입 방식은 여러 단점으로 인해 테스트 또는 간단한 구현을 할 때가 아니면 잘 사용되지 않는다.
스프링의 핵심 철학인 IoC와 DI를 실현하기 위해 스프링은 스프링 컨테이너라는 강력한 도구를 제공한다.
스프링 컨테이너는 애플리케이션에서 사용하는 객체를 생성하고, 관리하며, 필요한 곳에 주입하는 역할을 하고, 이 때 스프링 컨테이너에서 관리되는 객체를 스프링 빈(Spring Bean)이라고 부른다.
다음은 스프링 빈과 컨테이너의 개념, 빈 설정 방법, 컨테이너가 어떻게 빈을 관리하는지에 대해 정리한 내용이다.
스프링 공부를 시작하면 가장 많이 듣는 말 중 하나가 바로 "스프링 컨테이너"인데, 심지어 이 녀석은 이름도 부르는 사람마다 제각각이라 초반에 매우 헷갈렸다.
그래서 이게 뭐냐? 이 컨테이너라는 건 결국 "스프링이 객체(Bean)를 관리해주는 공간"이라고 보면 편하다.
쉽게 말하면, 객체를 언제 만들고, 어떻게 연결하고, 언제 없앨지 이런 것들을 알아서 처리해주는 똑똑한 관리자 같은 느낌이다. (예전에 게임을 만들 때, 싱글턴 아이들을 모아 관리하던 GameManager과 비슷한 역할을 한다.)
내가 머나먼 과거에 객체를 생성했을 때를 떠올려보자.
private MemberService memberService = new MemberService(new MemberRepository());지금은 이렇게 답없게 짜지는 않지만 분명 이렇게 코드를 짠 적이 있다.
물론 작고 간단한 프로젝트에선 이런 식으로 직접 객체를 생성하고 의존관계를 갖는 방식도 못쓸 건 아니다. 하지만 프로젝트 규모가 커지면 커질수록 점점 객체가 서로 복잡하게 얽히면서 관리가 상당히 어려워진다.
물론 여기서 임의의 컨테이너를 만들어 관리할 수도 있지만, 이미 다른 사람이 만들어놓은 컨테이너를 굳이 안쓸 이유는 없다.
그래서 여기서 스프링 컨테이너를 사용하게 된다면 어떤 일이 발생할까?
- 의존성 주입
객체 간의 관계를 자동으로 연결해준다.- 생성/소멸 관리
언제 객체를 만들고 없앨지 알아서 처리해준다.- 효율성
필요할 때만 객체를 생성하거나, 애플리케이션 시작 시 한 번에 생성하는 것도 가능하다.결론은, 내가 하기 귀찮은 것들을 알아서 해준다는 매우 큰 장점이 있다.
1. BeanFactory
- 가장 기본적인 컨테이너이다
- 필요할 때만 Bean을 생성하기 때문에 메모리 사용량이 적다.
- 근데
ApplicationContext안에 포함돼 있기 때문에 요즘은 따로 쓰진 않는다.2. ApplicationContext
- 대부분의 스프링 애플리케이션에서 사용되는 컨테이너 (사실상 "스프링 컨테이너"라는 말은 이녀석을 지칭한다고 볼 수있다.)
- BeanFactory를 확장한 기능을 제공
- 주요 특징:
- 애플리케이션 시작 시 모든 Singleton Bean을 초기화 (대충 의존관계도 알아서 잘 주입해 준다는 뜻)
- 이벤트(Event)와 리스너(Listener) 지원.
- AOP 및 트랜잭션 관리와 통합
- 구현체:
- AnnotationConfigApplicationContext: 자바 기반
@Configuration을 사용하는 컨테이너이다.- ClassPathXmlApplicationContext: XML 파일을 통해 설정을 로드하는 컨테이너인데, 얘네도 점점 줄어드는 추세라고 한다.
스프링 컨테이너의 동작은 생각보다 간?단하다.
- 설정 파일 로드
자바 설정이든, XML 설정이든간에 일단 컨테이너가 읽어온다.- Bean 생성
설정된 Bean(객체)을 컨테이너가 만든다.- 의존성 주입
DI 패턴을 사용해 알아서 잘 연결한다.- 초기화
필요한 초기 작업이 있으면 해준다.- 애플리케이션 실행
다 준비된 객체를 갖다가 내가 열심히 짜놓은 로직을 수행한다.- 소멸
컨테이너 종료 시, 소멸 콜백 메서드를 호출해서 리소스를 정리한다.
XML은 사용할 줄 모르는 관계로 Java설정을 사용하는 예제만 적어보았다.
@Configuration public class AppConfig{ @Bean public MemberService memberService(){ return new MemberService(memberRepository); } @Bean public MemberRepository memberRepository(){ return new MemberRepository(); } }이제 스프링 컨테이너를 통해 객체를 가져와보자.
public class Main{ public static void main(String[] args){ ApplicationContext appContext = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = appContext.getBean(MemberService.class); memberService.doAnything(); } }이 코드에서
appContext가 바로 스프링 컨테이너라 할 수 있겠다.
이 컨테이너가AppConfig를 통해MemberService와MemberRepository를 만들어다가 관리한다고 보면 된다.스프링 컨테이너는 애플리케이션의 중심이라고 할 수 있다.
객체 관리뿐만 아닌, 애플리케이션이 커질수록 더 유용한 기능들을 제공한다.
스프링 빈이라 함은 스프링 컨테이너가 관리하는 객체이다.
말인 즉슨, 평소 내가 열심히 객체 생성하고, 의존관계 주입하고, 제어하는 것을 스프링이 대신 해주는 것이다.
그렇다면 왜 그냥 객체를 쓰면 되지 스프링 빈이 필요할까?
아까 본 그 처참한 코드를 다시 소환해보면,
private MemberService memberService = new MemberService(new MemberRepository());다시 봐도 문제가 매우 많다는 것을 알 수 있다.
- 의존성 직접 관리
MemberService가MemberRepository에 의존한다는 것을 직접 설정하고 있다.- 재사용 어려움
MemberService가 또 필요하면 또 만들고 전역으로 관리해야 한다.- 테스트 어려움
새로운 의존성을 추가하려면 해당 소스 코드를 직접 열어서 수정해야 하고, 테스트하려면 코드가 복잡해진다.하지만 스프링 빈은 이러한 문제를 해결해준다.
스프링 컨테이너를 사용하는 이유와 같지 않냐는 궁금증이 생길 수 있는데,
실제로 같다.
스프링 빈은 총 세 가지 방법으로 등록할 수 있지만 XML은 할 줄 모르는 관계로 두 가지만 보도록 하자. (어차피 잘 쓰도 안한다고 한다.)
-
@Component사용@Component는 스프링이 객체를 알아서 스캔해서 등록하는 방식이다.@Component public class MemberService{ // ... }그리고 컴포넌트 스캔이 활성화 돼있어야 한다.
@Configuration @ComponentScan public class AppConfig{ }이렇게 하면
MemberService클래스가 스프링 빈으로 등록된다.
사실 컴포넌트 스캔은@Configuration에 포함 돼 있기 때문에 안적어도 된다.
아무튼 이렇게 한 번 등록해놓으면 나중에 ...context.getBean(MemverService.class)를 통해 언제든 꺼내 쓸 수 있다.
@Bean사용@Bean은 개발자가 직접 객체를 만들어 등록하는 방식이다.@Configuration public class Appconfig{ @Bean public MemberService memberService(){ return new MemberService(memberRepository); } @Bean public MemberRepository memberRepository(){ return new MemberRepository(); } }
Singleton스프링 빈은 기본적은로 싱글톤으로 관리된다.
말인 즉슨, 단 하나의 인스턴스만 만들어지고, 모든 요청에 대해 같은 객체를 제공한다는 것이다.MemberService memberService1 = appContext.getBean(MemberService.class); MemberService memberService2 = appContext.getBean(MemberService.class); assertThat(memberService1).isSameAs(memberService2);빈 등록이 잘 돼있다면 위 코드를 실행시 성공적으로 통과할 수 있다.
물론 필요에 따라 매번 새로운 객체를 생성하도록 설정할 수도 있다.
@Scope("prototype")
- 요청 할 때마다 새로운 객체 생성
@Scope("request")
- 웹 요청마다 새로운 객체 생성
@Scope("singleton")
- 디폴트값이며, 굳이 적을 필요는 없지만 실은 생략돼있다.
의존관계 주입 지원
스프링 빈은 서로 필요한 의존관계를 자동으로 주입받을 수 있다.
가령 예를 들어,MemberService가MemberRepository를 필요로 한다면,@Component public class MemberService{ private final MemberRepository memberRepository; @Autowired public MemberService(MemberRepository memberRepository){ this.memberRepository = memberRepository; } }스프링이 알아서
MemberRepository를 찾아서 주입해준다.
위의 DI에서 설명 했던 것 처럼 필요에 따라 생성자, 필드, 메서드를 통해 의존관계를 주입할 수 있지만 주로 생성자를 통해 주입한다.생명주기 관리
스프링 빈은 초기화부터 소멸까지 생명주기를 관리해준다.
특정 작업이 필요하다면, 스프링은 크게 세 가지 방법을 통해 빈 생명주기 콜백을 지원한다.
@PostConstruct,@PreDesrtroy
최신 스프링에서 가장 권장하는 방법으로, 어노테이션 하나만 붙이면 되기 때문에 매우 편리하다. 또한 스프링에 종속적인 기술이 아닌 자바 표준이기 때문에, 스프링이 아닌 다른 컨테이너에서도 동작한다. 하지만 스프링의 컴포넌트 스캔과 매우 잘 어울린다.
유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것인데, 외부 라이브러리를 초기화, 종료 해야 하면@Bean의 기능을 사용하면 된다.@Component public Class MemberService{ @PostConstruct public void init(){ //init } /* ... */ @PreDesrtroy public void close(){ //destroy } }
@Bean(initMethod="메서드명",destroyMethod="메서드명2")
빈을 직접 등록 할 당시에 생명주기 콜백 메서드의 메서드명을 지정해 실행할 수 있다.
여기서 재밌는 점은destroyMethod의 디폴트값이"(inferred)"인데, 직역하자면 "추론" 즉, 소멸 메서드의 이름을 추론해shutdown와close중 해당하는 메서드명이 있으면 알아서 실행한다.@Configuration public class Appconfig{ @Bean(initMethod="init",destroyMethod="close") public MemberService memberService(){ return new MemberService(memberRepository); } @Bean public MemberRepository memberRepository(){ return new MemberRepository(); } }public Class MemberService{ public void init(){ //init } /* ... */ public void close(){ //destroy } }
인터페이스
DisposableBean과InitializingBean라는 인터페이스를 활용해 콜백을 지원한다. 다만, 이 인터페이스는 스프링 전용이라 코드가 스프링 전용 인터페이스에 의존하게 된다. 또한 초기화, 소멸 메서드의 이름을 변경할 수 없는 것은 물론, 외부 라이브러리에도 적용할 수 없다. 그러면 이건 과연 어쩔 때 쓰냐?
안쓴다.import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; public class MemberServiece implements InitializingBean, DisposableBean { @Override public void afterPropertiesSet() throws Exception { //초기화 } @Override public void destroy() throws Exception { //소멸 } }쓰이지도 않는 게 왜 존재하냐는 의문을 가질 수 있는데, 이는 스프링 초창기에 나온 방법이라 지금은 쓰이지 않을 뿐, 원래부터 쓸모없는 존재는 아니었다,
더 좋은 방법들이 생겼기에 나는 단지 그걸 갖다 쓰면 될 뿐이다.
참고로 위의 세 방법은 모두 똑같이 동작하기에, 굳이 쓰고 싶다면 못 쓸 것도 없긴 하다.
그동안은 공부에 모든 시간을 할애하느라 블로그를 쓸 시간이 없다는 핑계로 블로그를 유기해왔는데, 막상 쓰면서 보니 배운 내용을 다시 정리하는 게 생각보다 큰 도움이 되지 않나 싶다. 앞으로는 매일은 아닐지라도 자주 기록하며 생각도 정리하는 시간을 가질 예정이다.