스프링 핵심 원리 - 기본편 복습 1편

voidmelody·2022년 8월 15일
0

객체지향과 스프링

출처 : https://inf.run/6Mn7

  • 스프링 프레임워크
    핵심 기술 : 스프링 DI(의존성주입)컨테이너, AOP, 이벤트
    웹 기술 : 스프링MVC, 스프링WebFlux
    데이터 접근 기술 : 트랜잭션, JDBC, ORM 지원
    기술 통합 : 캐시, 이메일, 원격접근, 스케줄링
    테스트 : 스프링 기반 테스트

  • 스프링 부트
    스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
    단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
    Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않음
    손 쉬운 빌드 구성을 위한 starter 종속성 제공
    스프링과 외부 라이브러리 자동 구성

스프링의 진짜 핵심

스프링은 자바 언어 기반의 프레임워크
자바 언어의 가장 큰 특징은 객체 지향 언어라는 점이다.
즉, 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있도록 도와주는 프레임워크이다.
객체지향은 우리가 자바 언어를 처음 배울 때 알다시피

  • 추상화
  • 캡슐화
  • 상속
  • 다형성이 있다.

우린 그 중 다형성에 집중해보려한다.
객체 지향 프로그래밍의 정의를 간단히 보면

'객체들의 모임, 각각의 객체는 메시지를 주고 받고 데이터를 처리할 수 있다. 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만든다.'

여기서 유연하고, 변경이 용이하다는 의미는 무엇일까?
레고 블럭을 조립하듯이
부품을 갈아 끼우듯이 컴포넌트를 쉽고 유연하게 변경하면서 개발하는 것을 말한다.

다형성을 실세계로 비유한다면,
역할구현으로 구분할 수 있다.

운전자를 클라이언트라 가정하자.
자동자의 역할은 인터페이스가 담당한다.
우린 자동자를 인터페이스로 틀만 잡아 놓는다면
추후에 k3을 타다가 다른 자동차로 바꿀 때 구현 클래스만 바꿔주면 클라이언트에서 수정할 필요없이 기능을 변경할 수 있다.

마찬가지로, 로미오 역할과 줄리엣 역할은 누가 와도 상관이 없다. 장동건, 원빈, 김태희 등은 구현 클래스에 불과하고 실제 기본 틀은 로미오, 쥴리엣 역할, 즉 인터페이스이다.

역할과 구현을 분리하는 것은 매우 중요하다.
역할과 구현을 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해진다.
이렇게 되면

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다. 구현 클래스가 뭐가 있는지 알 필요가 없는 것이다.
  • 구현 대상의 내부구조를 몰라도 되고 변경되어도 영향을 받지 않는다. 심지어 구현 대상 자체를 변경해도 영향을 받지 않는다.

자바에서는 이걸 다형성을 활용해서 구현했다.

  • 역할 = 인터페이스
  • 구현 = 인터페이스를 구현한 클래스, 구현 객체

객체를 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만들어주면 된다.


MemberRepository라는 인터페이스 안에 구현 클래스를 갈아끼우는 것은 오버라이딩을 활용해 구현가능하다.
코드로 보자.

public class MemberService{
	//private MemberRepository memberRepository = new MemoryMemberRepository();
    private MemberRepository memberRepository = new JdbcMemberRepository();

MemoryMemberRepository를 쓰고 있다가 JdbcMemberRepository로 바꾸는 것은 말 그대로 인터페이스에 구현 클래스를 바꿔주면 된다.

물론 역할과 구현을 분리하는 방법의 경우 단점도 존재한다.
인터페이스, 즉 역할 자체가 변경된다면 그걸 따르는 클라이언트와 서버 모두 큰 변경이 발생한다. 그렇기에 인터페이스를 신중히 써야한다.

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

