스프링이 어떻게 자바의 객체 지향의 특성을 잘 살려주도록 도와주는지 그 과정을 살펴보며 사용방법을 배워보자.
자바는 객체 지향 언어이다.
스프링 프레임워크은 좋은 객체 지향 애플리케이션을 만들 수 있도록 도와준다.
객체 지향 프로그래밍은
컴퓨터 프로그램을 명령어의 목록으로 보는 것이 아닌
객체들의 모임으로 파악하고 객체들 간의 메세지를 주고 받으며 데이터를 처리한다.
-> 결국 이는 유연하고 변경이 용이하게 만들어주어 대규모 개발에 많이 사용된다.
예를 들어 어떤 업체가 할인 정책을 두가지 가지고 있다고 가정해보자
한가지는 VIP에게 사는 가격에 10% 할인해주는 것
나머지는 VIP에게 사는 가격에 대해 1000원 깍아주는 것이다.
만약에 이 할인 정책이 분기마다 바뀐다고 했을 때
개발자는 아래와 같이 코드를 매번 바꿔줘야 한다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
위 코드는 의존성 역전 원칙(DIP)을 위반한다. 의존성 역전 원칙은 그 구체적 구현체에 의존하는 것이 아니라 추상에 의존하는 것을 말한다. 위 코드는 인터페이스에만 의존하는 것이 아니라 인터페이스를 구현하는 구현체에도 의존한다.
또한 개방-폐쇄 원칙도 위반한다. 변경에 자유롭지만 고쳐야할 부분은 적어야 하는데 위 코드는 이것 또한 지켜지지 않는다.
개발자들은 이 생각이 들었다.
그렇다면 생성자들을 한번에 모아놓은 외부 클래스를 만들자.
그러면 이제 구체적 구현체를 변경할 때 이 하나의 외부 클래스만 고치면 되는 것이다.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
이제 AppConfig만 고치면 된다. 또한 이 클래스를 이용해서 생성자를 주입하도록 하자.
💡그러기 위해서는 MemberServiceImpl와 OrderServiceImpl의 생성자 위 형태로 만들어줘야 한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//생성자 주입
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
//생성자 주입
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
💡 그러면 이제 어떤 형태로 사용하는지 보자
public class MemberApp {
public static void mian(String[] args){
AppConfig appConfig = new AppConfig();
//AppConfig에서 생성자를 가지고 온다
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
}
}
💡위 AppConfig에 중복이 있어 살짝 수정하자.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new memberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(
new memberRepository(),
new disocuntPolicy());
}
public MemberRepository memberRepository(){
return MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return FixDiscountPolicy();
}
}
외부 클래스 AppConfig가 모든 구현 객체들에 대한 제어 흐름을 관리한다. 구현 객체는 실제로 자기가 어떤 걸 실행하는지 모른다. FixDiscount 정책인지 RateDiscount 정책인지 알지 못하고 이를 결정하는 결정권자는 이제 외부 클래스가 되는 것이다. 이것을 제어의 역전(Inversion of Control)이라고 한다.
의존관계 주입(Dependency Injection)은 정적인 클래스 의존관계와 동적인 객체 인스턴스 의존관계가 있는데 정적인 클래스 의존관계는 구현 객체 시점의 의존관계라고 볼 수 있다. 실제로 FixDiscount 정책인지 RateDiscount 정책인지 알지 못하는 상태를 나타내는 관계이다.
동적인 객체 인스턴스 의존관계는 외부 클래스 AppConfig 시점의 의존관계로 실제로 구현되는 객체들만의 관계를 나타내는 의존관계이다.
AppConfig처럼 객체를 생성하고 의존관계를 연결해 주는 것을 IoC컨테이터 또는 DI 컨테이너라고 한다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuraion
public class AppConfig {
@Bean
public MemberService memberService() {
return new memberRepository();}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
new memberRepository(),
new disocuntPolicy());}
@Bean
public MemberRepository memberRepository(){
return MemoryMemberRepository(); }
@Bean
public DiscountPolicy discountPolicy(){
return FixDiscountPolicy();}
}
public class MemberApp {
public static void mian(String[] args){
//AppConfig appConfig = new AppConfig();
//AppConfig에서 생성자를 가지고 온다
//MemberService memberService = appConfig.memberService();
//OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext = new AnnotaionConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
}
}
@Configuration이 붙은 AppConfig를 설정 정보로 사용하여 @Bean이 붙은 메서드를 호출해서 반환된 객체들(생성자가 return값이다.)을 모두 스프링 컨테이너에 등록한다. 이 등록된 객체들을 스프링 빈이라고 하며 스프링 빈의 이름은 메서드명이 된다.
따라서 getBean()을 통해서 스프링 컨테이너에 등록된 객체를 찾을 때 "메서드명"으로 찾는 것을 확인할 수 있다.
스프링 컨테이너란 자바 객체의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 담당하는데 앞서 AppCofing가 DI컨테이너였는데 이를 스프링에서는 스프링 컨테이너가 담당한다고 보면 된다.
ApplicationContext ac =
new AnnotaionConfigApplicationContext(AppConfig.class);
ApplicationContext는 스프링 컨테이너이며 인터페이스이다.
이 인터페이스의 구현체가 AnnotaionConfigApplicationContext이다.
또한 스프링 컨테이너를 생성할 때 설정정보를 지정해주는데
이 설정 정보에 있는 객체들을 스프링 컨테이너에 등록한다. 즉 스프링 빈으로 만들어준다는 이야기이다. 스프링 빈으로 등록할 때는 의존관계도 같이 주입된다.
관계는 BeanFactory<-ApplictionContext<-AnnotationConfigApplication의 관계이다.
BeanFactory가 스프링 컨테이너의 최상위 인터페이스이며 getBean()의 기능을 제공하고 여기에 추가적으로 부가적인 기능을 가진 것인 ApplicationContext이다.
+) 앞서 설정정보로 AppConfig.class 정보를 설정했지만 그 외에도 다양한 설정 형식을 지원한다. ex) Groovy, XML 등
이것이 가능한 이유는 BeanDefinition을 통해서 역할과 구현을 나눴기 때문이다. 스프링 빈은 설정 정보가 어떤 형식인지는 관심 없고 오직 BeanDefinition에만 관심있다. 이 BeanDefinition에는 스프링 빈 설정 메타 정보가 저장되어 있다.
앞서 AppConfig를 통해서 순수 DI 컨테이너도 만들었고
@Configuraion,@Bean,ApplicationContext를 통해서 스프링을 이용해 스프링 컨테이너도 만들었다.
왜 굳이 순수한 DI 컨테이너가 아닌 스프링 컨테이너를 이용할까?
그 이유는 스프링 컨테이너는 싱글톤 패턴을 유지해주기 때문이다.
한 프로그램이 실행될 때 여러 클라이언트가 접속을 할 것이다.
만약 순수 DI 컨테이너로 설정되어져 있다면 사용자마다 객체를 새로 생성한다. 이는 메모리 낭비를 발생시키기 때문에
스프링 컨테이너는 싱글톤을 통해서 딱 하나의 객체만 생성하고 이를 여러 사용자가 공유할 수 있도록 보장한다.
클래스의 인스턴스가 딱 하나만 생성되는 것을 보장하는 디자인패턴
외부에서 생성자를 통해 인스턴스를 만드는 것을 막아두어야 한다.
이와 같이 싱글톤 패턴을 구현하기 위해서는 코드 자체가 많이 들어가고 구체 클래스에 의존한다. 또한 생성자가 private이기 때문에 자식 클래스를 만들기 어렵다
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 하나만 관리
스프링 컨테이너는 스프링 빈을 싱글톤으로 관리한다고 한다.
따라서 여러 사용자는 스프링 빈으로 등록된 인스턴스를 모두 공유하는 것이다.
스프링 빈에 등록된 인스턴스 = 어떤 클래스의 객체
만약 상태를 바꿔주는 메서드가 그 객체가 속한 클래스에 선언되어 있다면 문제가 발생된다. A 사용자가 객체의 속성을 바꿔놓는다면 그 영향이 그대로 B 사용자에게 가는 것이다. 따라서 스프링 빈으로 등록된 인스턴스들은 상태를 바꿔주는 속성과 메서드가 포함되지 않도록 설계해야 한다.
@Configuration은 바이트 코드를 조작하는 라이브러리를 사용하여 하나의 인스턴스가 생성되도록 해준다.
@ComponentScan과 짝궁은 @Component와 @Autowired다.
스프링 컨테이너에 등록할 스프링 빈이 수백개라면 모두 AppConfig.class 설정 정보 등록하는 일은 귀찮다.
그래서 등장한 것인 @ComponentScan이다.
package hello.core;
...
@Configuration
@ConponentScan(
excludeFilters = @Filter(type=FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
/*
@Bean
public MemberService memberService() {
return new memberRepository();}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
new memberRepository(),
new disocuntPolicy());}
@Bean
public MemberRepository memberRepository(){
return MemoryMemberRepository(); }
@Bean
public DiscountPolicy discountPolicy(){
return FixDiscountPolicy();
}
*/
}
코드를 살펴보면 주석처리가 된 스프링 빈들을 확인할 수 있다.
즉 이제는 설정정보에 하나하나 스프링 빈들을 등록하는 일을 하지 않고 직접 그 클래스에 가서 @Component를 달아준다.
@Component가 있으면 @ComponentScan이 그 클래스를 스프링 빈으로 등록한다.
그렇다면 의존관계는 어떻게 주입받을까?
아래와 같이 의존관계가 명시될 수 있는가?@Bean public MemberService memberService() { return new memberRepository();} @Bean public OrderService orderService() { return new OrderServiceImpl( new memberRepository(), new disocuntPolicy());}
에 대한 물음은 @Autowired가 해결해준다.
@ComponentScan이 어디에 위치해 있는가는 어디서부터 @Component가 선언된 클래스의 스프링 빈을 등록할 것인가를 결정한다. 참고로 @ComponentScan이 선언된 클래스가 속한 패키지가 그 시작위치가 된다.
하지만 "basePackages"속성을 이용해서 탐색할 패키지의 시작 위치를 결정할 수도 있고 "basePackageClasses"를 통해서 지정한 클래스의 패키지를 시작 위치로 정할 수도 있다.
중요한 것은 우리가 @ComponentScan을 통해서 시작 위치를 지정할 일은 별로 없다 왜냐하면 @SpringBootAplication이 프로젝트 시작 루트에 지정되어져 있고 이 설정 안에 @ComponentScan이 들어있다.
@Component, @Controller, @Service, @Repository, @Configuration
의존관계 주입이란
스프링 컨테이너 있는 스프링 빈들 간의 관계를 설정해주는 것!
의존관계를 주입하는 방법에는 4가지가 있다
생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입
가장 권장하는 방법은 생성자 주입이며 각 방법들을 살펴보자
불편, 필수의 의존관계
생성자가 딱 하나 있으면 @Autowirted 생략 가능
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private fianl DiscountPolicy discountPolicy;
//@Autowirted 생성자 하나여서 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
선택, 변경 가능성이 있는 의존관계
@Component
public class OrderServiceImpl implements OrderService {
// private final MemberRepository memberRepository;
//private fianl DiscountPolicy discountPolicy;
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowirted
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowirted
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
필드에 바로 주입, 간결하지만 외부에서 변경이 불가능해서 테스트가 힘들다 -> 사용❌
@Component
public class OrderServiceImpl implements OrderService {
// private final MemberRepository memberRepository;
//private fianl DiscountPolicy discountPolicy;
@Autowirted
private MemberRepository memberRepository;
@Autowirted
private DiscountPolicy discountPolicy;
}
@Component
public class OrderServiceImpl implements OrderService {
//private final MemberRepository memberRepository;
//private fianl DiscountPolicy discountPolicy;
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowirted
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
은 @Component와 @Autowired라고 했다.
그리고 의존관계 주입을 할 때 생성자 주입을 통해 하는 것을 권장했다.
여기서 더 편한 방법이 생겼는데
롬복 라이브러리에서 제공하는 @RequiredArgsConstructor기능이다.
이 기능은 final 키워드가 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private fianl DiscountPolicy discountPolicy;
//@Autowirted 생성자 하나여서 생략 가능
/*
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
*/
}
따라서 위와 같이 생성자에 대한 코드가 생략된다.
이제 @ComponentScan의 짝궁은 @Component와 @RequiredArgsConstructor이다.
@Autowired는 의존관계를 주입하기 위해서 스프링 빈을 찾을 때 ac.getBean(타입)을 사용해서 조회하는 것과 유사하게 찾는다.
타입은 추상 클래스가 될 것이다.
앞에 언급하였지만 스프링 빈으로 등록된 것은 아래와 같다.
@Component가 있으면 @ComponentScan이 그 클래스를 스프링 빈으로 등록한다.
- MemoryMemberRepository 클래스에 @Component추가
- FixDiscountPolicy 클래스에 @Component추가
하지만 내가 여기에 굵은 글씨로 된 부분을 스프링 빈으로 등록한다면 @Autowired로 의존관계를 주입할 때 두 개의 스프링 빈이 조회될 것이다. (Fix와 Rate)
@Component가 있으면 @ComponentScan이 그 클래스를 스프링 빈으로 등록한다.
- MemoryMemberRepository 클래스에 @Component추가
- FixDiscountPolicy 클래스에 @Component추가
- RateDiscountPolicy 클래스에 @Component추가
이 경우 오류가 발생하기 때문이 이를 해결하는 몇 가지 방법을 살펴보자.
@Component
public class FixDiscountPolicy implements DiscountPolicy{}
@Component
public class RateDiscountPolicy implements DiscountPolicy{}
@Autowired
private Discount rateDiscountPolicy
등록된 스프링빈 이름과 똑같이 바꾼다.
스프링빈 이름을 변경하는 것이 아니라 거기에 추가로 구분자를 넣어주는 것
@Component // 스프링 빈 이름은 여전히 fixDiscountPolicy
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{}
@Component // 스프링 빈 이름은 여전히 rateDiscountPolicy
@Qualifier("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Component
@Primary
public class FixDiscountPolicy implements DiscountPolicy{}
@Component
public class RateDiscountPolicy implements DiscountPolicy{}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
DB 컨넥션 방식을 선택해야 한다고 할 때
메인 DB는 @Primary로, 가끔씩 사용하는 DB는 @Qualifier로 지정하자. 두 개의 우선순위를 말하자면 @Qualifier가 더 높다. 따라서 두 개가 중복해서 있다고 할 때 더 자세히 명시하는 @Qualifier를 언급한다면 @Qualifier가 붙은 DB 컨넥션이 호출된다! 평상시에는 아무것도 언급하지 않으면 @Primary가 붙은 DB컨넥션이 호출된다