스프링 핵심 원리 이해1 - 예제
비지니스 요구사항과 설계
- 회원일반, vip 등급의 회원 존재
- DB는 미정
- 회원 가입, 조회 가능
- 주문과 할인 정책등급에 따라 할인 정책 가능
- VIP는 무조건 1000원 할인 (정책 가정, 확정은 아님)
- 회원이 상품 주문가능
=> 미정인 부분은 인터페이스로 역할/구현을 분리하면 된다!!
** 일단은 스프링이 아닌 순수 자바코드로 구현한다고 가정
회원 도메인
- 클라이언트 -> 회원서비스(가입, 조회) -> 회원 저장소(미정)
- MemberService(구현체: MemberServiceImpl) -> MemberRepository(구현체: 일단 MemoryMemberRepository)
- 클라이언트 객체 -> 회원서비스 객체(MemberServiceImpl) -> 멤버리포지토리 객체
회원 도메인 개발
회원 등급, 회원 엔티티 필요
회원등급
package hello.core.member;
public enum Grade {
BASIC,
VIP
}
회원 엔티티
package hello.core.member;
//id, name, grade 필요
public class Member {
private long id;
private String name;
private Grade grade;
public Member(long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public void setId(long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Grade getGrade() {
return grade;
}
}
회원 저장소
회원저장소 인터페이스, 구현체
회원저장소 인터페이스 - 저장, 멤버찾기 기능
package hello.core.member;
//id, name, grade 필요
public class Member {
private long id;
private String name;
private Grade grade;
public Member(long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public void setId(long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Grade getGrade() {
return grade;
}
}
구현체 - 일단 메모리 저장소
package hello.core.member;
import java.util.HashMap;
import java.util.Map;
//저장소 필요
public class MemoryMemberRepository implements MemberRepository{
//일단 임시 저장소 = 메모리 사용
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(),member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);//store는 Map이므로 키값으로 member찾음
}
}
회원 서비스
회원 가입과 조회 기능이 필요하다.
회원 서비스 인터페이스 (역할)
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
구현
package hello.core.member;
//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{
MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
일단 멤버도메인, 멤버서비스 ,멤버저장소 구현완료!!
일단 main으로 테스트
package hello.core.member;
//테스트해보는 용도
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1l, "memberA", Grade.VIP);
//회원가입
memberService.join(member);
//가입한 멤버가 있는지 조회
Member findMember = memberService.findMember(1L);
//확인
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
은 안좋으니 Junit으로 테스트
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join(){
//given - vip
Member member = new Member(1L, "memberA" , Grade.VIP);
//when - 회원가입
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
//then - 가입한 회원이 맞는지
Assertions.assertThat(member).isEqualTo(findMember);
}
}
=> memberServiceImpl에서 MemberRepository memberRepository = new MemoryMemberRepository(); 로 구현체를 직접 참조하고 있다. 따라서 구현에 의존하게 되는 문제점이 발생한다.!
주문과 할인 도메인 설계
- 회원은 상품 구매가능
- 등급에따라 할인됨
- 할인정책으론 모든 vip는 1000원 할인
- 클라이언트 - (주문생성) -> 주문 서비스 역할 - (회원조회) -> 회원저장소 역할
- 주문생성(id, 상품명, 상품가격), 회원조회(id로 조회) => 상품명과 상품가격은 간단하게 객체가 아니라 data로 만듬
- 주문서비스 역할 - (할인 적용) -> 할인 정책역할
- 할인 적용 ( vip인가? 확인)
- 주문서비스 역할 - (주문결과 반환) -> 클라이언트
- 주문결과는 간단하게 DB에 저장이 아니라 주문결과를 반환
클라이언트 -> 주문서비스 -> 1. 회원저장소 2.할인정책
할인 정책에는 고정가 할인 정책과 비율 할인 정책이 있다.
할인 정책 인터페이스
package hello.core.discount;
import hello.core.member.Member;
//vip 인지 확인해서 할인된 상품가격 결과를 반환한다.
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
고정가 할인 정책
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
//vip이면 1000원 할인, 할인금액 반환
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return discountFixAmount;
}
else{
return 0;
}
}
}
주문 서비스
먼저 주문 엔티티를 만든다.
(회원 id, 상품명, 상품가격)
package hello.core.order;
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
//할인된 결과 필요
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
this.memberId = memberId;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
//할인된 결과
public int calculatePrice(){
return itemPrice - discountPrice;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
public Long getMemberId() {
return memberId;
}
public String getItemName() {
return itemName;
}
public int getItemPrice() {
return itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
//결과 쉽게 보기 위해서
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
//저장소 조회
private final MemberRepository memberRepository = new MemoryMemberRepository();
//할인을 위해서 할인 정책 필요
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//id로 조회해서 vip 이면 할인정책 적용
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
주문과 할인 테스트
package hello.core.order;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import net.minidev.json.JSONUtil;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
//회원등급을 조회하고 그에맞는 할인이 적용되었는가?
//주문서비스 -> 저장소, 할인정책
public class OrderServiceTest {
//given
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
//주문 테스트
@Test
void createOrder() {
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
//when
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
//then
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
새로운 할인 정책 개발
10프로 할인 정책!
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
//할인율
private int discountPrice = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price*discountPrice/100;
}
else{
return 0;
}
}
}
새 할인 정책 적용!
OrderService 구현체에서 직접 선택해줘야된다 => 따라서 구현체에 의존하게된다.(인터페이스가 아니라)
클라이언트(OrderServiceImpl)의 코드를 수정해야한다.
OCP => 변경하지않고 확장가능 => 깨짐
DIP => 인터페이스 뿐만 아니라 구현체에 의존하게됨.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
//저장소 조회
private final MemberRepository memberRepository = new MemoryMemberRepository();
//할인을 위해서 할인 정책 필요
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy= new RateDiscountPolicy();
//id로 조회해서 vip 이면 할인정책 적용
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
따라서
클라이언트(OrderServiceImpl)를 공연의 예제에 비교하면
배우(로미오 역)이 직접 여배우(줄리엣)역을 섭외하는 격이다!!
배우는 대본에만 집중을 해야지 섭외의 역할까지 맡게되면서 다양한 책임을 가지게 된다.
그래서 관심사를 분리해야한다.
배우는 배우 역할에만 신경쓰게!
따라서 공연기획자가 필요하다.
공연을 구성, 배우 섭외, 역할에 맞는 배우를 지정하는 책일을 가지는 공연 기획자가 필요!
바로 공연기획자가 AppConfig이다.
AppConfig
애플리케이션의 전체 동작 방식을 구성한다.
구현 객체를 생성하고, 연결하고 책임을 가진다.
위 역할을 하는 별도의 설정 클래스이다.
package hello.core;
import hello.core.discount.DiscountPolicy;
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());
}
}
각 클라이언트의 생성자를 이용하여 맞는 구현체를 넣어준다.
=> 생성자 주입기법
그러면 이제 각 구현체에서 생성자를 통해서 받아오기만 하면 된다!!!
멤버서비스 구현체
package hello.core.member;
//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{
//배역 배우로 생각할때
//MemberRepository memberRepository = new MemoryMemberRepository();
//생성자로 맞는 구현체 가져옴
private final MemberRepository memberRepository;
//생성자
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
주문 서비스 구현체
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
//저장소 조회
//private final MemberRepository memberRepository = new MemoryMemberRepository();
//할인을 위해서 할인 정책 필요
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//private final DiscountPolicy discountPolicy= new RateDiscountPolicy();
//생성자 주입기법으로 맞는 구현체 가져옴
private final MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//id로 조회해서 vip 이면 할인정책 적용
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
AppConfig를 통하여 주입받기 위해 MemberApp 클래스를 수정한다.
package hello.core;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
//테스트해보는 용도
public class MemberApp {
public static void main(String[] args) {
//AppConfig를 통하여 주입
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();//주입!
//멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
//즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
//밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
//MemberService memberService = new MemberServiceImpl();
Member member = new Member(1l, "memberA", Grade.VIP);
//회원가입
memberService.join(member);
//가입한 멤버가 있는지 조회
Member findMember = memberService.findMember(1L);
//확인
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
주문 서비스 또한 appConfig로 인젝션을 해준다.
package hello.core;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
//주문 하기
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.orderService(); //주입!!\
MemberService memberService = appConfig.memberService();
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("Order = " + order);
}
}
각 테스트코드 또한 바꾸어 준다.
@BeforeEach를 사용하여 appconfig로 주입시켜준다.
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
//MemberService memberService = new MemberServiceImpl();
MemberService memberService;
@BeforeEach
void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join(){
//given - vip
Member member = new Member(1L, "memberA" , Grade.VIP);
//when - 회원가입
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
//then - 가입한 회원이 맞는지
Assertions.assertThat(member).isEqualTo(findMember);
}
}
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
//MemberService memberService = new MemberServiceImpl();
MemberService memberService;
@BeforeEach
void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join(){
//given - vip
Member member = new Member(1L, "memberA" , Grade.VIP);
//when - 회원가입
memberService.join(member);
Member findMember = memberService.findMember(member.getId());
//then - 가입한 회원이 맞는지
Assertions.assertThat(member).isEqualTo(findMember);
}
}
AppConfig 리팩터링
기존
public class AppConfig {
//메모리멤버리포지토리가 두번이나 생성되어 중복된다.
//그리고 각 역할과 구현이 뚜렷하게 눈에 보이지 않는다.
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
리팩터링 후
public class AppConfig {
//멤버 서비스는 멤버리포지토리가 필요하다.
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
//리포지토리는 멤버 리포지토리
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
//할인정책은 FixDiscountPolicy
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
//return new FixDiscountPolicy();
}
}
중복이 제거되었고, 각 역할과 구현이 명확하게 보인다.
또한 AppConfig 설정을 바꿔줌으로써 할인정책을 바꿀수 잇게 되었다.
따라서 클라이언트의 코드를 수정하지 않고 변경가능!!!
OCP = 확장에 유연, 변경에는 닫힘
을 지킬 수 있고
DIP = 인터페이스에 의존
도 가능해졌다~~!
AppConfig를 사용함으로써
사용영역과 구성영역으로 구분이 되었고, 변경사항시 구성영역만 변경하면 되게 되었따.
또한 구성영역을 사용함으로써 역할과 구현이 명확하게 분리 되고, 역할이 잘 보이게 되었고, 중복이 제거되었다.
정리하면, SRP, DIP, OCP를 적용하게 되었다.
SRP
한 클래스는 하나의 책임만 가져야한다.
기존 - 클라이언트가 직접 구현객체생성, 연결, 실행
AppConfig 사용 - 구현객체를 AppConfig가 생성하고 연결함, 따라서 클라이언트는 실행하는 책임만 가지게됨.
DIP 의존관계 역전 원칙
프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."
=> 의존관계 주입은 이 원칙을 따르는 방법 중 하나이다.
기존 - 새로운 할인정책을 만들고 적용시킬때, 클라이언트 코드 수정해야했음
AppConfig 사용 - AppConfig가 할인정책 객체 인스턴스를 클라이언트 코드 대신에 생성해서 클라이언트 코드에 의존관계를 주입했다.
즉, 외부에서 객체 인스턴스를 넣어주어 DIP원칙을 지켰다.
OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.
다형성을 사용하고 클라이언트가 DIP를 지키면서 OCP가능성이 높아졌다.
애플리케이션 => 사용영역 / 구성영역으로 분리되었다.
AppConfig가 의존관계를 클라이언트에 주입해주어 클라이언트 코드변경X
따라서, 소프트웨어 요소를 새로 확장해도 사용 영역의 변경은 없다 => 즉 닫혀있다.!
IoC, DI, 컨테이너
IoC? => Inversion of Control(제어의 역전) , 제어를 AppConfig가 가지게 됨(외부에서 제어흐름을 가지게 됨)
기존 - 클라이언트 구현객체가 스스로 서버구현객체 생성, 연결, 실행 => 구현 객체가 프로그램 제어흐름 조종!
AppConfig - 구현객체는 자신의 로직만 실행, 제어흐름은 AppConfig가 해줌
(예를 들면 OrderServiceImpl은 필요한 인터페이스를 호출 하지만 어떤 구현객체가 올지 모른다. => AppConfig가 주입해줌)
프레임워크 vs 라이브러리
프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다 => ex) Junit
내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리이다. => 직접 메서드 호출 등..
DI(Dependecy Injection)= 의존관계 주입
OrderServiceImpl(구현체)는 DiscountPolicy(인터페이스)에 의존한다. => 어떤 DiscountPolicy가 올지 모른다.
의존관계는 정적인 클래스 의존 관계, 실행시점에 결정되는 동적인 객체 의존 관계를 분리해서 생각해야한다.
- 정적인 클래스 의존 관계즉, 애플리케이션을 실행하지 않아도 분석가능!
- 하지만 어떤 구현객체가 주입되는지는 모름!
- 클래스가 사용하는 import 코드만 보고 의존관계 파악이 가능하다.
- 동적인 객체 의존 관계애플리케이션 실행 시점(런타임)에 외부에서 실제 구현객체를 생성해서 클라이언트에 전달!의존관계 주입을 통해서 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.
- 클라이언트와 서버 의존관계가 연결된다. => 의존관계 주입
- 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계
IoC컨테이너 = DI컨테이너 (어셉블러(조립), 오브젝트 팩토리(오브젝트를 만드니까) 등으로 불리기도함)
AppConfig와 같은 역할
객체를 생성, 관리하면서 의존관계를 연결해주는 것.
살짝 정리
이때까지 순수 자바코드로 DI, 의존관계 주입을 해보았다.
다음으론 스프링을 사용하여 의존관계 주입을 해본다.
스프링으로 전환
AppConfig 스프링 기반으로 변경
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;
//의존관계 주입!
@Configuration // 설정을 구성한다고 알려줌
public class AppConfig {
@Bean//스프링 빈으로 등록
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
MemberApp에 스프링 컨테이너(객체 생성, 관리, 의존관계 주입) 적용
package hello.core;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
//테스트해보는 용도
public class MemberApp {
public static void main(String[] args) {
//AppConfig를 통하여 주입
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();//주입!
//멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
//즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
//밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
//MemberService memberService = new MemberServiceImpl();
/**
* 스프링 컨테이너 적용
*/
//ApplicationContext를 스프링 컨테이너라 보면된다. 스프링은 모든것이 이 A.C로 부터 시작된다.
//AppConfig로부터 구성 정보를 가져온다.
// => 스프링이 빈들을 설정해서 스프링 컨테이너에 객체를 생성해서 갖고 있는다.
//어노테이션 기반으로 config를 하므로 AnnotationConfigApplicationContext 사용
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
//getBean(메서드명, 타입)으로 가져온다.
//MemberServiceImpl.class와 같이 구체 타입을 지정해도 되나, 구현에 의존하게 되는 꼴이 되버린다.
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1l, "memberA", Grade.VIP);
//회원가입
memberService.join(member);
//가입한 멤버가 있는지 조회
Member findMember = memberService.findMember(1L);
//확인
System.out.println("new member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
OrderApp에도 스프링 컨테이너를 적용시킨다.
package hello.core;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
//주문 하기
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// OrderService orderService = appConfig.orderService(); //주입!!\
// MemberService memberService = appConfig.memberService();
//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
//빈 가져옴
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("Order = " + order);
}
}
실행 후 로그를 보면
17:06:34.789 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
17:06:34.808 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
17:06:34.810 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
17:06:34.811 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
"Creating shared instance of singleton bean" 스프링에 등록된 것을 알 수 있다.
스프링에 등록시 (key(메서드명), value(타입))으로 저장이된다.
이걸 정리하면
- ApplicationContext = 스프링 컨테이너 (Context = 문맥)
- 기존엔 개발자가 AppConfig를 사용해서 직접 객체 생성하고 DI
- @Configuration 어노테이션이 붙은 AppConfig를 설정정보로 사용한다.
- @Bean 이 붙은 메서드를 모두 호출하고 반환된 객체를 스프링 컨테이너에 등록한다. (등록된 객체 = 스프링 빈)
- 관례로 @Bean이 붙은 메서드 명을 스프링 빈 이름으로 사용한다. (@Bean (name = "AA")와 같이 변경도 가능한데 왠만하면 관례따르는게 보기 편하고 관리 편하다.)
- 기존엔 직접 필요한 객체를 AppConfig를 이용해서 조회했다.
- => 스프링 컨테이너를 통해서 applicaionContext.getBean()메서드를 사용해서 스프링빈, 즉 객체를 찾게되었따.
즉, 직접 자바코드로 모든 것을 하다가
- 스프링 컨테이너에 객체를 스프링 빈으로 등록
- 스프링 컨테이너에서 스프링 빈을 찾아서 사용
과 같이 바뀌게 되었다.
이렇게 바꾸었을 때 장점은 무엇일까?..... => 뒤에서
스프링 컨테이너와 스프링 빈
스프링 컨테이너 생성과정을 다시 보면
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext 이 스프링 컨테이너이고, 인터페이스이다.
따라서 다형성이 적용된다.
"new AnnotationConfigApplicationContext(AppConfig.class);"클래스는 ApplicationContext 인터페이스의 구현체이다.
스프링 컨테이너는 어노테이션 기반의 자바 설정 클래스로 만들수도 있고, XML을 기반으로 만들 수도 있다.
정리하면
- 스프링 컨테이너 생성
- 스프링 컨테이너 생성(AppConfig.class) - 스프링 컨테이너 생성시 구성정보 지정해주어야함.
- 스프링 빈 등록빈 이름(Key) : 빈 객체(value) 로 저장된다. (빈이름은 메서드명이 사용된다.)
- ex) memberService : MemberServiceImpl@0x1
- 파라미터로 넘어온 설정정보(AppConfig.class)를 사용해서 @Bean들을 다 등록함.
- 스프링 빈 의존관계 설정 - 준비
- 빈들이 스프링 컨테이너에 등록이 되었지만 서로 연결되진 않았다. = 의존관계 주입이 안되었음
- 스프링빈 의존관계 설정 - 완료동적 의존 관계 연결시켜줌.
- (설정 정보를 참고해서 의존관계가 주입되어짐.)
- 각 객체들이 생성되면서 의존관계가 주입되어진다.
** 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져있다.
위와같이 자바코드로 스프링 빈 등록시, 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.
@Bean//스프링 빈으로 등록
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
} // -> 호출되면서 memberservice와 리포지토리가 자동으로 연결되게 된다
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
} // 호출되면서 리포지토리, 할인정책이 불려오면서 같이 연결되게 된다.
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
이제 컨테이너에 잘 등록되었는지 확인 해보자.
컨테이너에 등록된 모든 빈 조회
package hello.core.beanfind;
import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ApplicationContextInfoTest {
//스프링 컨테이너 불러옴
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean(){
//스프링 빈 이름들을 String으로 빼냄
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " Object = " + bean );
}
}
//스프링에 등록된 모든 빈정보가 나옴 (스프링 내부에서 사용하는 빈까지 다 나옴)
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean(){
//스프링 빈 이름들을 String으로 빼냄
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
//BeanDefinition => 빈에대한 정보들, 정보들 가져옴
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//role_appliction과 role_infrastructure(내부에서 사용하는 빈)가 있음
//역할로 걸러서 출력함
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " Object = " + bean);
}
}
}
}
스프링 빈 조회 - 기본
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회방법은
ac.getBean(빈이름, 타입)
ac.getBean(타입) => 이름 생략가능
만약 조회 대상이 없을 경우엔 예외가 발생한다.
NoSuchBeanDefinitionException: No bean named "xxxxx" available
package hello.core.beanfind;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ApplicationContextBasicFindTest {
//스프링 컨테이너 가져옴
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName(){
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
//isInstanceOf => 어떤 클래스인가?
}
@Test
@DisplayName("이름 없이 빈 타입으로 조회")
void findBeanByType(){
//인터페이스로 조회하면 알아서 사용하는 구현체(스프링 빈에 등록된)가 조회된다.
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
//isInstanceOf => 어떤 클래스인가?
}
@Test
@DisplayName("구체 타입으로 조회")
void findByName2(){
//구현체에 의존하게 된다.....
MemberService memberService = ac.getBean(MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
//isInstanceOf => 어떤 클래스인가?
}
//조회가 안되는 경우도 테스트 해봐야해!!
@Test
@DisplayName("빈 이름으로 조회X")
void findByNameX(){
//MemberService memberService = ac.getBean("XXX", MemberService.class);
//조회가 안되면 NoSuchBeanDefinitionException 터친다.
//Junit Assertion의 assertThrows를 사용해서 예외를 확인 해야한다.
assertThrows(NoSuchBeanDefinitionException.class, ()->ac.getBean("XXX", MemberService.class));
}
}
**참고 assertThrows
org.junit.jupiter.api.Assertions.assertThrows(
NoSuchBeanDefinitionException.class, // 발생이 예상되는 예외의 타입
()-> ac.getBean("xxxx", MemberService.class) // 예외가 발생될 수 있는 코드 블록
);
/*
.
먼저 해당 테스트 메소드는 존재하지 않는 빈의 이름으로 빈을 가져오려고 할 때 예외가 발생되는 상황을 테스트 하기 위한 메소드입니다.
ac.getBean("xxxx", MemberService.class); 해당 문장을 실행하면 존재하지 않는 빈의 이름(xxxx)으로 빈을 꺼내오려고 할 것입니다. 그러나 당연히 xxxx라는 이름으로 등록된 빈이 없기 때문에 NoSuchBeanDefinitionException 예외가 발생합니다.
.
assertThrows 메소드는 발생이 예상되는 예외의 타입, 예외가 발생될 수 있는 코드 블록을 파라미터로 받아서 실행됩니다.
이 때, assertThrows 내부에서는 예외가 발생될 수 있는 코드 블록을 실행합니다. 만약 해당 코드 블록을 실행 중 예외가 발생한다면 발생된 예외가 발생이 예상되는 예외의 타입과 일치하는지 아닌지 확인합니다. 이때 발생된 예외 타입과 예상되는 예외의 타입이 일치하면 테스트는 성공으로 처리됩니다.
.
아래 코드는 assertThrows의 내부 동작입니다. try 블록 안에서 코드를 실행하여 예외 발생시 catch 내에서 발생한 예외 타입과 예상되는 예외 타입을 비교하고 있습니다.
*/
스프링 빈 조회 - 동일한 타입이 둘 이상
타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생!!!!
이때는 빈 이름을 지정하자
ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
package hello.core.beanfind;
import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
//타입 으로 조회시 같은 타입이 둘 이상이면...
// 테스트를 위해 간단한 설정 클래스 만듬
public class ApplicationContextSameBeanFindTest {
//테스트를 위한 sameBean 설정 정보를 가져옴
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상이면 중복오류 발생")
void findBeanTypeDuplicate(){
//먼저 어떤 예외를 던지는지 확인해본다.
//MemberRepository bean = ac.getBean(MemberRepository.class);
/*
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memberRepository1,memberRepository2
NoUniqueBeanDefinitionException를 던진다.
*/
Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
void findBeanByName(){
//설정클래스에 메소드 둘다 MemberRepository이다.따라서 이름을 같이 지정
MemberRepository bean = ac.getBean("memberRepository1", MemberRepository.class);
org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(MemberRepository.class);
}
//특정 타입 모두 조회 => 둘 다 꺼내고 싶음
//getBeansOfType 사용하면 된다.
//MemberRepository형 모두 꺼냄
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanType(){
//getBeansOfType은 Map형으로 반환함 <이름, 타입>
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key);
}
System.out.println("beansOfType = " + beansOfType);
org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
//해당 타입의 개수가 2개이므로 2개 다 꺼내졌는지 확인
}
//구성 정보이므로 어노테이션 붙임
//메서드 명은 다르지만, 반환하는 타입은 같음
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1(){
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2(){
return new MemoryMemberRepository();
}
}
}
스프링 빈 조회 - 상속관계
부모타입으로 조회하면 자식타입도 함께 조회된다.
예를 들어 Object타입으로 조회시 모든 스프링빈을 조회하게 된다.
테스트코드로 확인해보자
package hello.core.beanfind;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import java.util.Map;
public class ApplicationContextExtendsFindTest {
//스프링 컨테이너
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류")
void findBeanByParentTypeDuplicate(){
//DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
//->NoUniqueBeanDefinitionException
Assertions.assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름 지정해서 쓰면댐")
void findBeanByParentTypeBeanName(){
DiscountPolicy bean = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(FixDiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeansBySubType(){
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 다 조회")
void findBeansByParentType1(){
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
System.out.println("key + "+ key + "value = "+ beansOfType.get(key));
}
}
@Test
@DisplayName("부모 타입으로 다 조회-object")
void findBeansByParentType2(){
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key + "+ key + "value = "+ beansOfType.get(key));
}
}
//설정 클래스이므로
@Configuration
static class TestConfig {
//스프링 빈에 등록
@Bean
public DiscountPolicy rateDiscountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy(){
return new FixDiscountPolicy();
}
}
}
직접 getBean할 경우는 잘 없음!!
개발하면서 애플리케이션 컨텍스트에서 빈을 조회할 일이 거의 없음.
하지만 기본기능이고, 가끔 순수 자바애플리케이션에서 스프링컨테이너를 생성해서 쓸때 사용한다.
BeanFactory와 ApplicationContext
ApplicationContext, AnnotationConfig(구현클래스) ---> ApplicationContext(인터페이스) ---> BeanFactory(인터페이스)
BeanFactory
스프링 컨테이너의 최상위 인터페이스.
스프링 빈을 관리, 조회하는 역할 담당, 예시로 getBean을 제공해줌
이전 코드들에서 대부분 사용한 기능은 BeanFactory가 제공하는 기능임,
Then, 왜 BeanFactory가 아닌 ApplicationContext를 사용했을까??
ApplicationContext
BeanFactory 기능을 모두 상속받아서 제공한다.
차이점은, 애플리케이션 개발시, 빈은 관리하고 조회하는 기능이외에 수많은 기능들이 필요하다.
예를 들면 ApplicationContext가 구현하는 인터페이스들을 보면
MessageSource = 메시지 소스를 활용한 국제화기능(한국에서 들어오면 한국어, 외국에서 들어오면 영어)
EnvironmnetCapable = 환경변수이다. 로컬, 개발, 운영등 구분해서 처리 (예를 들면 로컬 개발환경, 테스트서버 개발환경, 실제운영환경 등등 사용하는 DB가 다르다면? 맞게 설정)
ApplicationEventPublisher = 애플리케이션 이벤트이다. 이벤트를 발행하고 구독하는 모델을 편리하게 지원
ResourceLoader = 편리한 리소스 조회. 파일, 클래스패스,외부 등에서 리소스를 편리하게 조회
일단 이런게 있다 정도만 알고 가자...
정리하면 ApplicationContext는 BeanFactory 기능들에 편리한 기능을 추가한것이다. 따라서 BeanFactory는 거의 잘 안쓴다.
다양한 설정 형식 지원 - 자바코드, xml
스프링 설정정보를 java파일 이외에 xml, groovy 등등을 사용할 수 있다.
왜냐면
ApplicationContext(인터페이스) 구현체들로 다음과 같은 것들이 있기 때문이다.
AnnotationConfig AppliactionContext => AppConfig.class
GeneriXml ApplicationContext => appConfig.xml
Xxx ApplicationContext => appConfig.xxx =>다양한 설정형식을 지원한다.
XML 설정 사용해보기
요즘 잘 안쓰긴하지만 xml을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 잇다.
GenericXmlApplicationContext를 사용해서 xml 설정 파일을 넘기면 된다.
package hello.core.xml;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class XmlAppContext {
@Test
void xmlAppContext(){
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
이제 appConfig.xml 파일 작성!
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="memberService" class="hello.core.member.MemberServiceImpl" >
<constructor-arg name="memberRepository" ref="memberRepository" /> //생성자, 참조
</bean>
<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />
<bean id="orderService" class="hello.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
<constructor-arg name="discountPolicy" ref = "discountPolicy" />
</bean>
<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
</beans>
AppConfig.java와 유사한것을 알 수 있다.
중요한 것은 스프링이 이렇게 다양한 설정 형식을 지원한다는 것이다.
과연 어떻게???
스프링 빈 설정 메타 정보 - BeanDefinition
BeanDefinition이라는 추상화가 있다.
위에서 말한것처럼, 역할과 구현을 개념적으로 나눈것!!!
BeanDefinition을 빈 설정 메타정보라 한다.
@Bean, 당 각각 하나씩 메타정보가 생성되고, 스프링컨테이너는 이 메타정로를 기반으로 스프링 빈을 생성한다.
- 스프링 컨테이너 ------> BeanDefinition <-- AppConfig.class, AppConfig.xml, AppConfig.xxx따라서 BeanDefinition에는 뭐가 들어가는지 모른다. 그냥 그 역할을 수행하면 된다.
- 스프링 컨테이너는 BeanDefinition 인터페이스에만 의존한다.
너무 깊게 이해할 필요는 없으니 필기한거 참고!
그냥 저렇게 추상화를 사용해서 다양한 설정형식을 지원한다고 알고 있으면된다.
package hello.core.beandefinition;
import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class BeanDefinitionTest {
//getBeanDefinition을 사용하기 위해서
//ApplicationContext 대신 사용
//getBeanDefinition은 왠만해선 쓸일 거의 없음 => 빈 설정 메타정보임
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 설정 메타 정보 확인")
void findApplicationBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
System.out.println("beanDefinition = " + beanDefinition + "beanDefinition =" +beanDefinition);
}
}
}
}
싱글톤 컨테이너
스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
대부분 스프링앱? => 웹 애플리케이션
웹 애플리케이션? => 대부분 여러 고객이 동시에 요청함
스프링이 없는 순수 DI 코드는 memberService를 요청할때 마다 객체를 각각 생성해서 새로 만들어주게된다.
코드로 보자.
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
@DisplayName("스프링이 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
//1. 조회
MemberService memberService1 = appConfig.memberService();
//2. 조회
MemberService memberService2 = appConfig.memberService();
//두 객체 비교
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
//Same => 참조값 비교
//Equal => 값 비교
}
}
매번 새로운 객체를 만드니까 메모리 낭비도 된다.
초당 만건의 요청이 들어오면 멤버 서비스 객체가 초당 만개가 생성된다....
따라서 딱 하나의 객체만 만들고 서로 공유하도록 설계하면 된다. => 이것이 싱클톤 패턴
- 싱글톤 패턴
클래스의 인스턴스가 딱 하나만 생성되도록 보장하는 디자인 패턴!
따라서 객체 인스턴스가 2개 이상 생성되지 못하게 막아야함!!!
How?? => private 생성자로 외부에서 객체를 생성하지 못하게 막아버림, 외부에서 new로 생성 불가!
싱글톤 패턴 예제
package hello.core.singleton;
public class SingletonService {
//How? => 생성자를 private로 막아버림
//먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
private static final SingletonService instance = new SingletonService();
//객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
public static SingletonService getInstance(){
return instance;
}
//이제 생성자를 막아보자 private로
private SingletonService(){
}
//로직 테스트 위해서
public void logic(){
System.out.println("싱글톤 객체 호출됨!!");
}
}
이제 테스트!!
package hello.core.singleton;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonService {
//How? => 생성자를 private로 막아버림
//먼저 static영역에 객체를 하나만 생성해서 놔둠 =>인스턴스 하나만 생성해둠 먼저
private static final SingletonService instance = new SingletonService();
//객체 인스턴스가 필요한 경우, getInstance 메서드로 접근할수잇게 해둠
public static SingletonService getInstance(){
return instance;
}
//이제 생성자를 막아보자 private로
private SingletonService(){
}
//로직 테스트 위해서
public void logic(){
System.out.println("싱글톤 객체 호출됨!!");
}
@Test
@DisplayName("싱글톤 패턴이 적용된 객체 사용")
public void singletonServiceTest(){
//new SingletonService() => private로 막혀버렷쥬?
//두번 호출 해본다.
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
//이제 테스트 무엇을? 참조값이 같은지
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
}
싱글톤 패턴을 구현하는 방법은 여러방법들이 있다. 여기선 객체를 미리 생성해두는 단순하고 안전한 방법을 썻다.
하지만 싱글톤은 여러 단점들이 있다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. => 접근 못하게 막고, 미리 올려두고 하는 과정이 생겨버림
- 의존 관계산 클라이언트가 구체 클래스에 의존 -> DIP위반 ==>SingletonService.getInstance();로 꺼내버림 구체클래스 꺼냄
- DIP위반햇으니 OCP 위반할 가능성 많아짐
- 테스트하기 어려움 => 유연하지가 않게됨
- 내부 속성을 변경하거나 초기화 하기 어렵움
- private생성자이므로 자식 클래스를 만들기 어려움
위 단점들때문에 안티패턴이라고도 불림
하지만.... 스프링을 쓰면 스프링이 알아서 해결해줌... how?
밑에서 보자.
싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤 즉, 1개만 생성해서 관리한다.
(이때 까지 햇던 스프링 빈이 싱글톤으로 관리 되었다.)
싱글톤 컨테이너
- 싱글톤 패턴을 적용안해도 스프링 컨테이너가 다 알아서 해줌 => 객체 인스턴스를 싱글톤으로 관리함
- 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 함
- 싱글톤 객체를 생성하고 관리하는 기능 => 싱글톤 레지스트리
- 따라서 스프링을 사용하면 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
- 싱글톤 패턴을 위한 지저분한 코드가 없어짐
- DIP, OCP, 테스트, private 생성자로부터 자유로워짐!! 호우!
스프링 컨테이너를 사용하는 테스트 코드를 보자
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SingletonTest {
@Test
@DisplayName("스프링이 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
//1. 조회
MemberService memberService1 = appConfig.memberService();
//2. 조회
MemberService memberService2 = appConfig.memberService();
//두 객체 비교
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
//Same => 참조값 비교
//Equal => 값 비교
}
//=> 스프링 컨테이너를 사용!!!
@Test
@DisplayName("스프링컨테이너와 싱글톤")
void springContainer(){
//스프링 컨테이너
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
//2. 조회
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//두 객체 비교
Assertions.assertThat(memberService1).isSameAs(memberService2);
//Same => 참조값 비교
//Equal => 값 비교
}
}
멤버 서비스 객체를 공유하게 됨!!
하지만 이러한 싱글톤 방식도 문제가 있음....ㅠ
싱글톤 방식의 주의점
객체 인스턴스를 하나만 생성해서 공유하므로, 객체는 상태를 유지하면 안됨!!!
(운영체제 시간의 동시성 문제를 생각해보자..)
즉, 무상태로 설계해야한다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다!!
- 즉, 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.!!!
- 따라서 가급적 읽기만 해야됨
- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreaLocal등을 사용해야한다.
상태를 유지해서 발생하는 문제점 - 예시
package hello.core.singleton;
public class StatefulService {
private int price;// 상태를 유지함, 즉, 값을 가지고 있음
public void order(String name, int price){
System.out.println("name = " + name + "price = " + price);
this.price = price; // 값이 입력되버린다!!!!!!!!
}
public int getPrice(){
return price;
}
}
package hello.core.singleton;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
//스프링 컨테이너
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
//각각 price 입력해버림 = TheadA
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
statefulService1.order("userA", 10000);
//각각 price 입력해버림 = ThreadB
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
statefulService1.order("userB", 20000);
int price = statefulService1.getPrice();
//뒤에서 값이 바뀌니까 같지 않을 것이다.
Assertions.assertThat(price).isEqualTo(20000);
}
static class TestConfig{
//빈 등록
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
각각 Thead 2개라 가정(Order 하는 경우)
price필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경해버리게 된다.
싱글톤은 이런 문제가있다....
해결법으론 private int price를 사용하지 않고, int order로 price를 반환하게 해버린다.
즉 price를 지역 변수로 만들어버린다.
package hello.core.singleton;
public class StatefulService {
//private int price;// 상태를 유지함, 즉, 값을 가지고 있음
public int order(String name, int price){
System.out.println("name = " + name + "price = " + price);
return price; //
}
/*
public int getPrice(){
return price;
}
*/
}
package hello.core.singleton;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
//스프링 컨테이너
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
//각각 price 입력해버림 = TheadA
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
int userAPrice = statefulService1.order("userA", 10000);
//각각 price 입력해버림 = ThreadB
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
int userBPrice =statefulService1.order("userB", 20000);
//int price = statefulService1.getPrice();
//뒤에서 값이 바뀌니까 같지 않을 것이다.
Assertions.assertThat(userAPrice).isEqualTo(10000);
}
static class TestConfig{
//빈 등록
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
@Configuration과 싱글톤
AppConfig를 다시보자..
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;
//의존관계 주입!
@Configuration // 설정을 구성한다고 알려줌
public class AppConfig {
@Bean//스프링 빈으로 등록
public MemberService memberService(){
return new MemberServiceImpl(memberRepository()); //memberRepository 호출 => 생성
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());//memberRepository호출 => 생성
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
//과연 memberRepository => new MemoryMemberRepository();가 두번이나 되었을까??
//?? 모를땐 Test코드로 assertThat Same으로 참조값을 비교해보자...
과연 new MemoryMemberRepository();를 두번하여 각각 다른 객체 인스턴스가 만들어질까?
따라서 싱글톤이 깨질까?
결과적으로 말하면 서로 같은 객체 인스턴스이고, 호출도 한번만 되어진다.
??????? => 마! 이게 스프링이다....
@Configuration과 바이트코드 조작의 마법
스프링 컨테이너 = 싱글톤 레지스터
따라서 스프링 빈이 싱글톤이 되도록 보장해주어야한다.
하지만, 스프링이 자바코드까지 어떻게 하기는 어렵다.
자바코드상으론 분명 3번 호출되어야된다!!
어떻게 이걸 해결했을까???
바로 스프링이 클래스의 바이트코드를 조작하는 라이브러릴르 사용했다.
모든 비밀은 @Configuration을 적용한 AppConfig에 있다.
@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig또한 스프링 빈으로 등록이 되어진다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean);
}
결과는......
순수한 클래스일 경우 bean = hello.core.AppConfig와 같이 출력되어야횐다.
"bean = hello.core.AppConfig$$EnhancerBySpringCGLIB$$4ab6406e@7e276594"
??
바로 CGLIB라는 바이트 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임이의 다른 클래스를 만들고
그 클래스를 스프링 빈으로 등록했다.
AppConfig ->등록X
AppConfig 를 상속받은 클래스(스프링이 조작해둠) -> 등록
즉, 그 다른 임의의 클래스가 싱글톤을 보장해준다.
아마도..
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean이 붙은 메서드이면 스프링 빈으로 존재하면 그 빈을 반환하고, 존재하지 않으면 그때 생성해서 반환하는 코드가 동적으로 만들어진다.
여기서,, 그러면 @Configuration을 적용하지 않고 @Bean만 적용하면?
=> 싱글톤이 보장 안되어짐...
따라서 memoryMemberRepository가 여러번 호출되고 각 각 다 다른 인스턴스가 만들어진다.
즉, 결론은 스프링을 사용하면 다 해결된다.
따라서 구성영역이면 @Configuration을 사용하자.
컴포넌트 스캔
컴포넌트 스캔과 의존관계 자동주입 시작하기
이때까진 @Bean이나 을 사용하여서 설정정보에 직접 사용할 빈을 지정해줬다.
만약.. 등록해야되는 스프링빈이 수백개라면? ->설정정보도 커지고, 일일이 다 등록하기 귀찮고, 누락하는 문제도 발생될 수있다.
따라서 스프링을 사용하면 해결된다.
스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공!
그러면 설정정보가 없는데 의존관계는 어떻게 주입할까??...
바로 @Autowired로 의관관계를 자동으로 주입해준다.
기존 AppConfig.java는 그대로 두고 AutoAppConfig.java를 새로만든다.
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
//설정정보니까
@Configuration
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
//아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
}
자 이제 다시보면 (하다보니 주석에 다적어버려서 그냥 그대로 올려봣다.)
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
//설정정보니까
@Configuration
//컴포넌트 스캔을 사용!!
//기존 AppConfig도 컴포넌트 스캔대상이므로 스프링 컨테이너에 등록되지 않게 해주어야한다.
//제외할 필터로 어노테이션 타입의 Configuration이 있는 클래스를 컴포넌트 스캔하지 않게 한다.
//왜냐면 AppConfig는 @Configuration 가 붙어있으니까 이거 빼줄라고
//그리고 AppConfig는 수동으로 빈을 등록하는거니까 자동 등록 테스트를 위해 빈으로 등록하지 않는다.
@ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
//아무 내용이 없어도 알아서 다 긁어서 스프링빈으로 찾아낸다.
//어떻게? => 위를 보면 일단 컴포넌트 스캔을 사용한다고 적었다.
//그러면 컴포넌트들을 찾아야지! 어떻게? => @Component를 붙여서 알려주면됨!
//빈으로 등록할 객체들을 컴포넌트로 등록!!
//빈에는 구현체들이 등록이 되어있어야겟지? 그러면 구현체들에 @Component
//그러면 스프링 컨테이너에 빈으로 등록은 되었다만... 어떻게 의존관계를 연결시키지... 하아..
//바로 @Autowired 로 자동연결해주면 끄으읕 => 구현체에서 필요한 객체 인스턴스를 가져올 곳에 쓰면댐
}
/**
* excludeFilters에 대해 다시 설명하면
* @Configuration 어노테이션을 보면 @Component를 사용한다.!
* 따라서 @Configuration이 있는 것도 다 긁어온다.
* 그러므로 앞의 예제들에서 만든 AppConfig, TestConfig 등등 다 긁어와버리니까 제외시켜버렷다.
*
* (내 생각이긴한데 컴포넌트 스캔을 사용하는 설정파일은 AutoAppConfig뿐이니까
* 자기 자신이 설정파일이지만 자기 자신을 제외한 나머지 설정들을 컴포넌트 스캔시 등록 안해버리는거같다.
* 나중에 검색해봐야지...)
*/
즉, 컴포너트 스캔을 한다고 선언을 했다. 그러면 뭘 해야지??
컴포넌트 스캔이 찾을 수 있게 @Component라고 알려줘야댐
어디에?? => 스프링 빈으로 등록할 객체를!
어떤거지? => 실제 사용될 것들이니 실제 사용할 구현체이지
그러면 필요한 객체들을 @Component 을 붙여서 스프링 빈으로 등록을 했다.
킹치만.. 설정정보를 다시보면 아무내용도 없다. 그러면 의존관계 주입은 누가하냐???
@Autowired로 의존관계를 주입힌다.
어떻게? 해당 구현체가 필요한 곳에서!
다시 역할, 구현을 구분한다는거에 집중하자 => 배우(자기 역할)는 연기만하면 되지 상대배우(구현)가 누가될지는 몰라도 자기 역할만 하면된다.!
즉, 어떤 리포지토리가 쓰일진 모르지만, 그냥 리포지토리를 불러오면 된다! => 따라서 이러한 곳에 @Autowired로 의존관계 주입 !!
자 다시 정리하면, 구현체 들이 스프링빈으로 등록되어야하니
구현체(xxxIpml)들에 @Component!
그리고 구현체들이 다른 구현체가 필요할때 어떤 구현체가 올진 모르겟고 해당 역할만 필요로함! => @Autowired
package hello.core.member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component //컴포넌트 스캔하여 빈으로 등록하기 위해서 알려줌
//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{
//배역 배우로 생각할때
//MemberRepository memberRepository = new MemoryMemberRepository();
//생성자로 맞는 구현체 가져옴
private final MemberRepository memberRepository;
//리포지토리가 필요하네? 근데 어떤 저장소가 올진 내가 신경쓸게 아니지!
//따라서 의존관계 주입!
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
package hello.core.member;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component//빈으로 등록하기 위해 컴포넌트라고 알려줌
//저장소 필요
public class MemoryMemberRepository implements MemberRepository{
//일단 임시 저장소 = 메모리 사용
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(),member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);//store는 Map이므로 키값으로 member찾음
}
}
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
//할인율
private int discountPrice = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price*discountPrice/100;
}
else{
return 0;
}
}
}
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component //빈 등록
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
//저장소 조회
//private final MemberRepository memberRepository = new MemoryMemberRepository();
//할인을 위해서 할인 정책 필요
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//private final DiscountPolicy discountPolicy= new RateDiscountPolicy();
//생성자 주입기법으로 맞는 구현체 가져옴
private final MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//역할, 구현 구분! 배우는 누가 캐스팅될지 모르고 걍 연기만 하면댐
//여기선 어떤 리포지토리, 어떤 할인정책이 올지 모름
//그냥 리포지토리를 쓰고 할인정책을 쓰면댐!!!
//따라서 의존관계가 주입되어야되므로 @Autowired
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//id로 조회해서 vip 이면 할인정책 적용
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
자 이러면 이제 뭘해봐야지? => 테스트로 확인
테스트를 해보아서 진짜 다 스프링 빈으로 컨테이너에 등록이 되고 의존관계주입 즉 연결되었는지 확인 해보자!
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AutoAppConfigTest {
@Test
@DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
void basicScan(){
//스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
//설정정보로 스프링컨테이너 불러오고
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
//요렇게하면 안된다. 왜냐면 안알랴줌... 뒤에서 설명해줄게...
// MemberService memberService = ac.getBean("memberService", MemberService.class);
//타입으로 찾음
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
위 주석에서 안알랴줌 이유를 설명하자면..
먼저 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 봐야된다.
@Component 를 붙이면 이 어노테이션이 붙은 클래스들을 스프링 컨테이너에 스프링 빈으로 등록시킨다.
(스프링 컨테이너는 스프링 빈을 생성해서 가지고 있음)
등록 될땐, 자바빈 규약에 의해 xxxBean 와 같은 형식으로 빈이름이 지정된다.
(스프링 빈의 기본이름은 크래스 명을 사용하되, 맨 앞글자 소문자를 사용함)
예를 들면 @Component가 붙은 MemberServiceImpl을 보면 memberServiceImpl과 같이 빈이름이 설정된다.
자동 의존관계 주입은 @Autowired를 설정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
(즉 역할만 알고 있으면댐 구현체가 뭔진 몰라도 알아서 가져다줌)
이때 해당 스프링 빈을 찾는 방식은 타입이 같은 빈을 찾아서 주입한다.
앞서 getBean(MemberRepositroy.class)를 생각해보자 이러면 구현 객체인 MemoryMemberRepository가 나왔다.
따라서 MemberRepositroy가 필요하면 스프링 컨테이너에서 MemberRepositroy타입으로 등록된 빈을 찾아서 주입해준다.
킹치만 같은 타입이 여러개라면?!?!?!?! => 충돌이 일어나겟지... 뒤에서 설명한다.
탐색위치와 기본 스캔 대상
컴포넌트 스캔시 스캔할 탐색 시작 위치는 어디일까?
만약 모든 파일을 다 찾는다고 생각해보자.
그 모든 불러온 라이브러리들까지 다 뒤져본다고 생각하면 시간이 엄청 오래 걸릴 것이다.
따라서 꼭 필요한 위치부터 탐색하도록
시작위치를 지정할 수 있다.
@ComponentScan( basePackage = {"hello.core","hello.service"} )
이런식으로 해당 패키지를 포함한 하위패키지를 모두 스캔하게 설정해 줄 수 있다.
혹은 basePackage외에
basePackageClasses로 "hello.core.member" 와 같이 클래스를 지정해줄 수 있다.
그러면 member클래스의 패키지인 hello.core가 탐색 시작 위치가 된다.
기본값은 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작위치가 된다.
근데 위에껀 그렇다고 치고,
왠만하면 기본값을 따르자 => HOW??
설정 정보 클래스를 프로젝트 최상단에 두면 자동으로 그 밑을 다 스캔하게 된다.
(스프링 부트도 이러한 방법을 기본으로 제공한다.)
예를 들면 com.hello 위치에 설정 정보 클래스를 넣어두는거지
=>패키지 시작위치를 설정할 번거로움이 없어지쥬?
*참고로 스프링 부트를 사용하면 스프링 부트 대표 시작정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다.(이 설정안에 @ComponentScan이 들어 있기 때문이지)
**참고의 참고로 스프링 프로젝트 생성시 CoreApplication이 자동으로 생성된다. => 얘가 바로 스프링 부트를 실행한다. 스프링부트를 사용하면 @ComponentScan을 사용할 필요가 없어진다. 왜냐? => 바로 위 참고에서 @SpringBootApplication에 @ComponentScan이 들어있다고 말했음...
컴포넌트 스캔 기본 대상
컴포넌트 스캔은 @Component 외에도
- @Controller : 스프링 MVC 컨트롤러에서 사용 MVC컨트롤러로 인식
- @Service : 스프링 비지니스 로직에서 사용 (개발자가 보기 비지니스 흐름 보기 편하게 적어둠)
- @Repository : 스프링 데이터 접근 계층에서 사용
- 예를 들어 DB가 설정 안된경우 어떤 DB가 사용될지 모르니... 쿼리도 다 달라질거다... 이걸 중간에서 번역해준다고 생각
- @Configuration : 스프링 설정정보 (위에서 썻던거)
해당 어노테이션 클래스를 보면 전부 @Component가 들어가있다.
@Component
public @interface Controller {
}
@Component
public @interface Service {
}
@Component
public @interface Configuration {
}
사실 어노테이션에는 상속 기능이 없다...
이렇게 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은.. 바로 자바!..가 아니라
스프링이 지원하는 기능이다.
컴포넌트 스캔 이외에도 밑과 같은 부가기능을 수행한다.
- @Controller : 스프링 MVC 컨트롤러로 인식
- @Service : 특별한 처리를 하지 않는다. 개발자들이 핵심 비지니스 로직이 여기 있겠구나라고 비지니스 계층 인식에 도움준다.
- @Repository : 스프링 데이터 접근 계층으로 인식하고,데이터 계층의 예외를 스프링 예외로 변횐해준다.
- 예를 들어 DB가 설정 안된경우 어떤 DB가 사용될지 모르니... 쿼리도 다 달라질거다... 이걸 중간에서 번역해준다고 생각 => 추상화해서 반환
- @Configuration : 스프링 설정정보로 인식 , 스프링 빈이 싱그톤을 유지하도록 추가처리
** 참고로 userDefaultFilters옵션은 기본으로 켜져있는데 이 옵션을 끄면 기본 스캔 대상들이 제외된다. => 이런것도 잇구나~~
필터
두가지가 있다.
- includeFilters : 컴포넌트 대상을 추가로 지정
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정
예제로 확인 해보면
두 어노테이션을 만들어준다.
package hello.core.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)//어떤거에 적용? TYPE에 적용가능한 어노테이션이다~~ 즉, class 레벨에 적용하는 어노테이션
@Retention(RetentionPolicy.RUNTIME)//런타임 - 실행시에 이 어노테이션을 참조한다.
@Documented //JavaDoc 생성시 Annotation에 대한 정보도 함께 생성한다.
//어노테이션 선언
public @interface MyExcludeComponent {
}
package hello.core.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)//어떤거에 적용? TYPE에 적용가능한 어노테이션이다~~ 즉, class 레벨에 적용하는 어노테이션
@Retention(RetentionPolicy.RUNTIME)//런타임 - 실행시에 이 어노테이션을 참조한다.
@Documented //JavaDoc 생성시 Annotation에 대한 정보도 함께 생성한다.
//어노테이션 선언
public @interface MyIncludeComponent {
}
그리고 클래스 빈A, B를 만들어서 위에서 만든 어노테이션을 적용한다.
package hello.core.filter;
@MyIncludeComponent
public class BeanA {
}
package hello.core.filter;
@MyExcludeComponent
public class BeanB {
}
이제 테스트 해본다.
package hello.core.filter;
import hello.core.AppConfig;
import hello.core.AutoAppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;
public class ComponentFilterAppConfigTest {
@Test
void filterScan(){
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
Assertions.assertThat(beanA).isNotNull();
org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class, ()->ac.getBean("beanB", BeanB.class));
}
@Configuration //설정 정보라고 말해줌
//스프링 빈으로 등록할거 등록하지 않을거 설정
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class))
static class ComponentFilterAppConfig{
}
}
FilterType은 5가지 옵션이 있다.
- ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
- ex) org.example.SomeAnnotation
- ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
- ex) org.example.SomeClass
- ASPECTJ: AspectJ 패턴 사용
- ex) org.example..*Service+
- REGEX: 정규 표현식 ex) org.example.Default. CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
- ex) org.example.MyTypeFilter
예로 TYPE 즉 클래스로 제외할 수도 있따.
BeanA를 제외해보겟다.
@Configuration //설정 정보라고 말해줌
//스프링 빈으로 등록할거 등록하지 않을거 설정
@ComponentScan(
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)})
static class ComponentFilterAppConfig{
}
ANNOTATION외에는 잘 사용하지 않는다...
그리고 includeFilters도 잘 사용하지 않는다.
왜냐하면 @Component면 충분하기 때문이다.
** 최근 스프링 부트는 컴포넌트 스캐을 기본으로 제공한다. 따라서 개인적으로 옵션 변경보단 기본설정에 맞추어 사용하는 것이 편하다...
중복등록과 충돌
만약 컴포넌트 스캔에서 같은 빈 이름을 등록하면??
- 자동 빈 등록 vs 자동 빈 등록
- 수동 빈 등록 vs 자동 빈 등록
자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해서 자동으로 스프링 빈이 등록된다.
예를 들어 @Component("BeanA"), @Component("BeanA")와 같이 이름이 같은 빈을 등록하려고 하면
스프링은 오류를 발생시킨다. => ConfilctingBeanDefinitionException 발생
수동 빈 등록 vs 자동 빈 등록
@Component
public class MemoryMemberRepository implements MemberRepository {}
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class)
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
위와 같이 @Component로 자동으로 등록하고 @Bean으로 같은 이름을 수동으로 등록하는 경우
수동 빈이 우선권을 가진다 => 수동 빈이 자동 빈을 오버라이딩을 해버린다.
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing
그리고 위와 같은 로그가 남는다.
개발자가 의도적으로 위와같이 설정한다면 자동등록보단 수동등록한게 당연히 우선권을 가지는것이 좋다.
하지만... 이렇게하면 정말 잡기 어려운 버그가 만들어진다.
그래서 최근 스프링 부트(CoreApllication)에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.
의존관계 자동주입
의존관계 주입은 크게 4가지 방법이 있다.
- 생성자 주입
- 수정자 주입(setter주입)
- 필드 주입
- 일반 메서드 주입
생성자 주입
말그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
(위에서 했던 방식들)
생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. (그 다음부턴 호출 안되게 막을 수 있다.)
불변, 필수 의존관계에 사용한다. => OCP 지킴
@Component//OrderServiceImpl가 스프링 빈에 등록됨 => 객체 인스턴스가 생성되니까 생성자도 호출!
public class OrderServiceImpl implements OrderService {
//private final로 선언됨 => 무조건 값을 세팅해줘!!! 라고 알리는 격임
//따라서 무조건 값이 있어야댐!!! 아니면 null임
//외부에서 값 변경이 불가! => setter, getter가 없음
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
//생성자가 여기 있음!
//Autowired이므로 스프링 컨테이너에서 스프링 빈을 꺼내서(리포지토리, 할인정책)을 주입시켜줌
// 그리고 왠만하면 생성자에 있는 파라미터들을 다 넣어주는게 좋음! 아니 그래야댐
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자 주입은 빈을 등록하면서 같이 일어난다.
-> 빈을 등록해야하니까!
빈을 등록?? => 객체 인스턴스를 생성해서 스프링 컨테이너가 들고 있음!!
객체 인스턴스 생성?? => 객체를 생성해야하니까 생성자 호출!!
생성자가 하나인 경우는 Autowired 생략 가능하다. 물론 스프링 빈에만 해당댐
(@Component 햇으니까 스프링 빈임)
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// @Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
수정자 주입(setter 주입)
setter라 불리는 수정자 메서드로 의존관계를 주입함!
선택, 변경 가능성이 있는 의존관계에 사용
자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
//final이 아님 => 값 변경 되어도 됨
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//생성자로 주입 받는것이 아니라 setter 메서드로 주입받음
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
** @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.
=> 예를 들어 MemberRepository 쓸지 안쓸지 잘 모름....
@Component
public class OrderServiceImpl implements OrderService {
//final이 아님 => 값 변경 되어도 됨
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//쓸지 안쓸지 몰라서 false로 해놓음, 따라서 주입할 대상이 없어도 오류가 안남
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
** 자바빈 프로퍼티, 자바에서는 과거부터 필드의 값을 직접 변경하지 않고 setXxx, getXxx라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다.
필드 주입
말그대로 필드에 주입!
하지만 안티패턴이다...
코드가 간결하지만, 외부에서 변경이 불간으해서 테스트하기 힘들다는 단점이있다.
DI 프레임워크가 없으면 아무것도 할 수 없다. => 순수 자바코드로는 못한다.
즉, 사용하지 말자!!!!!!!!
주로 실제코드와 관계없는 테스트코드나, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 쓴다.
@Component
public class OrderServiceImpl implements OrderService {
//=> 값을 집어 넣을 수 없다... 어케넣어 setter도 없고 일반 메서드도 없고, 생성자로도 못넣고..
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
//값을 못 집어넣어버림
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Test
void fieldInjectionTest(){
//이렇게 new 임의로 생성한 객체는 Autowired 되지 않음
//Autowired는 스프링 빈에 등록된 객체를 연결하는것이므로
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder( ~~~ )
//할인 정책과 멤버리포지토리를 못 넘겨줌....
//set 메소드를 만들어서 넣어 주거나 해야댐
//orderService.setRepository(new MemoryMemberRepositroy) 처럼
}
==> 필드 인젝션은 결국 값을 넣으려면 setter가 필요하다.
즉, @Autowired로 땡겨온다곤 햇는데 넣어 주질 않으니 어케 그 값에 넣으란 말이냐....... 결국 setter와 같은게 필요한거지..
그럼 왜 필드 인젝션 씀? 그러므로 쓰지말자..
** 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다 => @Autowired는 스프링 빈에 등록된 걸 가져오는건데 스프링 컨테이너가 없는데 어케 가져와?!?! , 스프링 컨테이너 = ApplicationContext
예시)
@Test
@DisplayName("컴포넌트 스캔으로 빈이 등록되었는지 확인, 의존관계도 확인")
void basicScan(){
//스프링 컨테이너에 빈들이 등록되었는지 확인 해보면된다.
//설정정보로 스프링컨테이너 불러오고
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
// MemberService memberService = ac.getBean("memberService", MemberService.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
** @Bean을 사용하는 경우에, 파라미터에 의존관계는 자동 주입된다.
수동 등록시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있다.
@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
discountPolicy) {
new OrderServiceImpl(memberRepository, discountPolicy)
}
일반 메서드 주입
일반 메서드를 통해서 주입받을 수 있다. (사실상 수정자 주입이랑 같다..)
한번에 여러 필드를 주입 받을 수 있고 일반적으로 잘 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//일반 메서드 init를 통해서 주입 받음
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
** 의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
스프링 빈이 아닌 Member 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.
(스프링 컨테이너에 빈으로 등록되어 있어야지 가져오지, 등록이 안되었는데 어케 가져와?!)
옵션처리
주입할 스프링 빈이 없어도 동작해야할 때가 있다.
예를 들어 해당 스프링 빈이 있으면 실행하고, 없으면 해당 로직이 동작하지 않게 하는 때와 같이..
@Autowired만 사용하면 required 옵션의 기본값이 ture로 되어있어, 무조건 자동주입받을 빈이 있어야된다고 설정이되버린다. 따라서 자동 주입대상이 없으면 오류가 발생한다.
- @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다.
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입련된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다. 즉, null로 반환이 되는것을 막는다.
예제로 보자.
스프링 빈으로 등록되지 않은 Member 객체를 가져오도록 했다.
package hello.core.autowired;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;
import java.util.Optional;
public class AutowiredTest {
@Test
void AutoWiredOption() {
//설정정보도 스프링 빈으로 등록된다.
//스프링 컨테이너불러옴
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean{
//호출되지 않는다. 해당 객체가 스프링 빈으로 등록되어있지 않으면 가져오지 않으므로 호출 자체가 안됨.
@Autowired(required = false)
//여기서 Member는 앞에서 만든 hello.core의 멤버로 스프링 빈으로 등록되어있지 않다.
public void setNoBean1(Member member){
System.out.println("setNoBean1 = " + member);
}
//호출은 되나 null이 반환된다.
@Autowired //@Nullable 자동 주입할 대상이 없으므로 null
public void setNoBean2(@Nullable Member member){
System.out.println("setNoBean2 = " + member);
}
//Optional로 감싸서 반환 => Optional.empty 반환
@Autowired
public void setNoBean2(Optional<Member> member){
System.out.println("setNoBean2 = " + member);
}
}
}
member 객체 인스턴스를 가져와야되는데 스프링빈에 등록되어있지않으므로 null이 반환된다.
출력 결과
setNoBean2 = null
setNoBean3 = Optional.empty
setNoBean1은 required가 false이다. 따라서 스프링 빈으로 등록된 멤버가 없으므로 해당 메서드가 호출되어지지 않는다.
@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다.
예를 들면 생성자 자동 주입에서 특정 필드에만 사용해도된다.
public OrderServiceImpl(MemberRepository memberRepository, @Nullable DiscountPolicy discountPolicy) {...}
과 같이 마지막 파라미터는 없어도 생성자를 생성할 수 있게 할 수 있다.
(없으면 넣지 않아도 생성자가 생성댐)
왠만하면 생성자 주입을 쓰자
이유는 다음과 같다.
불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다!(공연을 하기 전에 배역이 다 정해져야댐! )
- (대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다!)
- 수정자 주입을 사용하면 setXxx 메서드를 public하게 열어둬야한다.
- 따라서 누가 실수로 바꿀수도 있다. 변경하면 안되는 메서드는 public으로 열어두지말자!
- 생성자 주입은 객체를 생성할 때 딱1번만 호출되므로, 이후에 호출되지 않는다. => 불변!
누락
프레임워크 없이 순수 자바코드 단위 테스트를 하는 경우에
예를 들어 OrderServiceImpl 을 순수한 자바 코드로 테스트 해보고 싶을때
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component //빈 등록
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
//저장소 조회
//private final MemberRepository memberRepository = new MemoryMemberRepository();
//할인을 위해서 할인 정책 필요
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//private final DiscountPolicy discountPolicy= new RateDiscountPolicy();
//생성자 주입기법으로 맞는 구현체 가져옴
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//생성자 대신에 set으로 주입 => 수정자 의존관계
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
//역할, 구현 구분! 배우는 누가 캐스팅될지 모르고 걍 연기만 하면댐
//여기선 어떤 리포지토리, 어떤 할인정책이 올지 모름
//그냥 리포지토리를 쓰고 할인정책을 쓰면댐!!!
//따라서 의존관계가 주입되어야되므로 @Autowired
// @Autowired
// public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// this.memberRepository = memberRepository;
// this.discountPolicy = discountPolicy;
// }
//id로 조회해서 vip 이면 할인정책 적용
//주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
테스트
package hello.core.order;
import org.junit.jupiter.api.Test;
import org.mockito.internal.matchers.Or;
import static org.junit.jupiter.api.Assertions.*;
//순수한 자바로 테스트
//OrderServiceImpl을 잘 만들엇는지 테스트
class OrderServiceImplTest {
@Test
void createOrder(){
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
단순 테스트를 위해서 잠깐 Appconfig설정을 바꾸자
@Bean
//주문 서비스는 저장소와 할인정책이 필요하다.
public OrderService orderService(){
// return new OrderServiceImpl(memberRepository(), discountPolicy());
return null;
}
실행결과
java.lang.NullPointerException
at hello.core.order.OrderServiceImpl.createOrder(OrderServiceImpl.java:55)
NullPointerException 에러가 난다.
왜냐하면 createOrder메서드를 다시 보자
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);//회원 가져와서
int discountPrice = discountPolicy.discount(member, itemPrice); //할인금액 적용
return new Order(memberId, itemName, itemPrice, discountPrice);
}
memberRepository와 discountPolicy가 필요하다.
가짜로 임의로 만들어서 넣어주더라도 뭔가 넣어 주어야된다.
=> 테스트는 누락을 해버렷음
다시 정리하면, OrderServiceImpl을 잘 만들었는지 확인 테스트하고 싶음.
하지만 OrderServiceImpl에서는 리포지토리나 할인정책이 필요함.
따라서 더미 리포지토리나 할인정책을 만들어서 즉, 아무 거나 넣으면댐 그냥(테스트용이니)
위에서 OrderServiceImpl을 생성자 주입에서 set 즉, 수정자 주입으로 변경하였음.
테스트 코드를 다시 보면 "new OrderServiceImpl();"
순수 자바코드이므로 컨테이너에서 객체를 받아오는게 아니라 직접 객체를 생성해서 사용함.
그리고 set으로 리포지토리와 할인정책을 받아서 설정해주어야댐.
하지만 set으로 받질 않았으니 아무런 객체 인스턴스가 안들어있음.
여기서 createOrder을 해버리니 리포지토리와 할인 정책이 null 인 상황에서 가져오려하므로 NPE가 발생함.
why?? 위 테스트는 순수 자바코드 테스트임!
그런데 위에선 @Autowired로 수정자 주입(set)으로 의존관계를 주입해놨었음.
근데 DI 컨테이너를 안띄었으므로 가져올 리포지토리, 할인 인스턴스가 없음.
즉, 의존관계가 주입 안되어있음!
다시 테스트 코드를 보면
@Test
void createOrder(){
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
생성자 메서드가 아니라 수정자 메서드로 의존관계를 주입 하였으므로 의존관계가 잘 안보이게 된다....
그리고 분명 의존관계가 빠졌지만 컴파일은 잘 되고 실행단계에서 오류가 난다.
하지만 생성자 주입을 사용하면..
//OrderServiceImpl을 잘 만들엇는지 테스트
class OrderServiceImplTest {
@Test
void createOrder(){
//생성자 단계에서 주입 데이터가 누락이 되어서 컴파일 자체가 안되는 컴파일 오류가 발생함.
// OrderServiceImpl orderService = new OrderServiceImpl();
OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
orderService.createOrder(1L, "itemA", 10000);
}
}
주입 데이터가 누락되었다고 오류가 뜬다.
세상에서 가장 좋은 오류는 컴파일 오류이다.. 바로 고치면댐!!
final 키워드
생성자 주입 방법의 또 다른 장점은 필드에 final 키워드를 사용할 수 있다는 것이다.
(setter와 같은 경우는 객체가 생성된 다음에 주입이 되므로 final을 쓰지 못한다.)
따라서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
@Component
public class OrderServiceImpl implements OrderService {
//final로 선언되어있다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
}
//...
}
해당 코드의 생성자를 보면 discountPolicy 주입이 누락되어있다.
따라서 자바는 컴파일 시점에 "java: variable discountPolicy might not have been initialized"오류를 발생시킨다.
컴파일 오류는 세상에서 가장 빠르고 좋은 오류다!!!!!!
참고)
final 키워드는 간단히 말하면 멤버 변수를 '상수'(const)로 만들겠다는 뜻입니다. 변수의 경우는 그렇고 메서드에 붙는 final은 재정의가 불가능, 클래스에 붙는 final은 상속 불가능 등으로 의미가 달라집니다.
.
상수는 한번 값을 할당하면, 다시 그 값을 변경할 수 없음을 말합니다. 그래서 자바에서는 상수는 선언과 동시에 값을 할당하도록 제한하고 있습니다.
private final int score = 0;
.
그러나 선언과 동시에 값을 할당하지 않아도 되는 예외가 있는데, 생성자에서 상수를 초기화 할 때 입니다. 생성자는 객체를 생성하기 위해 '반드시' 거쳐야 하는 과정이고, 여기서 상수값을 초기화를 하고 있다면 이는 '확실히 상수가 초기화 됨을 보장'합니다. 그래서 이런 경우는 컴파일을 허가 합니다.
private final int score;
public SomeClass(int score) {
this.score = score;
}
.
setter로 멤버 변수를 설정한다는 것은 객체가 생성되는 과정이 모두 끝난 이후에 setter 메서드를 호출하여 멤버 변수의 값을 할당하겠다는 뜻입니다. 만약 이 객체의 멤버 변수중 상수가 있을경우, 상수가 생성은 되었으나 언제 초기화 될지를 컴파일러가 알 수 없습니다. 그래서 이런 경우 final 키워드를 쓰지 못하도록 컴파일러가 막습니다.
.
자바는 개발자가 실수 할 수 있는 여지를 최대한 컴파일러 레벨에서 방지합니다. 그래서 생성한 객체를 해제하는 역할도 자바언어가 알아서 처리합니다. 과거 C나 C++ 언어에서는 이런 과정을 개발자가 전부 통제해야 하기에, 실수를 하게 되면 원인을 찾기가 더 힘든편입니다.
정리하면,
- 생성자 주입 방법을 사용하면 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살릴 수 있는 방법이다.
- 기본으로 생성자 주입 방법을 사용하고, 필수 값이 아닌 경우엔 수정자 주입 방식으로 옵션을 부여하자. 따라서 둘다 섞어서 사용하면된다.
- 항상 생성자 주입을 선택!!! 가끔 옵션이 필요한 경우에 수정자 주입!, 필드주입은 그냥 없다고 생각!
롬복과 최신 트렌드
막상 개발을 해보면, 대부분이 다 불변이다. 따라서 생성자에 final 키워드를 사용하게 된다.
근데 위에서 봣듯이,
- 생성자를 만든다
- 주입 받은 값을 대입하는 코드를 만든다
귀찮쥬??
필드 주입처럼 편한 건 없을려나... (필드 주입하면 생성자 없이 바로 들어오니까)
아래 코드를 최적화 해보자.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
롬복 라이브러리를 적용하면 된다.
롬복 라이브러리가 제공하는 @RequireArgsConstructor 기능을 사용하면 final 이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
즉, 생성자를 자동으로 만들어준다.(final이 붙은 필드들을)
start.io.spring으로 프로젝트 생성시 add dependecy로 롬복을 선택하면 자동 적용된다.
직접 사용하는 방법은...
build.gradle에 해당 코드 추가
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
// 그리고 dependency안에 추가
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
그리고 @RequiredArgsConstructor어노테이션을 붙이면 끝!
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
쉽쥬?
롬복이 자바의 어노테이션 프로세서라는 기능을 이용하여 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 생성자가 추가된걸 알 수 있다.
package hello.core;
import lombok.Getter;
import lombok.Setter;
//롬곡 라이브러리가 자동으로 setter, getter를 만들어준다.
@Setter
@Getter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("asdfasdf");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
조회 빈이 2개 이상 - 문제
@Autowired는 TYPE으로 조회한다.
@Autowired
private DiscountPolicy discountPolicy
하지만
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
해당 타입이 2개 스프링 빈으로 등록 되어 있다.....
그럼 과연 누굴 가져와야하느냐...
NoUniqueBeanDefinitionException 예외가 발생한다.
NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy
보면 하나의 빈을 기대했는데 두개의 빈이 발견되었다고 알려준다.
이때 하위타입으로 지정할 수도 있다.
@Autowired
private FixDiscountPolicy discountPolicy
하지만 이러면 DIP를 위배하고 유연성이 떨어진다.
또한 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 두개 있으면 해결이 안됨!
설정 구성에서 스프링 빈을 수동 등록해서 해결해도되지만, 의존관계 자동 주입에서 해결하는 여러 방법이있다.
@Autowired 필드 명, @Qualifier, @Primary
조회 대상 빈이 2개 이상인 경우엔
- @Autowired 필드명
- @Qualifier => @Qualfier끼리 매칭 => 빈 이름 매칭
- @Primary 사용
@Autowired 필드명
@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드이름, 파라미터 이름으로 빈 이름을 추가매칭한다.
@Autowired
private DiscountPolicy discountPolicy
위 코드에서 필드 이름을 바꾼다.
@Autowired
private DiscountPolicy rateDiscountPolicy
필드명 rateDiscountPolicy 이 정상 주입된다.
따라서 먼저 타입 매칭을 한 다음, 여러빈이 있을때 그중 같은 필드명을 가진 빈을 가져온다.
(여기선 fixDiscountPolicy, rateDiscountPolicy 두개가 있었으므로 DiscountPolicy 타입을 찾고 그중 rate를 가져온다.)
즉, 해당 타입 빈 찾음 => 여러개가 나오니까 필드명이름이랑 매칭되는 거 가져옴
@Qualifier 사용
@Qualifier는 추가 구분자를 붙여주는 방법이다.
주입시 추가적인 방법을 제공하는 것이지, 빈 이름을 변경하는 것은 아님
빈 등록시 @Qualifier로 추가 구분자 붙임
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
그리고 사용은
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
DiscountPolicy중 mainDiscountPolicy를 가져온다고 알 수 있다.
(구현체에 의존하게 되므로 DIP 위반이지만 트레이드 오프가 있다 생각해야댐)
혹은 생정자 말고 수정자 자동 주입으로 사용하면
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
return discountPolicy;
}
즉, 타입 앞에 @Qualifier("이름")을 붙여서 사용하면 된다.
만약 @Qualifier("mainDiscountPolicy")로 등록된 스프링 빈이 없으면?!?!
그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
하지만 @Qualifier는 스프링 빈이름을 찾는게 아니라 @Qualfier를 찾도록 하는 것이 좋다.
정리하면
@Qualifier끼리 매칭 => 없으면 빈이름 매칭 => 없으면 NoSuchBeanDefinitionException 예외 발생
@Primary 사용
@Primary는 우선 순위를 정하는 방법이다.
@Autowired 시에 여러 빈이 매칭되면 @Primary를 가져온다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
이러면 DiscountPolicy를 가져올때 RateDiscountPolicy가 우선권을 가지므로 Rate를 가져온다.
(따라서 @Qualifier와 같이 클라이언트 코드를 수정할 필요가 없다.)
그리고 또한 Qualifier의 단점은 모든 코드에 @Qualifier를 붙여야한다!!
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
등록할때와 그 스프링 빈을 사용할때 다 붙여야댐....
@Primary, @Qualfier 활용
예를 들어 코드에서
자주 사용하는 메인 DB의 커넥션을 획득하는 스프링 빈,
특별한 기능으로 가끔 사용하는 서브 DB의 커넥션을 획득하는 스프링 빈
이 있다고 가정.
메인DB 커넥션을 획득하는 스프링빈 => @Primary
서브 데이터베이스 커넥션을 획득하는 스프링 빈 => @Qualifier 사용하여 명시적 지정
과 같이 하면 깔끔해진다.
이때, 메인 DB의 스프링 빈을 등록할때 @Qualifier 를 지정해주는 것은 상관없다.
- @Primary, @Qualfier 둘 중 우선순위따라서 더 명시적인 @Qualifier가 우선권이 높다.
- 스프링은 자동보다 수동이, 넓은 범위보단 좁은 범위가 우선순위가 높다.
참고)
discountPolicy에 두 개의 빈이 찾아져버리므로, 특정 빈을 찾을 수 있도록 인자의 파라미터 이름을 수정해야했습니다. (@Autowired 필드명 방식)
이것이 개방-폐쇠 원칙을 못지킨 것이 아닌가 하는 의문이 들었습니다.
-> 네 맞습니다. 클라이언트 코드를 고쳐야 하기 때문에 OCP를 지키기 못했습니다.
@Quilifier 혹은 @Primary 어노테이션을 붙이기 위해 구현체의 클래스를 찾아가서 수정해줘야하는 것 같습니다.
-> 기존 구현 클래스의 애노테이션도 변경하지 않으면 더 좋겠지만, 이 부분까지는 컴포넌트 스캔의 한계입니다. @Bean을 사용하면 확실하게 되지만 약간은 불편하지요. 따라서 둘의 트레이드 오프로 이해하시면 됩니다.
어노테이션 직접 만들기
@Qualifier 사용시 만약
@Qualifier("mainDiscountPolicy")가 아니라
@Qualifier("mmainDiscountPolicy") 처럼 오타가 났다면?!?!?!
문자를 적으면 컴파일시 타입 체크가 되지 않는다. => 오류 찾기 힘들다...
따라서 직접 어노테이션을 만들어서 컴파일시 체크되게 한다.
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
//ctrl+n 으로 Qualifier 어노테이션꺼 다 긁어와주면댐
//즉 @Qualifier 설정 다 가져옴
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") //적어줌 여기서
public @interface MainDiscountPolicy {
}
그러면 기존에 사용했던 방식을 보자
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
@Qualifier("mainDiscountPolicy") //이렇게 직접 문자로 적어줬다. <==== 따라서 오타가 나면 찾기 힘들다..
public class RateDiscountPolicy implements DiscountPolicy{
//할인율
private int discountPrice = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price*discountPrice/100;
}
else{
return 0;
}
}
}
하지만 만든 어노테이션을 적용하면
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
//일단 Fix할인 정책에서 Rate할인정책으로 바뀌였으니 비율할인 정책 구현체를 빈으로 등록
@Component
//10퍼센트만 할인 한다고 가정
//@Qualifier("mainDiscountPolicy")
@MainDiscountPolicy // <========================= 요ㅕ기 수정
public class RateDiscountPolicy implements DiscountPolicy{
//할인율
private int discountPrice = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price*discountPrice/100;
}
else{
return 0;
}
}
}
@MainDiscountPolicy 라고 붙여주므로 오타가 나도 컴파일 시점에서 잡아주니까 찾기 쉽다!!
신! 난! 다!
또한 DiscountPolicy를 사용할 때 보면
//생성자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public OrderServiceImpl(MemberRepository memberRepository,@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//수정자 자동 주입
@Autowired
//@MainDiscountPolicy 사용
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
return discountPolicy;
}
사실 어노테이션에는 상속이라는 개념이 없다
그러면 어떻게 MainDiscountPolicy 어노테이션이 @Qualifier와 같은 역할을 하게 되었을까...
바로 스프링이 제공해주는 기능이다. (갓갓...)
따라서 @Qualifier뿐만 아니라 다른 어노테이션들도 함께 조합해서 사용 가능하다.
(@Autowired도 재정의 가능하다..)
킹치만,,, 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분멸하게 재정의하면 유지보수에 혼란을 준다..
조회한 빈이 모두 필요할 때, List, Map
의도적으로, 해당 타입의 스프링 빈이 다 필요한 경우가 있다.
예를 들면, 할인 서비스를 제공할때 클라이언트가 할인 정책들 선택할 수 있다고 가정!
코드로 보면
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class AllBeanTest {
@Test
void findAllBean(){
//둘다 빈으로 등록됨
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP); // vip 고객 등록
//직접 할인 정책 고름
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
//시스템이 확인
assertThat(discountPrice).isEqualTo(1000);
assertThat(discountService).isInstanceOf(DiscountService.class);
}
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
//확인해보자
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
//할인 금액 반환
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);//스프링 빈 반환함
//확인해보자
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
참고)
위 코드에서 AutoAppConfig를 설정파일로 스프링 컨테이너를 올리므로,,,
AutoAppConfig로 컴포넌트 스캔되는 빈들은 OrderServiceImpl이 있다.
OrderServiceImpl은 생성자로 리포지토리와 할인정책이 들어와야되는데....
현재 스프링 컨테이너에는 @Component로 FixDiscountPolicy, RateDiscountPolicy가 있다..
하지만 어떤 할인 정책이 들어올지 결정해주지 않아서 스프링 컨테이너 생성시 오류가 난다.
=> DiscountPolicy라는 타입이 두개 있는데 뭘 넣어줄지 안정해줫기때문이다.
왜냐면 스프링 빈 등록시 생성자도 같이 불려지기 때문이다.
따라서 OrderServiceImpl은 @Component를 주석처리해서 스프링 빈으로 등록하지 말자
결과를 보면
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@3d3ba765, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@25bc0606}
policies = [hello.core.discount.FixDiscountPolicy@3d3ba765, hello.core.discount.RateDiscountPolicy@25bc0606]
discountCode = fixDiscountPolicy
discountPolicy = hello.core.discount.FixDiscountPolicy@3d3ba765
할인 정책들이 다 빈에 등록되어 있고, fixDiscountPolicy가 잘 선택된걸 알 수있다.
자동, 수동의 올바른 실무 운영 기준
이때까지 @Component로 자동 빈 등록하는 법과 @Bean으로 수동 빈 등록하는 법을 봣는데
그러면 언제 자동을 쓰고 언제 수동을 써야될까?
결론은 시간이 갈 수록 점점 자동을 선호하는 추세다.
스프링은 @Component뿐만 아니라 @Service, @Controller, @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.
게다가 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.
앞에서 배운대로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이다.
하지만 일일히 수동으로 @Bean 다 적고 의존관계를 주입해주는것은 엄청 귀찮다....
또한 관리하는 빈 개수가 늘어나면... 언제 다 적냐... => 부담이 되버림
결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
따라서 수동 빈은 다음과 같은 경우에 사용한다.
애플리케이션 => 업무로직 + 기술 지원 로직
- 업무로직 : 웹을 지원하는 컨트롤러, 핵심 비지니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무로직!!, 보통 비지니스 요구사항을 개발할 때 추가되거나 변경된다
- 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. DB연결이나 공통 로그처리처럼 엄부 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
따라서 업무로직은 숫자도 많고, 한번 개발해야하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다.
=> 유사한패턴? = 자동 기능을 적극 사용한다. 문제가 발생해도 어디서 문제가 생겼는지 알기 쉽다.
기술지원 로직은 그 수가 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다.
기술지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
결론.
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록하자! 따라서 설정 정보에서 바로 나타나니까 유지하기가 쉽다!
위의 DiscountServie를 다시보면
조회한 빈이 모두 필요하다. DiscountService가 의존관계 자동주입으로 Map<String, DiscountPolicy> 에 주입 받는 상황에서
어떤 빈이 주입될지, 각 빈들의 이름은 무엇인지 코드만 봐선 알기 힘들다.
만약 이걸 남이 개발해서 준거라면??? => 더더욱 파악하기 힘듬
따라서 할인정책 설정정보를 만들고 거기에 수동 빈 등록하면
할인정책 설정정보만 보면 한눈에 보이게 된다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
혹은 자동으로 하고 싶은 경우는 DiscountPolicy 패키지를 만들어서 같이 두면 패키지만 열어보면 파악이된다!!
핵심은 바로 보고 이해가 되야한다
참고로 스프링과 스프링 부트가 자동으로 등록하는 수많은 빈들은 예외이다.
==>
스프링 자체를 잘 이해하고, 스프링의 의도대로 잘 사용하는게 중요하다.
스프링 부트의 경우 DataSource같은 DB 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
반면, 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 들어내는 것이 좋다.
결론.
따로 설정정보 만들거나 같은 패키지로 묶어서 알아보기 쉽게 하자
만약 기술 지원 객체라면 수동 등록하여 명확하게 들어나게하자
빈 생명주기 콜백
빈 생명주기 콜백 시작
DB 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종류 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료작업이 필요하다.
(보통 애플리케이션은 관계형DB를 쓰는데, 미리 애플리케이션 서버가 올라올때 DB랑 연결을 미리 해둔다.)
(네트워크 소켓또한 미리 열어두면 열려잇는 소켓으로 빨리 응답할 수 있다.)
예제코드로 스프링을 통해서 초기화 작업과 종료 작업을 어떻게 진행하는지 보자.
간단하게 외부 네트워크에 미리 연결을 하는 객체 하나를 생성한다고 가정!
(실제 네트워크에 연결하는 건 아니고, 단순히 문자만 출력하도록 함)
NetworkClient는 애플리케이션 시작 시점에 connect()를 호출해서 연결을 맺어두어야하고, 애플리케이션이 종료되면 disconnect()를 호출해서 연결을 끊어야된다.
package hello.core.lifecycle;
//예제를 위한 가짜 networkclient => 실제 연결 안함
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출!!!, url = " + url);
connect(); //객체가 스프링 빈에 등록되니까 미리 연결
call("초기화 연결 메시지");
}
//외부에서 url 넣음
public void setUrl(String url) {
this.url = url;
}
//서비스 시작시 호출
public void connect(){
System.out.println("connect = " + url);
}
//서비스 종료시 호출
public void disconnect(){
System.out.println("close = " + url);
}
//call할 경우 url과 메시지 보여줌
public void call(String message){
System.out.println("call = " + url + "message = " + message);
}
}
이제 테스트 해보자.
package hello.core.lifecycle;
import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
/**
* 직접 ApplicationContext를 close할 일은 없다!
* 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
* ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
*/
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient networkClient = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig{
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
실행 결과를 보자
생성자 호출!!!, url = null
connect = null
call = nullmessage = 초기화 연결 메시지
?????
null이 나와버린다....
why?? => 다시 위로 가서 생성자 부분을 봐바, url 정보 없이 connect가 호출되어진다.
public NetworkClient() {
System.out.println("생성자 호출!!!, url = " + url);
connect(); //객체가 스프링 빈에 등록되니까 미리 연결
call("초기화 연결 메시지");
}
즉, 객체를 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해서 setUrl()이 호출되어야지 url이 생긴다.
스프링 빈의 라이프 사이클을 간단하게 보면
객체 생성 -> 의존관계 주입
(생성자 주입같은 경우는 객체 생성시점에 연결되어진다. 객체 생성하려면 생성자를 불러와야되니까..)
즉, 객체 생성하고 의존관계 주입이 다 끝나야지 필요한 데이터를 사용할 수 있는 준비가 완료된다.
but... 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까??
스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다.
또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료할 수 있다.
정리하면
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
- 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백 : 빈이 소멸되기 직전에 호출
참고로 객체의 생성과 초기화를 분리하자....
생성자 : 필수정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.
초기화 : 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
따라서 생성자에 다 넣지말고 생성하는 것과 초기화하는 것을 명확히 나누는 것이 유지보수 관점에서 좋다.
(초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우엔 생성자에서 한번에 다 처리하는게 더 나을 수 있다.)
즉, 객체 내부에 값을 세팅하는 것같이 가벼운 작업만 생성자에 넣자...
또한 생성/초기화 분리시, 동작을 지연할 수 있다는 장점이 있다.
예시로, 객체를 생성하고 외부 커넥션과 같은 행동은 어떤 액션이 들어오기 전까지 미룰 수 있다.
따라서 (액션이 들어오면) 그 때 초기화하면 된다.
싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.
이와 다르게 생명주기가 짧은 빈들도 있다. 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다. 더 자세한건 뒤에서 ....
스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
- 인터페이스(IntializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestory 어노테이션 지원
인터페이스(IntializingBean, DisposableBean) 구현해서 사용
package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public NetworkClient() {
System.out.println("생성자 호출!!!, url = " + url);
connect(); //객체가 스프링 빈에 등록되니까 미리 연결
call("초기화 연결 메시지");
}
//외부에서 url 넣음
public void setUrl(String url) {
this.url = url;
}
//서비스 시작시 호출
public void connect(){
System.out.println("connect = " + url);
}
//서비스 종료시 호출
public void disconnect(){
System.out.println("close = " + url);
}
//call할 경우 url과 메시지 보여줌
public void call(String message){
System.out.println("call = " + url + "message = " + message);
}
//디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
@Override
public void destroy() throws Exception {
disconnect();
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 연결 메시지");
}
}
테스트 결과
생성자 호출!!!, url = null
connect = null
call = nullmessage = 초기화 연결 메시지
connect = http://hello-spring.dev
call = http://hello-spring.devmessage = 초기화 연결 메시지
23:39:19.229 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Thu Mar 31 23:39:19 KST 2022
close = http://hello-spring.dev
초기화 메서드가 주입 완료 후에 적절하게 호출된 것을 확인할 수 있다.
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 연결 메시지");
}
이 메서드가 실행되었다.
connect = http://hello-spring.dev
call = http://hello-spring.devmessage = 초기화 연결 메시지
그리고 스프링 컨테이너의 종료가 호출되자 소멸 메서드가 호출된 것도 확인할 수 있다.
//디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
@Override
public void destroy() throws Exception {
disconnect();
}
이 메서드가 호출되었다.
23:39:19.229 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Thu Mar 31 23:39:19 KST 2022
close = http://hello-spring.dev
즉, 스프링이 알아서 초기화시 afterPropertiesSet메서드를 호출하고 종료시 destroy 메서드를 호출해준다.
하지만 초기화, 소멸 인터페이스를 사용하는 것은 단점이 있다.
- 해당 인터페이스들은 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
- 따라서 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 또한 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
이러한 단점이 있다고 걱정 ㄴㄴ
인터페이스를 사용해서 초기화 종료하는 방법은 스프링 초창기에 나온 방법들이다.
지금은 더 나은 방법들이 있어서 거의 사용하지 않는다.
(찾아보면 2003년도에 만든 방법이다.....)
\1. 외부 라이브러리를 수정할 수 없기 때문에 InitializingBean, DisposableBean을 외부 라이브러리에 적용할 수 없습니다.
코드를 수정할 수 없으니 해당 인터페이스들을 적용할 수 없고
해당 인터페이스를 적용할 수 없으니 메서드(afterPropertiesSet, destroy)를 구현할 수 없고
메서드가 구현되지 않으니 스프링이 로드될 때 afterPropertiesSet, destroy가 호출되지 않습니다.
구현되지 않은 메서드를 호출할 수는 없는 것이죠.
그래서 외부 라이브러리에 InitializingBean, DisposableBean을 적용할 수 없는 것입니다.
만약 난 무조건 InitializingBean, DisposableBean을 사용해서 초기화와 종료를 하겠다면 외부 라이브러리의 Wrapper Class를 만들고 해당 Wrapper Class에 InitializingBean, DisposableBean을 구현하여 Wrapper Class를 빈으로 등록할 수도 있을 것 같습니다. 저라면 그냥 @Bean의 속성(init, destory)을 사용하겠습니다.
\2. "코드가 아니라 설정 정보를 사용한다"에서 코드는 외부 라이브러리 코드를 말합니다. 만약 외부 라이브러리가 수정 가능한 형태(.java)가 아니라 .class 형태라면 우리가 할 수 있는 것은 외부 라이브러리의 API를 호출하는 것 밖에 없습니다.
설정 정보는 Bean을 수동등록하는 Configuration파일을 말합니다.
@Bean()의 init, destory 속성에 외부 라이브러리의 초기화, 종료 메서드를 지정하는 것만으로도 스프링이 시작되고 종료될 때 @Bean의 init, destory 속성에 지정된 메서드가 실행됩니다.
그래서 (외부 라이브러리의) 코드가 아니라 설정 정보(Bean을 수동 등록하는 Configuration파일)를 사용한다 라고 말씀하신 겁니다.
위의 맥락에 따라 외부라이브러리 코드 수정 없이 초기화 메서드, 종료 메서드만 지정해주면 되기 때문에 코드 수정이 필요한 InitializingBean, DisposableBean과 달리 외부 라이브러리를 사용할 수 있다고 표현하신 겁니다.
먼저 외부 라이브러리가 어떤 형태의 파일이며 그걸 어떻게 사용하는지에 대해 아시면 이해하는데 도움되실 것 같네요.
외부 라이브러리는 보통 class파일로 구성되어 있습니다. 이를 사용자게에 배포할 때는 일반적으로 java파일이 아닌 컴파일한 뒤 생성된 결과물인 class파일을 jar로 패키징하여 배포합니다.
여기서 문제가 발생합니다.
class파일은 바이트코드로 구성되어있기 때문에 JVM이 읽기 편한 파일이지 개발자가 읽으면서 수정하기 편한 파일이 아닙니다.
그러므로 개발자가 class파일을 수정하기 어렵다는 것입니다. (어려운 것이지 못하는 것은 아닙니다.)
.
예를 들어보겠습니다.
A라는 외부 라이브러리가 있으며 당연히 외부 라이브러리 특성상 모든 파일은 컴파일된 class파일입니다.
이 라이브러리에는 NetworkClient라는 클래스가 있습니다.
이 클래스에 aInit, aDestroy라는 메서드가 존재합니다.
개발자인 우리는 이 클래스를 스프링 빈으로 수동 등록하였습니다.
그런데 스프링이 로딩되고 종료될 때 자동으로 aInit(), aDestroy()가 실행되게 만드려고 합니다.
스프링 초창기에 지원했던 InitializingBean, DisposableBean 인터페이스를 사용하고자 합니다.
위 2가지 인터페이스를 NetworkClient라는 클래스에 적용하여 afterPropertiesSet, destroy를 구현하려고 했습니다.
그런데 NetworkClient는 java파일이 아니라 바이트코드로 구성된 class파일이어서 코드를 변경하기 어려운 것입니다.
그래서 NetworkClient 클래서를 직접 수정하는 방법(인터페이스를 구현하는 것) 대신 수동으로 빈을 등록할 때 @Bean의 속성인 init, destroy를 사용하는 방법을 택했습니다.
이미 NetworkClient 클래스에서 제공하는 메서드인 aInit, aDestroy가 존재하므로 init, destroy 속성의 값으로 aInit, aDestroy를 넣어 스프링이 시작하고 종료될 때 aInit, aDestroy가 실행되게 했습니다.
빈 등록 초기화, 소멸 메서드 지정
설정 정보에 @Bean(initMethod = "init", destoryMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.
NetworkClient 에 초기화, 소멸 메서드의 이름을 바꾸고 테스트 해보자.
package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출!!!, url = " + url);
connect(); //객체가 스프링 빈에 등록되니까 미리 연결
call("초기화 연결 메시지");
}
//외부에서 url 넣음
public void setUrl(String url) {
this.url = url;
}
//서비스 시작시 호출
public void connect(){
System.out.println("connect = " + url);
}
//서비스 종료시 호출
public void disconnect(){
System.out.println("close = " + url);
}
//call할 경우 url과 메시지 보여줌
public void call(String message){
System.out.println("call = " + url + " message = " + message);
}
//디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
// @Override
// public void destroy() throws Exception {
// 이름 바꾼다.
public void close() throws Exception {
disconnect();
}
// @Override
// public void afterPropertiesSet() throws Exception {
//이름 바꿈
public void init() throws Exception {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
// 이름을 바꿧으니 이제 설정정보에 초기화, 소멸 메서드를 알려준다.
}
초기화, 소멸 메서드 이름을 바꾸었으니 이제 설정 정보에 알려주자.
package hello.core.lifecycle;
import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
/**
* 직접 ApplicationContext를 close할 일은 없다!
* 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
* ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
*/
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient networkClient = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig{
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
결과.. 결과를 보자..
생성자 호출!!!, url = null
connect = null
call = null message = 초기화 연결 메시지
NetworkClient.init
connect = http://hello-spring.dev
call = http://hello-spring.dev message = 초기화 연결 메시지
20:35:23.488 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Fri Apr 01 20:35:23 KST 2022
close = http://hello-spring.dev
초기화 메서드와 소멸 메서드가 잘 호출된 것을 볼 수 있다.
자 위와같이 설정 정보를 사용한 결과를 보자.
- 메서드 이름을 자유롭게 줄 수 있게 되었다.
- 스프링 빈이 스프링 코드에 의존하지 않는다. => 위처럼 메서드 이름 맘대로 바꿔도 됨, 인터페이스 상속받아서 쓴게 아니니까
- 코드가 아니라 설정정보를 사용하기 때문에 코들르 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있게 된다.
- (즉, 초기화 할땐 init, 소멸할땐 close를 써!! 라고 지정 가능, 만약 외부라이브러리 초기화 이름이"init2"이면 init2로 이름 설정해주면 댄다.)
종료 메서드 추론
- @Bean의 destoryMethod 속성에는 아주 특별한 기능있음 => 바로.... 기본값이 AbstarctBeanDefinition.INFER_METHOD 임 => String INFER_METHOD = "(infered)" => 즈윽,, 그냥 기본 값이 infer(추론하다)임
- 라이브러리는 대부분 "close", "shutdown" 이라는 이름의 종료 메서드를 사용하므로 이값들을 추론함
- close나 shutdown 이라는 이름의 메서드를 알아서 찾아서 가져옴. 말그대로 이름을 추론해서 가져옴
- 그러므로 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작함(만약 close하지 않을 일이 있거나 그런다면,,, 거의 없겠지만,,,)
- 추론 기능을 끄고 싶으면 destoryMethod="" 처럼 빈 공백을 지정하면된다.
어노테이션 @PostConsturct, @PreDestroy <= 결론을 말하면 그냥 이거 쓰면 댐
해당 어노테이션들은 java 진영에서 공식적으로 지원하는 것이므로, 스프링 이외의 다른 컨테이너에도 적용이된다.
import javax.annotation.PostConstruct;
javax 인걸 알 수있다. => javax로 시작하면 java진영에서 공식적으로 지원하는 것.
코드로 바로 보자...
package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
//예제를 위한 가짜 networkclient => 실제 연결 안함
//InitializingBean => afterPropertiesSet() 메서드가 있음
//말 그대로 설정 끝나면, 의존 관계 주입이 끝나면 set해줌
//DisposableBean => destory 메서드가 있다.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출!!!, url = " + url);
connect(); //객체가 스프링 빈에 등록되니까 미리 연결
call("초기화 연결 메시지");
}
//외부에서 url 넣음
public void setUrl(String url) {
this.url = url;
}
//서비스 시작시 호출
public void connect(){
System.out.println("connect = " + url);
}
//서비스 종료시 호출
public void disconnect(){
System.out.println("close = " + url);
}
//call할 경우 url과 메시지 보여줌
public void call(String message){
System.out.println("call = " + url + " message = " + message);
}
//디버그를 보면 컨테이너가 내려간 것을 볼 수 있다.
// @Override
// public void destroy() throws Exception {
// 이름 바꾼다.
@PreDestroy // <====
public void close() throws Exception {
disconnect();
}
// @Override
// public void afterPropertiesSet() throws Exception {
//이름 바꿈
@PostConstruct // <====
public void init() throws Exception {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
}
아까 설정정보에 적엇던 정보는 주석처리 해버리고..
package hello.core.lifecycle;
import ch.qos.logback.core.spi.LifeCycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
/**
* 직접 ApplicationContext를 close할 일은 없다!
* 따라서 ApplicationContext의 하위 구현 클래스인 "ConfigurableApplicationContext"을 사용한다.
* ConfigurableApplicationContext하위 클래스로 AnnotationConfigApplicationContext가 있다.
*/
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient networkClient = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig{
//@Bean(initMethod = "init", destroyMethod = "close")
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
잘 호출 되는 것을 알 수 있다.
생성자 호출!!!, url = null
connect = null
call = null message = 초기화 연결 메시지
NetworkClient.init
connect = http://hello-spring.dev
call = http://hello-spring.dev message = 초기화 연결 메시지
21:17:01.553 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4659191b, started on Fri Apr 01 21:17:01 KST 2022
close = http://hello-spring.dev
@PostConstruct, @PreDestory 어노테이션 특징
- 최신 스프링에서 가장 권장하는 방법
- 어노테이션 하나만 붙이면 되니까 편리함
- 스프링에 종속적인게 아니라 자바표준임 => 다른 컨테이너에도 적용가능
- 컴포넌트 스캔과 잘 어울림 (@Bean(~~~~)적어 줄 필요가 없어지니까...)
- 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리를 초기화, 종료해야하면 @Bean의 기능을 사용하자
오픈소스들을 외부 라이브러리라고 이해하시면 됩니다. 이 경우 우리가 소스코드를 포함하는것이 아니라 이미 컴파일된 class 파일이 모여있는 jar 파일을 포함하게 됩니다.
따라서 소스코드 수정이 불가능합니다.
@Postconstruct, @PreDestroy를 적용하려면 소스코드를 수정해야겠지요?
감사합니다.
왜 어노테이션을 사용하면 외부 라이브러리에 사용을 하지 못할까라는 생각이 들었습니다.
보면 @Bean을 이용하면 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용할 수 있다고 하셨습니다.
예로 gradle을 들어보겠습니다. gradle은 저희가 수정할 수 없는 외부 라이브러리입니다. 여기서 테스트를 한다고 했을 때, 우리는 테스트 코드를 짜면서 직접 @Bean으로 등록할 때, 해당 라이브러리에 있는 클래스 안에 있는 메소드들을 파악하고 빈으로 직접 등록하여 초기화, 종료를 할 수 있다.
그러나 어노테이션은 코드에 @을 붙여야하는데 코드를 수정할 수 없기 때문에 사용할 수 없다.
->
@PostConstruct, @PreDestroy 애노테이션들은 코드가 수정 가능한 곳에 적용 가능하며
그렇지 못한 곳에서는 @Bean의 init method, destory method를 통해서 초기화, 종료 메서드를 지정할 수 있습니다.
여기서 고칠 수 없는 외부라이브러리를 NetworkClient라고 하고
@Bean의 initMethod와 destroyMethod를 지정하면
이 지정한 메소드가 NetworkClient안에 만들어야 한다고 인텔리제이에서 오류수정이 뜨는데 외부라이브러리는 수정할 수가 없는 상황인데 이 부분이 잘 이해가 되질 않습니다. ->
외부에서 제공하는 라이브러리들은 보통 어떤 초기화, 종료 메서드를 제공하는지 알려줍니다.
외부에서 제공하는 라이브러리가 초기화, 종료 메서드가 없다면 스프링에서도 없는 메서드를 호출할 수는 없습니다.
정리
- @PostConstruct, @PreDestroy 어노테이션을 사용하자.
- 코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야되면 @Bean 의 initMethod, destoryMethod 를 사용하자.
빈 스코프
이때까지는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될 때까지 유지된다고 알았다.
하지만 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.
스코프 = 빈이 존재할 수 있는 범위
스프링이 지원하는 스코프
- 싱글톤 : 기본 스코프 = 디폴드 값임, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 짧은 범위의 스코프즉, 요청하면 그때 만든다. + 의존관계 주입도 해준다. + 초기화메서드가 있으면 초기화까지 해서 던져준다.
- (client에게 반환해주고, 컨테이너는 더이상 관리하지 않는다! => 그러면 종료메서드는 누가 호출해주냐... 클라이언트가 호출하던가 해야댐)
- 컨테이너가 빈을 만들어서 던져준 뒤 관리하지 않는다.
- 웹 관련 스코프
- request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프이다.
- Request(생성) ~~~ Response(소멸)
- session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
싱글톤, 프로토타입, request 정도만 알면된다
빈 스코프를 지정하는 방법
@Scope("prototype")
@Component
public class Bean{}
@Scope("prototype")
@Bean
PrototypeBean Bean(){
return new Bean();
}
프로토타입 스코프
싱글톤 스코프 빈 => 조회시 항상 같은 인스턴스를 반환했음
프로토타입 스코프 => 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환함
따라서 스프링 컨테이너는 요청하는 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
그리고 생성한 빈을 클라이언트에게 던져주고 관리하지 않는다.
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리함
그러므로 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있음. @PreDestory같은 종료메서드는 호출되지 않음.
따라서 소멸하고 싶으면 client가 해줘야됨.
일단 먼저 기존 싱글톤 사용시 객체를 두번 조회해보자
package hello.core.scope;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
//기존 싱글 톤의 경우 생성된 인스턴스가 같다
public class SingletonTest {
@Test
public void singletonBeanFind(){
//Component class 를 넣어주는 것이다.
//자동으로 component 스캔 대상이된다.
//따라서 스프링 빈으로 등록되는 것이다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
//@Configuration이 붙지 않으면 순수 객체로 등록된다.
//@Configuration 은 등록하는 빈들이 여러개 있다.
@Scope("singleton")//디폴트값임
static class SingletonBean {
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
@PreDestroy
public void close(){
System.out.println("SingletonBean.destroy");
}
}
}
테스트가 정상적으로 작동한다.
assertThat(singletonBean1).isSameAs(singletonBean2);
SingletonBean.init
23:00:17.485 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:00:17 KST 2022
SingletonBean.destroy
싱글톤은 초기화가 한번만 되었따.
이제 프로토타입 스코프 빈을 테스트 해보자
package hello.core.scope;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class PrototypeTest {
@Test
public void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close(); // 컨테이너 종료
}
//@Configuration이 붙지 않으면 순수 객체로 등록된다.
//@Configuration 은 등록하는 빈들이 여러개 있다.
@Scope("prototype")//프로토타입으로 설정
static class PrototypeBean {
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void close(){
System.out.println("PrototypeBean.destroy");
}
}
}
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
성공적으로 동작함을 알 수 있다.
이제 결과를 보자
PrototypeBean.init
PrototypeBean.init
23:06:14.069 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:06:13 KST 2022
Process finished with exit code 0
앞선 싱글톤 테스트 결과와 비교하면
SingletonBean.init
23:00:17.485 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4310d43, started on Fri Apr 01 23:00:17 KST 2022
SingletonBean.destroy
PrototypeBean.init PrototypeBean.init
두번 초기화가 되엇고, 테스트 성공으로 각각 다른 인스턴스가 생겨난것을 알 수 있다.
하지만 싱글톤 테스트시에는 SingletonBean.destroy 즉 소멸자가 잘 호출되었는데
프로토타입 테스트시에는 소멸자가 호출되지 않았다.
따라서 직접 prototypebean1.destory()와 같이 직접 닫아줘야한다.
정리
- 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
- 컨테이너는 생성, 의존관계주입, 초기화까지만 관여한다.
- 그러므로 종료 메서드가 호출되지 않는다.
- 프로토타입 빈은 해당 빈을 조회한 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접해야한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
둘다 섞어쓰면 문제가 발생할 수 도 있다.
왜일까? 두 빈의 스코프를 잘 생각해보자.
만약 싱글톤 빈이 프로토타입 빈을 호출하면???
문제) 이때 프로토타입 빈의 스코프는??
-> 바로 싱글톤빈이 죽을때 까지 같이 들고 있는 거다.
원래 프로토타입 빈의 목적은?? -> 조회시 마다 새로 생성해서 다 다른 객체 인스턴스를 가지게하는것!
싱글톤빈이 안고 가버리면?? 같은 객체 인스턴스가 튀어나와버림
테스트로 확인해보자
먼저 프로토 타입 빈을 조회하여 addCount를 각각 해보자
package hello.core.scope;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0;
public void addCount(){
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void close(){
System.out.println("PrototypeBean.destroy");
}
}
}
테스트가 잘 돌아가는 것으로 보아 각각 1씩 저장됨을 알 수 있다.
여기서 이제 싱글톤 빈이 프로토타입 빈을 조회하게 해보자.
즉, 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하게해보자.
- clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
- 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환. (여기서 프로토타입 빈의 count필드 값은 0이다)
- clientBean은 프로토타입 빈을 내부 필드에 보관한다.(정확히는 참조값을 보관한다.)
- 클라이언트A는 clientBean을 스프링 컨테이너에 요청하고 받는다. (clientBean은 싱글톤이므로 항상 같은 빈임)
- 클라이언트A는 clientBean.logic()을 호출한다.
- clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. => count값이 1이됨
- 그 다음 클라이언트B가 clientBean을 스프링빈에 요청하고 받는다.
- 그리고 같은 로직을 실행한다. => prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. => count값이 2가 된다.
왜일까? clientBean은 싱글톤이다. 따라서 항상 같은 객체이다.
앞서 클라이언트A가 싱글톤인 clientBean을 요청하고 프로토타입 빈 로직 addCount()을 실행했다.
따라서 싱글톤 clientBean은 프로토타입 prototypeBean을 가지게 된다.
그래서 클라이언트B 도 같은 prototypeBean을 사용하게 된다.
테스트 코드로 보자
package hello.core.scope;
import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
assertThat(clientBean1.logic()).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
assertThat(clientBean2.logic()).isEqualTo(2); //2가 되어버린다.
}
//귀찮아서 롬복으로 생성자 만듬.. => 의존관계 주입
@RequiredArgsConstructor
//디폴트 이므로 싱글톤임
static class ClientBean{
private final PrototypeBean prototypeBean;
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0;
public void addCount(){
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void close(){
System.out.println("PrototypeBean.destroy");
}
}
}
결론적으론 싱글톤 빈과 함께 계속 유지되어서 문제이다.
원했던건 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때마다 새로 생성해서 사용하는 것을 원했다...
여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
ex)ClientA, ClientB가 각각 의존 관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입받는다.
프로토타입 스코트 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결
가장 간단한 방법으론 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
package hello.core.scope;
import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
assertThat(clientBean1.logic()).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
assertThat(clientBean2.logic()).isEqualTo(1);
}
//귀찮아서 롬복으로 생성자 만듬..
//@RequiredArgsConstructor
//디폴트 이므로 싱글톤임
static class ClientBean{
//프로토타입을 사용할때 마다 스프링 컨테이너에 새로 요청!!
@Autowired //필드주입
private ApplicationContext ac;
//private final PrototypeBean prototypeBean;
//그리고 로직 실행시 매번 새로 받아옴
public int logic(){
//매번 새로운 PrototypeBean 생성 => 컨테이너에 요청
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0;
public void addCount(){
count++;
}
public int getCount() {
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void close(){
System.out.println("PrototypeBean.destroy");
}
}
}
결과를 보면
PrototypeBean.init
PrototypeBean.init
ac.getBean()으로 항상 새로운 프로토타입 빈이 생성된다.
이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존 관계 조회(탐색)이라 한다.
(외부에서 의존관계를 주입 받는게 아니라 직접 스프링 컨테이너에게 요청하여 가져왔다.)
스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위테스트도 어려워진다.
스프링 컨테이너에 종속되면 안되는 이유???
거의 그럴일이 없기는 한데, 스프링 말고 다른 DI 컨테이너들도 있습니다^^
스프링 -> 다른 DI 컨테이너로 이동할 때까지 함께 고민하는 부분입니다.
결론적으로 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL정도의 기능만 제공하는 무언가이다
스프링은 모든게 준비되었다. 후훗..
ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.
과거에는 ObjectFactory가 있었지만, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.
//디폴트 이므로 싱글톤임
static class ClientBean{
//
// //프로토타입을 사용할때 마다 스프링 컨테이너에 새로 요청!!
// @Autowired //필드주입
// private ApplicationContext ac;
//private final PrototypeBean prototypeBean;
//필드 주입이다...(테스트니까) => ObjectProvider객체는 빈으로 등록안했는데 어케 의존관계 주입??
//스프링이 기본적으로 들고 있는다. 즉, 자동 주입해준다.
//대리자 역할을 한다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
//그리고 로직 실행시 매번 새로 받아옴
public int logic(){
//getObject 하면 알아서 찾아준다.
PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- prototypeBeanObjectProvider.getObject() 으로 항상 새로운 프로토타입 빈이 생성된다.
- ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. => 즉 DL
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
- ObjectProvider는 지금 딱 필요한 DL정도의 기능만 제공한다.스프링 컨테이너에 직접 찔러서 요청하는게 아니라 대리자 역할을 해준다.
- 즉, 스프링 컨테이너를 통해서 찾아주는 과정을 간단하게 도와주는 것이다.
ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요없다. 스프링에 의존한다.
ObjectProvider : ObjectFactory상속, 옵션, 스트림 처리등 편의기능이 많고, 별도의 라이브러리가 필요없다. 스프링에 의존
\1. 그러면 프로토타입을 요청을 하고 생성된 후 생성된 빈을 찾아 반환해주는 것 까지의 모든 과정이 Provider가 혼자 수행하는 역할이 맞나요?(요청 및 반환)
=> Provider가 특화된 기능을 모아서 제공하지만 모든 동작을 단독으로 처리하는 것은 아닙니다. 예를들어 프로토타입 빈을 생성하는 것은 빈 팩토리에게 위임합니다.
\2. 또한 이 모든 과정 자체를 DL이라고 볼 수 있는건가요?(생성된 것만을 조회하는것이 아닌 더 넓은 의미로, 처음 요청(조회)하여 생성하고 반환 해주는 것까지 DL이라고 볼 수 있는지)
아니면 요청하고 생성하는 것은 제외한 생성된 빈을 조회하여 반환해주는 것까지의 과정이 DL인가요?
=> 어디까지나 Provider의 핵심 역할은 빈을 조회하여 가져오는 것입니다.(DL) 다만, Prototype의 경우 새로운 빈을 생성해야 하기 때문에 Provider가 빈 팩토리에 이를 요청합니다.
\3. getObjetct() 등으로 요청을 한 뒤에, 빈이 생성되는게 맞나요?(순서가 맞나요?)
=> 네 맞습니다.
\4. ObjectProvider prototypeBeanProvider @Autowired로 주입 받아서 new 같은 초기화를 안해도 되나요?
=> 이미 생성된 빈을 주입 받는 것이기 때문에 별도로 생성하지 않아도 됩니다.
\2. 이번 강의에서는 앞 강의와 다르게 @Configuration 을 사용하지 않으셨는데, 그냥 하나의 설정파일에 여러개 빈을 한꺼번에 등록하느냐, 아니면 각각 등록하느냐의 차이만 있을 뿐인가요?
Provider를 통해서 싱글톤 빈을 DL 하는 경우에는 싱글톤 빈이 생성 되는 것이 아니라 조회됩니다^^
프로토타입은 조회할 때 마다 새로 생성됩니다.
DL은 컨테이너를 통해서 빈을 찾아온다고 생각하시면 됩니다.
이번강의에서 ObjectProvider 오브젝트를 Autowired로 의존관계 주입을 받아서 사용하는데요!
제가 토비의 스프링 책이랑 강사님 강의랑 같이 공부중인데, Autowired 로 의존관계 주입을 받을때에는 일단 스프링 컨테이너에 등록된 빈들 중에서 타입이 맞는 오브젝트를 주입해주는 것으로 알고있는데요
그렇다면 저 예제에서 ObjectProvider 인터페이스를 구현한 오브젝트가 스프링 컨테이너에 빈으로 등록이 되어야 의존관계 주입을 받을 수 있을것 같은데요
책에서는 ObjectFactory 예제이긴 하지만 어쨋든 XML 파일에 ObjectFactoryCreatingFactoryBean 이라는 빈을 생성해서 ObjectFactory 의존관계를 주입받아서 사용하는데, 강의에서는 빈 생성없이 그냥 바로 주입받아서 쓰는 것 같아서 질문드립니다.
=>
이러한 문제를 해결하는 다양한 방법이 있는데요. ObjectProvider를 사용하면 별도의 빈을 등록하지 않아도 스프링이 이런 부분을 자동으로 처리를 해줍니다.
"ApplicationContext에 비해 ObjectProvider는 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다."
라는 말이 이해가 안됩니다..
스프링 없이 순수 자바 코드만으로 단위 테스트할 경우,
테스트할 코드가 ApplicationContext에 의존하던, ObjectProvider에 의존하던 DL 기능을 사용할 수 없는 건 마찬가지 아닌가요?
DL 기능을 구현한다고 하더라도 ApplicationContext이던 ObjectProvider이던 상관없지 않나요?
그래서 ApplicationContext이 아닌 ObjectProvider를 써야하는 이유를 잘 모르겠습니다!
=>
테스트에서는 가짜 구현체를 만드는 경우가 있습니다.
여기에서 가짜 구현체는 모든 기능이 동작하지 않아도 됩니다. 테스트를 실행하는데 필요한 정도만 동작하면 됩니다.
ApplicationContext를 mock으로 만들려면 그곳에 있는 수 많은 인터페이스를 모두 구현해야 합니다.
반면에 ObjectProvider를 사용하면 그곳에서 제공하는 매우 적은 인터페이스만 구현해도 되므로 mock을 상대적으로 쉽게 만들 수 있습니다.
다음 코드를 통해서 스프링 컨테이너가 생성되는데요.
이 시점에 스프링은 생성자의 파라미터로 넘긴 ClientBean.class, PrototypeBean.class 두 클래스는 스프링 빈으로 등록됩니다.
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
스프링 빈으로 둘이 등록되어 있기 때문에 @Autowired가 가능합니다.
질문3) "스프링"에서 '컨테이너'라는 것은 하나의 프로젝트 안에서 단 1개로 global하게 존재하는 객체가 아닌, 이곳저곳에 여러 개로 존재할 수 있는 독립적인 객체인가요?
-> 다음 코드가 바로 스프링 컨테이너 1개를 뜻합니다.
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
이것을 여러개 생성한다면 독립적으로 존재할 수 있습니다.
그런데 보통 1개만 사용합니다.
provider.get();을 하면 스프링 컨테이너를 통해서 조회하기 때문에, 필요한 의존관계 주입과 초기화의 도움을 받을 수 있습니다.
반면에 new PrototypeBean()을 사용하게되면 의존관계 주입도 안되고, 필요한 초기화도 안됩니다. 모든 것을 직접 수동으로 해주어야 합니다.
그렇다고 둘중에 어떤 것이 좋다기 보다는 개발 시점에 더 적절한 방법을 선택하면 됩니다^^
스프링에서 제공하는 Provider외에 자바 표준으로 제공하는 JSR-330 Provider도 있따.
javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
따라서 해당 라이브러리를 gradle에 추가해야된다.
추가
implementation 'javax.inject:javax.inject:1'
package javax.inject;
public interface Provider<T> {
T get();
}
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
provider.get()을 통해서 항상 새로운 프토토타입 빈이 생성된다.
provider의 get() 호출 시 내부에서는 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환한다. => DL
자바 표준이고 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 쉬워진다.
Provider는 지금 딱 필요한 DL정도의 기능만 제공한다.
장점= 심플 = 단점
get() 메서드 하나로 깅으 매우 단순하다.
별도의 라이브러리가 필요하다.
정리
- 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 프로토타입 빈을 사용하면된다.
- 실무에서 웹 애플리케이션을 개발하다보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기때문에 잘 안씀
- ObjectProvier, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
그럼 둘 중 뭘써야할까??
대부분 스프링이 더 다양하고 편히란 기능을 제공해주기때문에 다른 컨테이너를 쓸일이 없다면 스프링이 제공하는 기능을 쓰면된다.
웹스코프
- 웹 환경에서만 동작한다.
- 스프링이 해당 스코프의 종료시점까지 관리한다 => 따라서 종료 메서드가 호출된다.
웹스코드 종류
- request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리됨.
- (요청마다 각각 따로 호출된다.)
- sessoin : HTTP Session과 동일한 생명주기를 가지는 스코프
- application : 서블릿컨텍스트와 동일한 생명주기를 가지는 스코프
- websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
request를 예로 들면
클라이언트A ->(HTTP request 요청) -> Controller@x01 -> Service@x02
클라이언트B ->(HTTP request 요청) -> Controller@x01 -> Service@x02
여기서 Controller는 myLogger를 요청하는데 각 요청마다 객체를 새로 생성해서 만든다.
A전용@x03 , B전용@x04 .myLogger
- 컨트롤러는 Request Scope 관련 객체를 조회
- (위 예시에서는 log를 찍는다고 가정)
- A클라이언트 전용으로 객체가 만들어진다.
- 서비스 객체에서 로그객체 조회, 그리고 HTTP request가 같으면 같은 객체를 본다.
- B클라이언트 요청이 들어오면, 다른 HTTP 요청이므로 별도의 객체를 생성한다.
=> HTTP request에 딱 요청이 들어오고 나갈때까지의 Life cycle 동안은 무조건 같은애(같은 객체)가 관리된다.
웹 환경 추가
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리 추가를 해줘야한다.
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
위 라이브러리를 추가하면 hello.core.CoreApplication 의 main메서드를 실행하면 웹 애플리케이션이 실행되는 것을 확인할 수 있다.
spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.
스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동한다.
웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로, AnnotationConfigServletWebServerApplicationContext (컨테이너이다)를 기반으로 애플리케이션을 구동한다.
만약 기본 포트인 8080 포트를 다른곳에서 사용중이어서 오류가 발생하면 포트를 변경해야 한다. 9090 포트로 변경하려면 다음 설정을 추가하자. main/resources/application.properties
server.port=9090
request 스코프 예제 개발
만약 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 request 스코프를 사용하면 편하다.
- 기대하는 공통포멧 [UUID] [requestURL] {message}
- UUID를 사용해서 HTTP 요청을 구분하자
- requestURL 정보를 넣어서 어떤 URL을 요청해서 남은 로그인지 확인
MyLogger => 로그 남김 (기대하는 공통포멧 [UUID] [requestURL] {message})
LogDemoController => 해당 요청 들어오면 로직이 돌게 처리함.
LogDemoService => 비지니스 로직, 로그를 찍어줌
package hello.core.common;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Component
@Scope(value = "request") //request 스코프
public class MyLogger {
/**
* 기대하는 공통포멧 [UUID] [requestURL] {message}
*/
private String uuid;
private String requestURL;
//빈이 생성되는 시점을 알수 없으므로 set으로 주입받음
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message){
System.out.println("[" + uuid +"[" + requestURL + "]" + "[" + message +"]");
}
@PostConstruct
//초기화 => uuid값 설정
public void init(){
//절대로 안겹치게 id 랜덤으로 생성해줌
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo") //log-demo 요청이 오면
@ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
//HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
// http request 정보를 받을 수 있음
public String logDemo(HttpServletRequest request){
//HttpServletRequest로 http request 정보 받앗으므로
//getRequestURL로 요청된 url 알 수 있음
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId"); //일단 아무로직 만듬
return "OK";
}
}
package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
/**
* 비니지스 로직 이 있는 서비스 계층
* 여기서도 로그를 출력
*
* request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
* 파라미터가 많아서 지저분해진다.
*
* 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
*
* 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
* 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
*
* request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
* MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
*
*/
private final MyLogger myLogger;
//myLogger 의 log 사용.
public void logic(String id){
myLogger.log("Service id = " + id);
}
}
실행하면 에러가 발생한다.
Caused by: org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
LogDemoController에선 생성시점에 MyLogger가 필요하다.
@Component
@Scope(value = "request") //request 스코프
public class MyLogger {
Mylogger의 스코프틑 request 스코프이다....
따라서 생성자 의존관계 주입시 mylogger가 없다!!
즉, 실제 고객의 요청이 있어야지 mylogger가 생성되어진다.
해결방안
스코프와 Provider
provider를 사용하는 것이다.
(스프링의 provider 사용해봄)
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {
private final LogDemoService logDemoService;
// private final MyLogger myLogger;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
@RequestMapping("log-demo") //log-demo 요청이 오면
@ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
//HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
// http request 정보를 받을 수 있음
public String logDemo(HttpServletRequest request){
//HttpServletRequest로 http request 정보 받앗으므로
//getRequestURL로 요청된 url 알 수 있음
String requestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId"); //일단 아무로직 만듬
return "OK";
}
}
package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
/**
* 비니지스 로직 이 있는 서비스 계층
* 여기서도 로그를 출력
*
* request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
* 파라미터가 많아서 지저분해진다.
*
* 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
*
* 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
* 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
*
* request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
* MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
*
*/
// private final MyLogger myLogger;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
//myLogger 의 log 사용.
public void logic(String id){
//getObeject로 조회함.
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.log("Service id = " + id);
}
}
웹브라우저로 해당 request를 요청하면
[64d2b075-c69e-4303-a57c-5cd751b5a3bd] request scope bean create:hello.core.common.MyLogger@1c470380
[64d2b075-c69e-4303-a57c-5cd751b5a3bd[/log-demo][controller test]
[64d2b075-c69e-4303-a57c-5cd751b5a3bd[/log-demo][Service id = testId]
[64d2b075-c69e-4303-a57c-5cd751b5a3bd] request scope bean close:hello.core.common.MyLogger@1c470380
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c] request scope bean create:hello.core.common.MyLogger@4890e67a
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c[/log-demo][controller test]
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c[/log-demo][Service id = testId]
[b94fe3c7-0582-40ae-83a5-d61d0df8ed9c] request scope bean close:hello.core.common.MyLogger@4890e67a
[55130fb8-e05e-4f55-8b8f-461e68d86a75] request scope bean create:hello.core.common.MyLogger@15afd791
[55130fb8-e05e-4f55-8b8f-461e68d86a75[/log-demo][controller test]
[55130fb8-e05e-4f55-8b8f-461e68d86a75[/log-demo][Service id = testId]
[55130fb8-e05e-4f55-8b8f-461e68d86a75] request scope bean close:hello.core.common.MyLogger@15afd791
각각 다른 객체가 생성됨을 알 수 있다.
- ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject() 를 LogDemoController, LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다. => 직접 구분하려면 힘들다...
하지만 여기서 더 줄일 수 있다...
스코프와 프록시
프록시 방법을 사용해보자.
프록시를 사용하여서 Provider의 기능을 대체할 수 도 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //request 스코프
public class MyLogger {
proxyMode = ScopedProxyMode.TARGET_CLASS
TARGET_CLASS : 적용 대상이 인터페이스가 아닌 클래스이면
INTERFACE : 적용대상이 인터페이스이면
이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
프록시 객체는 하나만 생성되고 필요한 곳에 프록시 객체가 주입된다.
요청이 들어오면, MyLogger의 프록시 객체는 MyLogger 빈을 찾을테고 없으면 생성한다.
- 일단 가짜 프록시 객체를 주입
- 요청이 들어오면 프록시 객체는 MyLogger 빈을 찾고 없으면 생성한다.
- 요청이 응답될때가지 동일한 MyLogger 빈 사용
즉 일단 가짜 끼워넣고 진짜 요청이 들어오면 그때 MyLogger 빈 반환
\1. 코드적으로는 싱글톤을 사용하는 것 처럼 작성해도 되지만 실제로는 (예제상) LogDemoController와 LogDemoService가 서로 다른 프록시 객체와 의존관계를 맺고 있는것으로 이해하였는데요, 이게 맞는지 궁금합니다.
=> 프록시 객체는 하나만 생성됩니다. 그리고 필요한 곳에 프록시 객체가 주입됩니다.
\2. 또한 이렇게 각각의 프록시 객체와 의존관계를 맺게 되더라도, 이 각각의 프록시 객체가 진짜 MyLogger를 이용할 때에는 여전히 동일한 MyLogger가 사용되는지 궁금합니다.
=> 요청이 들어왔을 때 MyLogger의 프록시 객체는 MyLogger 빈을 찾을테고 없으면 생성합니다. 이후부터는 해당 요청이 응답될 때까지 동일한 MyLogger 빈을 사용하게 됩니다.
\1. request스코프는 고객의 http request요청이 있어야지만 생성이 가능하다
2.provider는 provider를 이용한 지연처리, DL을 통해 요청시점까지 기다렸다가 요청시점에 생성한다
\1. Proxy Mode를 사용했을 때, CGLIB를 활용한 빈 객체는 스프링 컨테이너가 생성될 때 최초로 단 한 번만 생성된다. 프록시 객체가 호출되면, 호출될 때 마다 진짜 객체를 만든다. 위의 내용처럼 이해를 하는 것이 맞을까요? 그리고 싱글톤 스코프처럼 사용 시 문제가 발생할 수 있다는 것은 프록시 객체는 단 한번 생성되나, 진짜 객체는 매번 생성되고 소멸되니 그것에 대한 COST가 비싸다는 것으로 이해를 하면 될까요?
=> 프록시 객체가 호출될 때마다 진짜 객체를 매번 생성할지는 말지는 프록시 객체가 알고 있는 진짜 빈의 스코프에 따라 달라집니다. 싱글톤 빈처럼 사용되는 것이 프록시 객체의 장점이지만 그 뒤에서 실제 동작하는 방식은 스코프(Request, Singleton, Protorype 등)에 따라 달리지기 때문에 싱글톤처럼 사용시 문제가 발생할 수 있다는 것입니다.
\2. 이 강의해서 사용된 REQUEST 스코프의 동작 방식이 궁금합니다. 사용된 코드 동작방식을 보면 HTTP REQUEST가 들어오면 REQUEST SCOPE 객체를 만들고 나갈 때까지객체를 유지하고, 나간다면 다음으로 들어온 HTTP REQUEST를 입력받아서 동작하는 것처럼 보입니다. 즉, 항상 HTTP REQUEST SCOPE 객체는 스프링 컨테이너에 하나만 존재하는 것처럼 보입니다. 그런데 예를 들어 정말 동시에 HTTP REQUEST가 들어오면, 어떻게 동작하는지 알 수 있을까요? 동시에 들어오면 HTTP REQUEST SCOPE 빈이 스프링 컨테이너에 다수 개가 생성될 것 같아서... 기존에 컨트롤러와 서비스가 동작하던 것처럼 동작하지 않을 것 같습니다.
=> 스코프는 객체가 아니라 빈의 라이프 사이클 범위를 말하는 것입니다. 특정 빈의 스코프가 Request라면 사용자 요청이 서버에 들어올 때마다 빈이 생성되고 요청이 다 처리되면 소멸됩니다. 따라서 다수의 요청이 들어왔을 때 Request 스코프인 빈을 호출하게 되면 각 요청별로 빈이 생성되어 사용됩니다.
싱글톤처럼 사용 시 문제가 될 수 있다는 부분의 좀 더 정확한 것이 무엇인지 알려주실 수 있으실까요? 프록시 객체는 어찌되었건 필요한 시점에는 객체가 만들어져 있어, 프로그램 동작 과정에서 문제가 없는 것으로 알고 있습니다.
예를 들어 프록시 객체의 진짜 객체가 싱글톤이라고 했을 때, 싱글톤처럼 사용하는 것은 문제가 없을 것입니다. 프록시 객체의 진짜 객체가 Request 스코프라고 가정하면, 매번 프록시 객체를 호출할 때 마다, 객체를 생성하고 반환해주기 때문에 객체를 생성하는데 많은 비용이 들 것입니다. 그런데 이를 싱글톤 객체라고 착각하고 개발자가 그렇게 설계를 할 경우, 이런 비용 관점에서 문제가 있다고 보는 것이 타당할까요?
말씀하신 '싱글톤처럼 사용 시 문제가 될 수 있다는 것'이의 간단한 예라도 하나 알려주시면 너무 감사드리겠습니다.
=> (상태를 가지지 않는) 싱글톤을 사용하는 이유 중 하나는 멀티스레드 환경에서 객체 재사용성을 높이기 위함입니다. 억지스러울 수 있지만 예를 들어보자면 엄청나게 많은 요청이 쏟아질 때 싱글톤처럼 보이는 프록시 객체를 호출했습니다. 그런데 프록시 객체가 뒤에서 호출한 건 프로토타입 스코프를 가지는 빈이었던 겁니다. 그러면 수많은 빈이 생성될테고 이는 서버에 불필요한 부하를 주게 될 것입니다. 원래 의도는 싱글톤 빈인 줄 알고 열심히 호출했었던 것이니깐요.
==> 다수의 요청이 들어오면, 각 요청 별로 빈이 생성되어 사용된다는 것은 이해를 하고 있습니다. 질문이 정확하지 못한 점 죄송합니다. 이렇게 질문드리는것이 제 질문 의도와 맞는 것 같습니다.
ClientA가 Http request를 요청하고, Controller에서 해당 request를 처리하고 있는 상황입니다. 이 때, ClientB에서 Http Request를 요청이 또 들어옵니다. 이 때, ClientA의 http 요청은 반환이 되지 않은 상황입니다. 이런 상황이라면 ClientB의 Request 스코프 객체는 ClientA의 Http 요청이 끝나서 완료될 때까지 생성이 연기될까요? 아니면, ClientA의 요청과는 별개로 ClientB의 Request 스코프 객체가 생성되어 스프링 컨테이너에 관리가 될까요?
=> 이미 알고 계신 것 같습니다. 각 요청(request)별로 빈이 생성되고 컨테이너에 의해 관리됩니다.
\3. 한 가지 추가 질문이 있습니다! 혹시 이런 프록시 모드로 생성되는 객체는 스프링 컨테이너가 생성될 때, 단 한번 생성된다고 이해를 하면 될까요?
=> 네, 프록시 객체는 한 번만 생성됩니다.
주입되어있는 프록시객체는 진짜 객체의 클래스를 상속받았으므로, request가 들어올 때에도 그대로 자리를 유지하여 사용된다. 단, request가 들어올 때 setter 같은 게 동작하여 프록시객체를 진짜스럽게 만들어준다.
스프링 빈에는 객체를 상속받은 프록시가 등록됩니다. (별도의 설정이 없을경우)
프록시를 직접 구현하는 예제를 정리한 블로그 링크를 남기니 참고 하시기 바랍니다.
https://mangkyu.tistory.com/m/175?category=761302
ProxyMode로 사용하게 되면 PrototypeBean 내부 메서드를 호출할 때마다 새로운 빈이 생성됩니다.
addCount();, getCount();를 순차적으로 호출하게 될텐데
addCount() 호출할 때 빈이 새롭게 생성되고
getCount() 호출할 때 빈이 새롭게 생성됩니다.
실제로 addCount() 로직을 탑니다만 getCount()를 호출할 때에는 새롭게 생성된 빈에서 호출하기 때문에 count가 0으로 보이는 것입니다.
addCount(), getCount() 내에서 prototypebean에 대한 정보를 출력하는 코드를 작성해보시면 서로 다른 빈임을 확인하실 수 있습니다.
Protytype에서의 ProxyMode를 사용할 때에 대한 동작은 아래 블로그를 참고해주세요.
https://renuevo.github.io/spring/scope/spring-scope/
감사합니다. 다시 듣고왔는데 configuration부분 에서는 이용해서 스프링이 가짜 객체를 만들어 싱글톤을 보장하는 것이기 때문에 cglib기술을 사용하는것이고 provider를 사용해야 할 때 나중에 생성될 객체를 위해 가짜 객체를 만들어놓기 위해 cglib기술을 사용하는 것이라고 보면 될까요?
네 Configuration에서는 스프링이 의존관계 주입을 위해서 메서드를 호출할 때 싱글톤을 보장하기 위해서 Configuration 클래스를 가짜 객체로 만들어줍니다^^
프록시는 싱글톤으로 되어있지만 진짜 빈을 찾을때는 싱글톤 객체가 되면 안될거 같은데 프록시가 진짜 빈을 요청 할때마다 스프링 컨테이너는 요청이 들어올때마다 새로운 request scope 객체를 생성해서 넘겨주나요 ?
스프링 컨테이너는 동일한 하나의 HTTP 요청안에서 처음 조회시에는 빈이 없으니 생성해서 반환하고, 두번째 조회시에는 앞서 빈을 만들어 두었으니 만들어둔 빈을 반환합니다.
객체가 사라지는 시점은 사용자에게 클라이언트에게 리턴값을 준 후 없어지나요?..
네 맞습니다
코드 다시 원상 복구!
package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
/**
* 비니지스 로직 이 있는 서비스 계층
* 여기서도 로그를 출력
*
* request scope르 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면
* 파라미터가 많아서 지저분해진다.
*
* 다른 문제로는 reqeustURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게된다.
*
* 웹과 관련된 부분은 컨트롤러까지만 사용해야된다.
* 서비스계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
*
* request Scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
* MyLogger의 멤버 변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
*
*/
private final MyLogger myLogger;
// private final ObjectProvider<MyLogger> myLoggerObjectProvider;
//myLogger 의 log 사용.
public void logic(String id){
// MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.log("Service id = " + id);
}
}
package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor //롬복 라이브러리로 생성자 자동 생성
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
// private final ObjectProvider<MyLogger> myLoggerObjectProvider;
@RequestMapping("log-demo") //log-demo 요청이 오면
@ResponseBody //뷰화면 없이 그냥 바디부에 보내버림
//HttpServletRequest => 자바에서 제공하는 표준 서블릿 규약에 의한
// http request 정보를 받을 수 있음
public String logDemo(HttpServletRequest request){
//HttpServletRequest 로 http request 정보 받앗으므로
//getRequestURL로 요청된 url 알 수 있음
String requestURL = request.getRequestURI().toString();
// MyLogger myLogger = myLoggerObjectProvider.getObject();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId"); //일단 아무로직 만듬
return "OK";
}
}
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$d7393e42
[07ec8e0b-b299-4395-aebf-3245b535fbe9] request scope bean create:hello.core.common.MyLogger@5650b334
[07ec8e0b-b299-4395-aebf-3245b535fbe9[/log-demo][controller test]
[07ec8e0b-b299-4395-aebf-3245b535fbe9[/log-demo][Service id = testId]
[07ec8e0b-b299-4395-aebf-3245b535fbe9] request scope bean close:hello.core.common.MyLogger@5650b334
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$d7393e42
CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
@Scope의 proxyMode = ScopedProxyMode.TARGET_CLASS 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
MyLogger 클래스가 아니라 EnhancerBySpringCGLIB이라는 클래스로 만들어진 객체가 대신 등록된다.
그리고 getBean을 하여서 MyLogger를 찾아도 프록시 객체가 조회된다.
따라서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.
즉,
클라이언트A ---> MyLoggerProxy(프록시 객체) ---> 진짜 myLogger.logic() 호출 => request scope (A전용) x03
클라이언트B ---> MyLoggerProxy(프록시 객체) ---> 진짜 myLogger.logic() 호출 => request scope (B 전용) x04
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임로직이 들어있다.
즉, 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게 동일하게 사용할 수 있다.(다형성)
정리
- CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입한다.
- 가짜 프록시 객체는 실제요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 있다.
- 가짜 프록시 객체는 request scope와는 관계가 없다. 가짜이고, 내부에 위임로직만있다. 싱글톤 처럼 동작한다.
- Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진자 객체 조회를 꼭 필요한 시점까지 지연처리한다는 것!
- 단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 다형성과 DI컨테이너가 가진 강점이다.
- 꼭 웹 스코프가 아니여도 프록시를 사용할 수 있다.