  • SRP : 단일 책임 원칙(Single Responsibility principle)
    한 클래스는 하나의 책임만 가져야한다.
    만약 한 클래스가 여러 기능과 책임을 가진다면 변경할 때 큰 파급효과를 불러올 것이다.
    그렇기에, 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것이다.
  • OCP : 개방 폐쇄 원칙(Open/Closed principle)
    소프트웨어는 확장에는 열려 있으나, 변경에는 닫혀 있어야한다. 어렵게 생각하지 말자.
    다형성을 활용해서 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하는 걸 생각하면 된다.
    하지만 여기서 문제점이 발생한다.
public class MemberService{
	//private MemberRepository memberRepository = new MemoryMemberRepository();
    private MemberRepository memberRepository = new JdbcMemberRepository();

이 코드를 다시 보자.
구현 객체를 변경하려면 클라이언트 코드를 변경해야한다.
분명 다형성을 활용했지만, OCP 원칙을 지킬 수가 없다.
이걸 해결하려면, 인터페이스만 냅두고 다른 곳에서 구현 객체를 인터페이스에 넣어주는(연관관계를 맺어주는) 제 3자가 필요하다.

  • ISP 인터페이스 분리 원칙(Interface segregation principle)
    특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
    여러 개로 나눌수록, 인터페이스가 명확해지고, 대체 가능성이 높아진다.
  • DIP 의존관계 역전 원칙(Dependency inversion principle)
    '추상화에 의존해야지, 구체화에 의존해서는 안된다.'
    즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존해야한다. 즉, 역할에 의존해야하는 것이다.
    하지만 방금의 코드처럼 MemberService는 인터페이스에 의존하지만 동시에 구현 클래스에도 의존한다.
    그렇기에 문제가 발생한다.

결론은, 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다. 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경되기 때문이다.
이를 해결하기 위해 나온 것이 스프링(DI 컨테이너)이다.

스프링 핵심 원리 이해 - 예제 만들기

비즈니스 요구사항 & 설계

  • 회원
    • 회원 가입, 조회
    • 회원 등급 : 일반, VIP
    • 회원 데이터 : 자체 DB 구축 OR 외부시스템연동 (미정)
  • 주문 할인 정책
    • 회원은 상품을 주문 가능
    • 회원 등급에 따라 할인 정책 적용
    • 할인 정책의 경우 VIP는 1000원 할인하는 고정 금액 할인(추후 변경 가능 -> 인터페이스)

보다시피, 회원 데이터나 할인 정책의 경우 바로 결정하기가 힘들다.
그렇기에 인터페이스를 만들고, 구현체를 언제든지 갈아끼울 수 있게 설계해보자.

  • 회원 도메인 협력관계 : 기획자도 볼 수 있는 그림

  • 회원 클래스 다이어그램 : 도메인 협력 관계를 토대로 개발자가 만든 그림.
    실제 서버를 실행하지 않고 클래스를 볼 수 있음

  • 회원 객체 다이어그램 : 객체의 경우 서버를 올리면서 동적으로 결정.

  • 회원 등급

public enum Grade{
	BASIC,
    VIP
}
  • 회원 Entity
public class Member{
	private Long id;
    private String name;
    private Grade grade;
    
    //생성자, Getter & Setter 생략
}
  • 회원 저장소 인터페이스
public interface MemberRepository{

	void save(Member member);
    
	Member findById(Long memberId);
}
  • 메모리 회원 저장소 구현체
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로 HashMap을 활용했는데, 동시성 이슈로 인해서 실무에선 ConcurrentHashMap을 주로 활용한다.

  • 회원 서비스 인터페이스
public interface MemberService{
	
    // 가입
    void join(Member member);
    
    // 조회
    Member findMember(Long memberId);
}
  • 회원 서비스 구현체
public class MemberServiceImpl implements MemberService{
	
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    public void join(Member member){
    	memberRepository.save(member);
    }
    
