[Spring] 스프링 핵심 원리 이해
여기서 할건 스프링 안쓰고 순수 자바로 할 거
동작 확인
기본 메인 클래스 실행( CoreApplication.main() ) IntelliJ Gradle 대신에 자바 직접 실행 최근 IntelliJ 버전은 Gradle을 통해서 실행 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다. 다 음과 같이 변경하면 자바로 바로 실행해서 실행속도가 더 빠르다. Preferences Build, Execution, Deployment Build Tools Gradle Build and run using: Gradle IntelliJ IDEA Run tests using: Gradle IntelliJ IDEA
비즈니스 요구사항과 설계
회원
회원을 가입하고 조회할 수 있다.
회원은 일반과 VIP 두 가지 등급이 있다.
회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
주문과 할인 정책
회원은 상품을 주문할 수 있다.
회원 등급에 따라 할인 정책을 적용할 수 있다.
할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있
다.)
할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을
미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
요구사항을 보면 회원 데이터, 할인 정책 같은 부분은 지금 결정하기 어려운 부분이다. 그렇다고 이런 정책이
결정될 때 까지 개발을 무기한 기다릴 수 도 없다. 우리는 앞에서 배운 객체 지향 설계 방법이 있지 않은가!
인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하면 된다. 그럼 시작해보자.
회원 도메인 설계
회원 도메인 요구사항 회원을 가입하고 조회할 수 있다. 회원은 일반과 VIP 두 가지 등급이 있다. 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정),
화면 단축키 쓸떄 맥이랑 윈도우 헷갈리면 여기 keymap에 가서 보면 옆에 단축키가 뭐인지 나온다.
그리고 중간에 뭐 안되면 alt+enter 눌러보자.
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); //꺼내온다.
}
}
근데 이렇게 테스트 하면 너무 많다. 일일이 다 해야되기 떄문 그래서 Junit5를 사용하게 된다.
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join(){
//given(이런이런게 주어졌을때)
Member member = new Member(1L, "memberA", Grade.VIP );
//when(이럴떄)
memberService.join(member);
Member findMember = memberService.findMember(1L);
//then(이렇게 된다.)
Assertions.assertThat(member).isEqualTo(findMember);
}
}
이렇게 테스트 코드를 작성하고 멤버가 같으면(찾으면) success를 띄운다.
MemberRepository는 인터페이스 Impl의존하는데 오른쪽 MemoryMemberRepository는 구현체(실제 할당하는거) 의존.
Impl이 둘다 의존. 추상화에도 의존하고 구현체에도 의존하고 있다.
회원 도메인 설계의 문제점 이 코드의 설계상 문제점은 무엇일까요? 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까요? DIP를 잘 지키고 있을까요? 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음 주문까지 만들고나서 문제점과 해결 방안을 설명
주문과 할인 도메인 설계
주문과 할인 정책 회원은 상품을 주문할 수 있다. 회원 등급에 따라 할인 정책을 적용할 수 있다. 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있 다.) 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
- 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
- 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회 한다.
- 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
- 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
참고: 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해 질 수 있어서 생략하고, 단순히 주문 결과를 반환한다.
새로운 할인 정책 개발 새로운 할인 정책을 확장해보자. 악덕 기획자: 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액 당 할인하는 정률% 할인으로 변경하고 싶어요. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 거에요! 순진 개발자: 제가 처음부터 고정 금액 할인은 아니라고 했잖아요. 악덕 기획자: 애자일 소프트웨어 개발 선언 몰라요? “계획을 따르기보다 변화에 대응하기를” 순진 개발자: … (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)
참고: 애자일 소프트웨어 개발 선언 https://agilemanifesto.org/iso/ko/manifesto.html 순진 개발자가 정말 객체지향 설계 원칙을 잘 준수 했는지 확인해보자. 이번에는 주문한 금액의 %를 할인해 주는 새로운 정률 할인 정책을 추가하자.
윈도우에서 ctrl+shift+t로 여기서 Junit5 테스트를 하고 뒤에 test를 붙여준다.
인터페이스, 구체적인거도 의존하고 다의존하면 망한거(DIP위반)
잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다! DIP 위반
정책 변경
중요!: 그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! OCP 위반
기름차에서 전기차로 바꿨다고 라이센스 갱신하지 않지만 이 경우 라이센스를 바꿔야 할 경우가 된거임.
어떻게 문제를 해결할 수 있을까? 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다. 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다. DIP 위반 추상에만 의존하도록 변경(인터페이스에만 의존) DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
인터페이스에만 의존하도록 설계를 변경하자
AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.
철저히 인터페이스에 의존 =DIP 지키고 있다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다. MemberServiceImpl MemoryMemberRepository OrderServiceImpl FixDiscountPolicy AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다. MemberServiceImpl MemoryMemberRepository OrderServiceImpl MemoryMemberRepository , FixDiscountPolicy
참고: 지금은 각 클래스에 생성자가 없어서 컴파일 오류가 발생한다. 바로 다음에 코드에서 생성자를 만든 다.
MemberServiceImpl - 생성자 주입
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
이 내용들이 엄청 중요하다! 설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
단지 MemberRepository 인터페이스만 의존한다. MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정 된다. MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
new MemoryMemberRepository() 이 부분이 중복 제거되었다. 이제 MemoryMemberRepository 를 다 른 구현체로 변경할 때 한 부분만 변경하면 된다. AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
새로운 구조와 할인 정책 적용
처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자. FixDiscountPolicy RateDiscountPolicy 어떤 부분만 변경하면 되겠는가? AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다. 그림 - 사용, 구성의 분리
AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy RateDiscountPolicy 객 체로 변경했다. 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 클라이언 트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다. 구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.
지금까지의 흐름을 정리해보자. 새로운 할인 정책 개발 새로운 할인 정책 적용과 문제점 관심사의 분리 AppConfig 리팩터링 새로운 구조와 할인 정책 적용 새로운 할인 정책 개발 다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것 자체는 아무 문제가 없음 새로운 할인 정책 적용과 문제점 새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야함 주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy 도 함께 의존 DIP 위반 관심사의 분리 애플리케이션을 하나의 공연으로 생각 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고, 실행함 비유를 하면 기존에는 남자 주인공 배우가 공연도 하고, 동시에 여자 주인공도 직접 초빙하는 다양한 책임을 가지고 있음 공연을 구성하고, 담당 배우를 섭외하고, 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점 공연 기획자인 AppConfig가 등장 AppConfig는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임 이제부터 클라이언트 객체는 자신의 역할을 실행하는 것만 집중, 권한이 줄어듬(책임이 명확해짐)
좋은 객체 지향 설계의 5가지 원칙의 적용
여기서 3가지 SRP, DIP, OCP 적용
SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다. 클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있음 SRP 단일 책임 원칙을 따르면서 관심사를 분리함 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당 클라이언트 객체는 실행하는 책임만 담당 DIP 의존관계 역전 원칙 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다. 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다. 왜냐하면 기존 클라 이언트 코드( OrderServiceImpl )는 DIP를 지키며 DiscountPolicy 추상화 인터페이스에 의존하는 것 같았지만, FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했다. 클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경했다. 하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없다. AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드 에 의존관계를 주입했다. 이렇게해서 DIP 원칙을 따르면서 문제도 해결했다.
OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다 다형성 사용하고 클라이언트가 DIP를 지킴 애플리케이션을 사용 영역과 구성 영역으로 나눔 AppConfig가 의존관계를 FixDiscountPolicy RateDiscountPolicy 로 변경해서 클라이언트 코 드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨 소프트웨어 요소를 새롭게 확장해도 사용 역영의 변경은 닫혀 있다!
IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한 마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다. 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제 어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호 출하지만 어떤 구현 객체들이 실행될지 모른다. 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl 도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService 인터페 이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
프레임워크 vs 라이브러리
프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)
반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다. 의존관계 주입 DI(Dependency Injection) OrderServiceImpl 은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모 른다. 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리 해서 생각해야 한다.
정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션 을 실행하지 않아도 분석할 수 있다. 클래스 다이어그램을 보자 OrderServiceImpl 은 MemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl 에 주입 될지 알 수 없다.
동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
객체 다이어그램
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언 트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다. 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다. 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스 를 변경할 수 있다. 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽 게 변경할 수 있다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다. 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
@Bean을 넣으면 스프링 컨테이너에 등록이 된다.
ApplicationContext 를 스프링 컨테이너라 한다. ApplicationContext 는 인터페이스이다. 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다. 직전에 AppConfig 를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것 이다. 자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext )를 만들어보자. new AnnotationConfigApplicationContext(AppConfig.class); 이 클래스는 ApplicationContext 인터페이스의 구현체이다.
참고: 더 정확히는 스프링 컨테이너를 부를 때 BeanFactory , ApplicationContext 로 구분해서 이야기 한다. 이 부분은 뒤에서 설명하겠다. BeanFactory 를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext 를 스프링 컨테이너라 한다.
이제부터 스프링 빈으로 컨테이너에 넣어서 관리하게 된다.
스프링 컨테이너의 생성 과정
- 스프링 컨테이너 생성
new AnnotationConfigApplicationContext(AppConfig.class) 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다. 여기서는 AppConfig.class 를 구성 정보로 지정했다.
- 스프링 빈 등록
스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다. 빈 이름 빈 이름은 메서드 이름을 사용한다. 빈 이름을 직접 부여할 수 도 있다. @Bean(name=”memberService2”)
주의: 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.
- 스프링 빈 의존관계 설정 - 준비
- 스프링 빈 의존관계 설정 - 완료
스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다. 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. 이 차이는 뒤에 싱글톤 컨테이너에서 설명한다. 참고 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다. 그런데 이렇게 자바 코드로 스프링 빈 을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. 여기서는 이해를 돕기 위해 개념적으 로 나누어 설명했다. 자세한 내용은 의존관계 자동 주입에서 다시 설명하겠다. 정리 스프링 컨테이너를 생성하고, 설정(구성) 정보를 참고해서 스프링 빈도 등록하고, 의존관계도 설정했다. 이제 스프링 컨테이너에서 데이터를 조회해보자.
컨테이너에 등록된 모든 빈 조회
스프링 컨테이너에 실제 스프링 빈들이 잘 등록 되었는지 확인해보자.
모든 빈 출력하기
실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다. ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다. ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다. 애플리케이션 빈 출력하기 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력해보자. 스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다. ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈 ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈 스프링 빈 조회 - 기본 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법 ac.getBean(빈이름, 타입) ac.getBean(타입) 조회 대상 스프링 빈이 없으면 예외 발생 NoSuchBeanDefinitionException: No bean named ‘xxxxx’ available
스프링 빈 조회 - 상속 관계
부모 타입으로 조회하면, 자식 타입도 함께 조회한다. 그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
BeanFactory와 AppilicationContext
Bean팩토리에 우리가 사용했던 기능이 다 들어있다.(getBean이라던지) Appilcation은 Bean팩토리의 기능을 모두 상속받아서 제공한다.
BeanFactory 스프링 컨테이너의 최상위 인터페이스다. 스프링 빈을 관리하고 조회하는 역할을 담당한다. getBean() 을 제공한다. 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다
ApplicationContext BeanFactory 기능을 모두 상속받아서 제공한다. 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이가 뭘까? 애플리케이션을 개발할 때는 빈은 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.
그럼 BeanFactory와 ApplicationContext의 차이는 뭐지?
메시지소스를 활용한 국제화 기능
예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
환경변수
로컬, 개발, 운영등을 구분해서 처리
애플리케이션 이벤트
이벤트를 발행하고 구독하는 모델을 편리하게 지원
편리한 리소스 조회
파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
정리
ApplicationContext는 BeanFactory의 기능을 상속받는다. ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다. BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다. BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.
다양한 설정 형식 지원
- 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
- 자바 코드, XML, Groovy 등등
애노테이션 기반 자바 코드 설정 사용 지금까지 했던 것이다. new AnnotationConfigApplicationContext(AppConfig.class) AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다. XML 설정 사용 최근에는 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는다. 아직 많은 레거시 프로젝 트 들이 XML로 되어 있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므 로 한번쯤 배워두는 것도 괜찮다. GenericXmlApplictionContext 를 사용하면서 xml 설정 파일을 넘기면 된다
스프링 빈 설정 메타 정보 - BeanDefinition
스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까? 그 중심에는 BeanDefinition 이라는 추상화 가 있다. 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것이다! XML을 읽어서 BeanDefinition을 만들면 된다. 자바 코드를 읽어서 BeanDefinition을 만들면 된다. 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다. BeanDefinition 을 빈 설정 메타정보라 한다. @Bean ,
코드 레벨로 조금 더 깊이 있게 들어가보자.
AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다. GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성한다. 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성 하면 된다.
BeanDefinition 살펴보기
BeanDefinition 정보 BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음) factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService Scope: 싱글톤(기본값) lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부 InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명 DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명 Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할 의 빈을 사용하면 없음)
정리
BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다. 하지만 실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다. 어려우면 그냥 넘어가면 된다^^! BeanDefinition에 대해서는 너무 깊이있게 이해하기 보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다. 가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼 때, BeanDefinition 이라는 것이 보일 때가 있다. 이때 이러한 메커니즘을 떠올리면 된다.
싱글톤 컨테이너
- 웹 애플리케이션과 싱글톤 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발 할 수 있다. 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
싱글톤 패턴
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다. 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
- private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.
private으로 new 키워드를 막아두었다. 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.
참고: 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안 전한 방법을 선택했다. 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유 해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다. 싱글톤 패턴 문제점 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다. 테스트하기 어렵다. 내부 속성을 변경하거나 초기화 하기 어렵다. private 생성자로 자식 클래스를 만들기 어렵다. 결론적으로 유연성이 떨어진다. 안티패턴으로 불리기도 한다.
싱글턴 컨테이너
싱글톤 컨테이너 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한 다. 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다. 싱글톤 컨테이너 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다. 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다. 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다. 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다. 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다. DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.
스프링 컨테이너를 사용하는 테스트 코드
@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);
//참조값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
싱글톤 컨테이너 적용 후
덤으로 스프링은 99퍼센트 싱글턴 방식을 쓴다.
스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유 해서 효율적으로 재사용할 수 있다.
참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새 로운 객체를 생성해서 반환하는 기능도 제공한다. 자세한 내용은 뒤에 빈 스코프에서 설명하겠다.
###싱글톤 방식의 주의점
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱 글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지 (stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다! 특정 클라이언트에 의존적인 필드가 있으면 안된다. 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다! 가급적 읽기만 가능해야 한다. 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다. 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!
package hello.core.singleton;
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);
StatefulService statefulService1 = ac.getBean("statefulService",
StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService",
StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
@Configuration과 싱글톤
@Configuration은 싱글턴을 위해 존재한다.
이렇게 호출이 되어야 할 것 같은데예상한 대로 안 되고 아래처럼 나온다 (3번 호출이 아니라 1번 호출) => 스프링은 어떻게 해서든 싱글턴을 보장하려고 해준다.
@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.getClass());
//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
사실 AnnotationConfigApplicationContext 에 파라미터로 넘긴 값은 스프링 빈으로 등록된다. 그래서 AppConfig 도 스프링 빈이 된다. AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해보자. bean = class hello.core.AppConfig\(EnhancerBySpringCGLIB\)bd479d70 순수한 클래스라면 다음과 같이 출력되어야 한다. class hello.core.AppConfig 그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스 를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작 성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.) AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성 해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 덕분에 싱글톤이 보장되는 것이다.
@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까? @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?
//@Configuration 삭제
public class AppConfig {
}
이제 똑같이 실행해보자.
bean = class hello.core.AppConfig
이 출력 결과를 통해서 AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
이 출력 결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있다. 1번은 @Bean에 의해 스 프링 컨테이너에 등록하기 위해서이고, 2번은 각각 memberRepository() 를 호출하면서 발생한 코드다. 인스턴스가 같은지 테스트 결과
memberService -> memberRepository =
hello.core.member.MemoryMemberRepository@6239aba6
orderService -> memberRepository =
hello.core.member.MemoryMemberRepository@3e6104fc
memberRepository = hello.core.member.MemoryMemberRepository@12359a82
당연히 인스턴스가 같은지 테스트 하는 코드도 실패하고, 각각 다 다른 MemoryMemberRepository 인 스턴스를 가지고 있다.
확인이 끝났으면 @Configuration이 동작하도록 다시 돌려놓자. 정리 @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다. memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다. 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.
컴포넌트 스캔
컴포넌트 스캔과 의존관계 자동 주입 시작하기
지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한 다.
또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.
1. @ComponentScan
@ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다. 빈 이름 기본 전략: MemberServiceImpl 클래스 memberServiceImpl 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component(“memberService2”) 이런식으로 이름을 부여하면 된다.
2. @Autowired 의존관계 자동 주입
생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다. getBean(MemberRepository.class) 와 동일하다고 이해하면 된다. 더 자세한 내용은 뒤에서 설명한다
생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.
탐색 위치와 기본 스캔 대상
탐색할 패키지의 시작 위치 지정 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다. @ComponentScan( basePackages = “hello.core”, } basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한 다. basePackages = {“hello.core”, “hello.service”} 이렇게 여러 시작 위치를 지정할 수도 있다. basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다. 만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장하는 방법 개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상 단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다. 예를 들어서 프로젝트가 다음과 같이 구조가 되어 있으면 com.hello com.hello.serivce com.hello.repository com.hello 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략한다.
이렇게 하면 com.hello 를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다.
그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다 생각한 다.
참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝 트 시작 루트 위치에 두는 것이 관례이다. (그리고 이 설정안에 바로 @ComponentScan 이 들어있다!)
includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다. excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않 는다.
FilterType 옵션
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
- 예를 들어서 BeanA도 빼고 싶으면 다음과 같이 추가하면 된다. @ComponentScan( includeFilters = { @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), }, excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class), @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class) } )
참고: @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다. 특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.
중복 등록과 충돌
- 컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
다음 두가지 상황이 있다.
- 자동 빈 등록 vs 자동 빈 등록
- 수동 빈 등록 vs 자동 빈 등록
자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다. ConflictingBeanDefinitionException 예외 발생
수동 빈 등록 vs 자동 빈 등록
만약 수동 빈 등록과 자동 빈 등록에서 빈 이름이 충돌되면 어떻게 될까?
의존관계 자동 주입
다양한 의존관계 주입 방법 의존관계 주입은 크게 4가지 방법이 있다. 생성자 주입 수정자 주입(setter 주입) 필드 주입 일반 메서드 주입 생성자 주입 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다. 지금까지 우리가 진행했던 방법이 바로 생성자 주입이다. 특징 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 불변, 필수 의존관계에 사용
생성자 주입을 선택해라!
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유는 다음과 같다. 불변 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.) 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다. 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다. 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.