    public Member findMember(Long MemberId){
    	return memberRepository.findById(memberId);
    }
}

여기서 문제가 생긴다.
MemberRepository memberRepository = new MemoryMemberRepository();
이렇게 의존관계가 인터페이스뿐만이 아니라 구현까지 모두 의존하는 문제점이 발생하고 있다.

어떻게 해결해야할까? 입문편에서 의존관계 주입을 배웠다 우린.
해답은 예제만들기2에서 자세히 설명..!

###주문과 할인 도메인 설계

주문 서비스를 할 때 회원 조회를 통해서 회원 등급을 가져와야하고
그 등급을 활용해서 또 할인 정책의 적용 유무를 판단해야한다.

역할과 구현을 따로 분리했다. 수정해야할 때 구현체만 바꿔서 끼워주면 된다.


OrderService의 구현체가 저장소와 할인정책의 인터페이스를 가져다가 쓰면 저장소와 할인 정책이 변경되어도 주문서비스는 변경하지 않아도 된다. 이제 코드로 좀 더 쉽게 이해해보자.

  • 주문 서비스 인터페이스
public interface OrderService{
	Order createOrder(Long memberId, String itemName, int itemPrice);
}
  • 주문 서비스 구현체
public class OrderServiceImpl implements OrderService{
	private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountpolicy = new FixDiscountPolicy();
    
    @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);
    }
}

스프링 핵심 원리 이해 - 객체 지향 원리 적용

public class OrderServiceImpl implements OrderService{
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

다형성을 활용해서 인터페이스와 구현 객체를 분리했다.
하지만 OCP, DIP 같은 객체지향 원칙을 준수하지 못했다.
왜 그럴까?
DIP : 주문서비스 클라이언트(OrderServiceImpl)가 추상 인터페이스인 DiscountPolicy뿐만이 아니라 구체(구현)클래스(FixDiscountPolicy)에도 의존하고 있다.
OCP : 현재 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 줘서 위반이 된다.

우리가 기대했던 의존 관계는

단순히 DiscountPolicy 인터페이스에만 의존한다 생각했다.

하지만 실제 의존관계는

클라이언트인 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만이 아니라 구현체인 FixDiscountPolicy에도 의존하고 있다.
그래서 만약 FixDiscountPolicy를 RateDiscountPolicy로 변경하려하면 OrderServiceImpl의 코드도 변경해야 한다.

이걸 해결하려면 우리가 원하는 것처럼 OrderServiceImpl이 인터페이스인 DiscountPolicy에만 의존하도록 만들어야한다.

public class OrderServiceImpl implements OrderService{
	//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private DiscountPolicy discountpolicy;

이렇게 인터페이스에만 의존하도록 코드를 변경했다.
하지만 구현체가 없기 때문에 실행하면 NullPointerException이 발생할 것이다.
이 문제를 해결하려면, 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 넣어줘야한다.

관심사의 분리 (AppConfig)

애플리케이션을 하나의 공연이라 가정해보자.
각각의 인터페이스를 배우 역할이라 생각해보자.
그런데 실제 배역에 맞는 배우를 선택하는 것은 누가 할까?

로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는 배우들이 정하는 게 아니다.
아까의 코드는 마치 로미오 역할(인터페이스)을 하는 배우A(구현체)가 줄리엣 역할(인터페이스)을 하는 배우B(구현체)를 직접 초대하는 것이랑 같다.
현재 배우A는 공연도 해야하고 배우B도 초대해야하는 다양한 책임을 가지고 있다. 이는 좋지 못하다.

관심사를 분리하자
이게 핵심이다.
배우는 본인의 역할인 배역에만 몰입해야한다.
배우A는 줄리엣 역할에 누가 오더라도 똑같이 공연을 할 줄 알아야 한다.
공연을 구성하고, 배우들을 섭외하고, 배우를 지정하는 것은 배우가 아닌 기획자가 해야한다.
기획자를 만들고, 배우기획자의 책임을 확실히 분리해보자.

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 별도의 클래스를 만들어보자.
public class AppConfig{
	
    public MemberService memberService(){
    	return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    public OrderService orderService(){
    	return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

AppConfig는 구현 객체를 생성해서 생성한 객체를 생성자를 통해서 주입 시켜주고 있다.
물론 생성자를 통해 주입하니, 기존의 코드들에서 생성자를 수정해줘야한다.

public class MemberServiceImpl implements MemberService{
	
    private final MemberRepository memberRepository;
    
    public MemberServiceImpl(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }

이제 MemberServiceImpl에서 구현객체를 넣어줄 필요가 없다.
단지 MemberRepository 인터페이스에만 의존한다.
MemberServiceImpl 입장에서는 생성자를 통해 어떤 구현 객체가 들어올지는 모르지만 실행하는데에는 전혀 문제가 없다.
생성자를 통해 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다. 물론 지금의 코드에서는 불완전한다.
실제로는 AppConfig객체를 만들어서 실행해야겠지.

public class OrderServiceImpl implements OrderService{
	private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

OrderServiceImpl도 위의 코드와 마찬가지로 인터페이스에만 의존하고 구현 객체는 AppConfig가 주입해줄 것이다.

이제 AppConfig를 실행해보자.

public class MemberApp{
	
    public static void main(String[] args){
    	AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        ....
   	}
}
public class OrderApp{
	public static void main(String[] args){
    	AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
  	}
}

흐름 정리

  • 새로운 할인 정책 개발
    다형성 덕분에 새로운 할인 정책을 개발하고 갈아 끼우는 것 자체는 문제가 없었음.

  • -문제점
    하지만 새로 개발한 할인 정책을 적용하려고 하니 클라이언트 코드인 서비스 구현체도 함께 변경해야한다.
    주문 서비스 클라이언트가 인터페이스(DiscountPolicy)뿐만 아니라 구현체인 (FixDiscountPolicy)도 함께 의존해야 한다. - DIP 위반
    그렇기에, 구현체에 의존하는 것을 막아야한다.

  • 관심사의 분리
    기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고 실행했다.
    해당 공연을 구성하고, 배우를 섭외하고, 지정하는 책임을 담당하는 별도의 기획자가 필요하다.
    그렇기에 기획자 역할을 하는 AppConfig를 만들었다.
    AppConfig는 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결해주는 역할을 했다.
    이렇다보니, 이제 클라이언트 객체는 자신의 역할만 집중하면 된다.

SRP , DIP, OCP

SRP (Single Responsibility Principle)

한 클래스는 하나의 책임만 가져야 한다.

아까의 클라이언트 객체는 직접 구현 객체를 생성하고 연결하고 실행하는 다양한 책임을 가지고 있었다.
SRP 단일 책임 원칙을 따르면서 관심사를 분리했다.
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당함으로써, 클라이언트 객체는 실행하는 책임만 담당하게 되었다.

DIP (Dependency Inversion Principle)

자신보다 변하기 쉬운 것에 의존하지 마라.

추상화에 의존해야지, 구체화에 의존하면 안된다.

클라이언트 코드가 추상화 인터페이스(DiscountPolicy)에만 의존하도록 코드를 변경했지만, 인터페이스만으로는 아무것도 실행할 수가 없다.
그렇기에 AppConfig가 객체 인스턴스를 대신 생성해서 클라이언트 코드에 의존관계를 주입해줬다.

OCP(Open-closed principle)

소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다.

애플리케이션을 사용 영역과 구성 영역으로 나누었다.
AppConfig가 의존관계를 변경해서 클라이언트 코드에 주입하므로 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.

IoC, DI, 컨테이너

제어의 역전(Inversion of Control)

기존 프로그램에서는 클라이언트 구현 객체가 스스로 서버 구현 객체를 생성하고, 연결하고, 실행했다.
구현 객체가 프로그램의 제어 흐름을 스스로 조종하기에, 개발자 입장에서는 자연스러운 흐름이다.

반면에 AppConfig가 등장한 후에는 구현 객체는 자신의 로직만을 실행한다. 제어 흐름은 이제 AppConfig가 가져간다.
즉, 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지는 모른다.

AppConfig처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라 한다.

의존관계 주입(DI, Dependency Injection)

의존 관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 이렇게 둘을 분리해서 생각해야 한다.

  • 정적인 클래스 의존관계
    클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 파악할 수 있다.

    해당 다이어그램을 보면
    OrderServiceImpl은 인터페이스인 MemberRepository, DiscountPolicy에 의존하고 있는 것을 알 수 있다.
    하지만 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 들어올지는 알 수 없다.

  • 동적인 객체 인스턴스 의존 관계
    애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다.

    애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고, 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
    의존관계 주입을 사용함으로써 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
    의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 DI 컨테이너라 한다.

스프링 전환하기

이제 순수한 자바 코드 대신에 스프링을 사용해보자

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

AppConfig에 설정을 구성한다는 뜻의 @Configuraiton을 붙여주었다.
또 각 메서드에 @Bean을 붙여줌으로써 스프링 컨테이너에 스프링 빈으로 등록했다.

이제 그러면 스프링 컨테이너를 적용해보자.

public class MemberApp {
    public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new
                AnnotationConfigApplicationContext(AppConfig.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("find Member = " + findMember.getName());
    }
}
public class OrderApp {
    public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
        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);
    }
}

아까 기존에는 AppConfig 라는 클래스를 만들어서 직접 객체를 생성하고 의존관계를 주입했지만, 이제는 스프링 컨테이너를 사용한다.

스프링 컨테이너는 @Configuration이 붙어있는 현재 예시로 든 AppConfig를 설정 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체들을 스프링 컨테이너에 등록한다.
이렇게 스프링 컨테이너에 등록된 객체들을 스프링 빈이라 한다.
스프링 빈은 @Bean이 붙은 메서드 이름을 스프링 빈의 이름으로 사용한다.

이전에는 AppConfig를 통해서 직접 조회했지만, 이제는 스프링 컨테이너를 통해서 필요한 스프링 빈을 찾아야한다. 스프링 빈은 applicationContext.getBean()을 통해 찾을 수 있다.
이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경했다.

스프링 컨테이너와 스프링 빈

스프링 컨테이너 생성

// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext스프링 컨테이너 라 한다.
ApplicationContext인터페이스 이다.

스프링 컨테이너 생성과정

  1. 스프링 컨테이너 생성
new AnnotationConfigApplicatioContext(AppConfig.class);

구성 정보로 여기선 AppConfig.class를 넘겨주었다.

  1. 스프링 빈 등록

    구성 정보로 넘겨온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
    빈 이름은 기본적으로 메서드 이름을 사용하는데, 빈 이름을 직접 부여할 수도 있다.
    @Bean(name = "memberService2") 이런 식으로.
    하지만 빈 이름은 항상 다른 이름을 부여해야 한다.
    같은 이름을 부여하게 되면, 다른 빈이 무시되거나 덮어버릴 수 있기 때문에..

  2. 스프링 빈 의존관계 설정

    스프링 컨테이너가 설정 정보를 참고해서 의존관계를 주입한다.
    지금 당장 보기엔 그저 자바 코드를 호출하는 것처럼 보이지만, 실제로는 차이가 있다. 지금은 싱글톤 때문에 다르다 정도로만 알고 싱글톤에서 추후 보충 설명하겠다.

스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 따로 나뉘어져 있다.
그런데 지금의 자바 코드를 보면 생성자를 호출하면서 동시에 의존관계 주입도 동시에 처리되기 때문에 그렇게 보일 수 있다.

  • 스프링 컨테이너 등록된 모든 빈 조회
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

@Test
void findAllBean(){
	String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for(String beanDefinitionName : beanDefinitionNames){
    	Object bean = ac.getBean(beanDefinitionName);
        
@Test
void findApplicationBean(){
	String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for(String beanDefinitionName : beanDefinitionNames){
    	BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
        //Role ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
        //Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
        if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
        	Object bean = ac.getBean(beanDefinitionName);
           
         }
    }
}
  • 스프링 빈 조회
class ApplicationContextBasicFindTest{
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    @Test
    //빈 이름으로 조회
    void findBeanByName(){
    	MemberService memberService = ac.getBean("memberService", memberService.class);
    }
    
    @Test
    //이름 없이 타입만으로 조회
    void findBeanByType(){
    	MemberService memberService = ac.getBean(MemberService.class);
    }
  • 스프링 빈 조회 - 동일한 타입이 둘 이상
class ApplicationContextSameBeanFindTest{
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Configuration
    static class SameBeanConfig{
    	@Bean
        public MemberRepository memberRepository1(){
        	return new MemoryMemberRepository();
        }
        
        @Bean
        public MemberRepository memberRepository2(){
        	return new MemoryMemberRepository();
        }
        
    @Test
    //타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류 발생
    void findBeanByTypeDuplicate(){
    	DiscountPolicy bean = ac.getBean(MemberRepository.class);
    }
    @Test
    //타입으로 조회 시 같으 타입이 둘 이상 있으면, 빈 이름을 지정해줌으로써 좀더 구체화 
    void findBeanByName(){
    	MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
    }
    
    @Test
    //특정 타입을 모두 조회하기
    void findAllBeanByType(){
    	Map<String,MemberRepository> beanOfType = ac.getBeansOfType(MemberRepository.class);
        for(String key : beansOfType.keySet()){
        	System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }   
  • 스프링 빈 조회 - 상속 관계
    부모타입으로 조회를 하면, 자식 타입도 함께 조회한다.
    그렇기에, 객체의 최고 부모인 Object타입으로 조회하면, 모든 스프링 빈을 조회한다.
class ApplicationContextExtendsFindTest{
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    
    @Configuration
    static class TestConfig{
    	@Bean 
        public DiscountPolicy rateDiscountPolicy(){
        	return new RateDiscountPolicy();
        }
        
        @Bean
        public DiscountPolicy fixDiscountPolicy(){
        	return new FixDiscountPolicy();
        }
     }
     @Test
     //부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류 발생
     void findBeanByParentTypeDuplicate(){
     	DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
     }
     
     @Test
     //부모 타입으로 조회시,자식이 둘 이상 있으면 빈 이름을 지정
     void findBeanByParentTypeBeanName(){
     	DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
     }
     
     @Test
     // 특정 하위 타입으로 조회
     void findBeanBySubtype(){
     	RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
     }
     
     @Test
     //부모 타입으로 모두 조회하기
     void findAllBeanParentType(){
     	Map<String,DiscountPolicy> beansOfType = ac.getBeanOfType(DiscountPolicy.class);
     }
     
     @Test
     //부모 타입으로 모두 조회하기 - Object
     void findAllBeanByObjectType(){
     	Map<String,Object> beansOfType = ac.getBeansOfType(Object.class);
        for(String key : beansOfType.keySet()){
        	System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
     }
}
     
  • BeanFactory 와 ApplicationContext

    BeanFactory는 스프링 컨테이너의 최상위 인터페이스이다.
    스프링 빈을 관리하고 조회하는 역할을 담당하며, getBean()을 제공한다.

ApplicationContext는 BeanFactory 기능을 모두 상속받아서 제공한다.
애플리케이션을 개발할 때에는 빈을 관리하고 조회하는 기능은 물론 수 많은 부가기능이 필요하다. 이를 ApplicationContext가 제공한다.
그래서 보통 BeanFactory를 직접 사용할 일은 거의 없다.
부가기능이 포함되어있는 ApplicationContext를 사용한다.
그리고 이 둘을 스프링 컨테이너라고 한다.

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

스프링은 자바 뿐만이 아니라, xml 등의 다른 설정 형식도 지원한다.
이러한 다양한 설정 형식을 지원할 수 있는 이유는 BeanDefinition이라는 추상화가 있다.
역할과 구현을 개념적으로 나눈 것이다.
XML을 읽어서 BeanDefinition을 만들거나
Java를 읽어서 BeanDefinition을 만들거나.
스프링 컨테이너는 BeanDefinition만 알면 된다.
이 BeanDefinition을 빈 설정 메타정보라고 한다.
@Bean 당 하나씩 메타 정보가 생성된다.

실무에서는 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다.
그렇기에, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.

profile
어제보다 오늘 더 나은 삶을 위해

0개의 댓글