스프링의 핵심
좋은 객체 지향 이란?
다형성이란 ?
// car라는 인터페이스 가 있고 bmw, porche라는 구현체가 있으면
public class CarService {
// 각각의 구현체의 인스턴스의 반환 타입이 car가 될 수 있다.
private car car1 = new bmw()
private car car2 = new porche()
}
오버라이딩
다형성의 본질
객체 지향의 5가지 원칙(SOLID)
SRP: 단일 책임 원칙(single responsibility principle)
OCP: 개방-폐쇄 원칙 (Open/closed principle)
LSP: 리스코프 치환 원칙 (Liskov substitution principle)
ISP: 인터페이스 분리 원칙 (Interface segregation principle)
DIP: 의존관계 역전 원칙 (Dependency inversion principle)
비즈니스 요구 사항
회원 도메인 설계


MemberService( interface ) ---> MemberServiceImpl( 구현체 )
MemberRepository( interface ) --> MemoryMemberRepository(구현체)
--> DbMemberRepository(구현체)
* 일단 메모리에 저장하는 방식으로 개발을 하고 추후에 변경 계획 *
회원 도메인 개발
public enum Grade {
BASIC,
VIP
}
package springcore.core.member;
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;
}
// 🥊 getter와 setter를 만든다.
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
// 회원 저장 기능과 id로 회원을 찾는 기능을 구현할 것이라고 명시하는 것
package springcore.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
package springcore.core.member;
import java.util.HashMap;
import java.util.Map;
//🥊 메모리에 데이터를 저장하는 구현체 이므로 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);
}
}
package springcore.core.member;
public interface MemberService {
// 회원 가입
void join(Member member);
// 회원 조회
Member findMember(Long memberId);
}
package springcore.core.member;
import java.nio.channels.MembershipKey;
public class MemberServiceImpl implements MemberService{
// 회원 가입, 찾기를 하려면 데이터 저장소가 필요하다.
// 따라서 MemberRepository라는 인터페이스를 따르는 것들 중
MemoryMemberRepository라는 구현체의 인스턴스 생성
//🥊 그리고 현재 MemberRepository라는 인터페이스를 반환 타입으로 하고 있지만
// 다형성에 의해서 MemoryMemberRepository에 @Override한 메서드가 호출이 될 것이다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 생성한 MemoryMemberRepository안의 메서드를 활용하여 저장하고 조회
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
순수 자바 코드로 회원 도메인 테스트
package springcore.core;
import springcore.core.member.Grade;
import springcore.core.member.Member;
import springcore.core.member.MemberService;
import springcore.core.member.MemberServiceImpl;
public class MemberApp {
public static void main(String[] args) {
//🎈 서비스를 가져 오고
MemberService memberService = new MemberServiceImpl();
//🎈 Member 인스턴스를 가져와
Member member = new Member(1L, "memberA", Grade.VIP);
//🎈 회원 가입을 실행해 본다.
memberService.join(member);
//🎈 회원 가입이 정상적으로 되면 등록이 되어 있을 것이므로 id값으로 찾아
본다.
Member findMember = memberService.findMember(1L);
System.out.println("find member = " + findMember.getName());
System.out.println("new member = " + member.getName());
}
}
junit 테스트 프레임워크를 통한 회원 도메인 테스트
package springcore.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import springcore.core.member.Grade;
import springcore.core.member.Member;
import springcore.core.member.MemberService;
import springcore.core.member.MemberServiceImpl;
import static org.junit.jupiter.api.Assertions.*;
// 🎈 테스트 라는 건 기능을 테스트 하는 것이므로 내부의 메서드에서
테스트 진행
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 이렇게 된다. import org.assertj.core.api.Assertions;를
import하면 Assertions.assertThat을 사용할 수 있음
//🎈 member와 findMember랑 같냐고 묻는 것
Assertions.assertThat(member).isEqualTo(findMember);
}
}
회원 도메인의 문제점
public class MemberServiceImpl implements MemberService{
//❗ MemberServiceImpl은 아래 보이는 것 처럼 MemberRepository라는 인터페이스
에도 의존하고 있고, MemoryMemberRepository라는 구현체에도 의존하고 있다.
따라서 추상화에만 의존해야 하고, 구체화에는 의존하면 안되는 DIP원칙을
지키지 못하는 것.
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
주문과 할인 도메인 설계
주문과 할인 도메인 개발
package springcore.core.discount;
import springcore.core.member.Member;
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
//❗ 정액 할인 구현체 이므로, 고정 할인 금액을 정하고 회원 등급에 따라 할인 금액을
리턴
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000;
// ❗enum타입은 == 으로 비교하는 것이 맞다.
// ❗회원이 vip면 할인 금액인 1000원을 리턴
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
// 주문을 할 때 memberId, itemName, itemPrice, discointPrice를 받는다.
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 Long getMemberId() {
return memberId;
}
public String getItemName() {
return itemName;
}
public int getItemPrice() {
return itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
// toString메서드를 넣으면 해당 객체를 출력할 때 아래와 같이 결과를 받을 수 있다.
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
public interface OrderService {
// 사용자가 주문을 하면 Order 객체를 반환하는 메소드
Order createOrder(Long memberId, String itemName, int itemPrice);
}
//🥊 OrderService를 보면 DisCountPolicy라는 인터페이스를 따르는 구현체 중에서
FixDiscountPolicy를 선택하여 사용하고 있다. 만약 할인 정책이 바뀌면
FixDiscointPolicy대신에 다른 구현체로 바꾸면 된다.
//🥊 createOrder 메서드 안에서는 가져온 할인 정책이 뭐 어떻게 구성되어 있는지
전혀 몰라도 되고 그냥 사용만 하면 된다. 이 부분은 설계가 잘 된 것으로 볼 수
있는데 그 이유는 단일 책임 원칙을 잘 지키고 있기 때문이다. 만약 할인 정책에
문제가 생긴다면 OrderServiceImpl의 코드는 수정할 필요가 없고, 그저
DiscountPolicy와 관련된 코드만 수정하면 되기 때문이다.
public class OrderServiceImpl implements OrderService {
// 서비스는 Repository랑 할인 정책이 필요하다.
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 OrderApp {
public static void main(String[] args) {
// 멤버랑, 오더 서비스를 만든다.
// ❗주문 테스트를 할려면 회원과 주문에 관한 것이 필요하므로
// ❗관련된 인터페이스를 가져오고, 구현체를 선택한 것.
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
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);
System.out.printLn("order.caculatePrice =" + order.calculatePrice());
}
}
order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
public class OrderServiceTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
새로운 할인 정책 개발
RateDiscountPolicy라는 새로운 구현체 생성
//🥊 아래의 interface를 따르는
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
//🥊 RateDiscountPolicy라는 구현체를 만든다.
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10; //10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
//🥊 테스트 코드 까지 만들어서 할인이 적용이 되는지 보자.
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
Member member = new Member(1L, "memberVIP", Grade.VIP);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(1000);
}
//🥊 @DisplayName은 해당 메서드로 하는 테스트의 이름을 그냥 지정할 수 있게 한다.
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(0);
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//🥊 기존의 new FixDiscountPolicy를 RateDiscountPolicy로 바꿔주면 끝난다.
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@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 MemberRepository memberRepository = new MemoryMemberRepository();
//🥊 클라이언트인 Service가 추상화 에만 의존 즉, interface에만 의존해야 하는데
아래를 보면 DiscountPolicy라는 interface와 FixDiscountPolicy()라는
구현체 에도 의존을 하고 있다. 따라서 FixDiscountPolicy를
RateDiscountPolicy로 코드를 수정해야 하기 때문에 Dip, Ocp 원칙을
위반하고 있는 것이다.
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@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 MemberRepository memberRepository = new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//🥊 아래 처럼 interface만을 의존하게 하였지만, 구현체가 없기 때문에
테스트 코드를 돌려 보면 당연히 에러가 발생한다. 그럼 어떻게 해야 할까?
private DiscountPolicy discountPolicy;
@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 MemberServiceImpl implements MemberService{
//🥊 여기서는 MemberRepository라는 interface만을 의존하고, 해당 interface를 따르는
구현체 중 어떤 것을 선택할지는 AppConfig에서 정한다.
private final MemberRepository memberRepository;
//🥊 생성자를 통해 들어오는 구현체를 의존하고 있던 인터페이스에 할당한다.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 생성한 MemoryMemberRepository안의 메서드를 활용하여 저장하고 조회
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
public class AppConfig {
//🥊 여기서 구현체를 선택하여 주입
MemberServiceImpl 클래스에 생성자를 생성해 두었으므로
아래와 같이 호출 할 때 전달한 구현체가 MemberServiceImpl에서
의존하고 있는 인터페이스에 할당 될 것이다.
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
public class OrderServiceImpl implements OrderService {
//🥊 추상화 에만 의존할 수 있게 interface만을 선택하고
private final MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//🥊 생성자를 통해 구현체를 주입 받는다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@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 AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
의존관계 주입 ( Dependency Injection )
AppConfig 실행
public class MemberApp {
public static void main(String[] args) {
//🥊 AppConfig를 만든다.
AppConfig appConfig = new AppConfig();
//🥊 AppConfig안의 memberService를 꺼낸다.
//🥊 여기에는 memberServiceImpl이 들어가 있을 것
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("find member = " + findMember.getName());
System.out.println("new member = " + member.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
// 멤버랑, 오더 서비스를 만든다.
// MemberService memberService = new MemberServiceImpl(memberRepository);
// OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
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);
}
}
테스트 코드도 수정해 주자.
MemberServiceTest
class MemberServiceTest {
MemberService memberService;
// @BeforeEach는 각각의 테스트 메서드 실행 전에 실행되게 하는 역할을 한다.
@BeforeEach
public void beforeEach () {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
//given 이런 환경이 주어졌을 때
Member member = new Member(1L, "memberA", Grade.VIP);
//when 이렇게 하면
memberService.join(member);
Member findMember = memberService.findMember(1L);
//then 이렇게 된다. import org.assertj.core.api.Assertions;를 import한것
// member와 findMember랑 같냐고 묻는 것
Assertions.assertThat(member).isEqualTo(findMember);
}
}
public class OrderServiceTest {
// MemberService memberService = new MemberServiceImpl(memberRepository);
// OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
MemberService memberService;
OrderService orderService;
// @BeforeEach는 각각의 테스트 메서드 실행 전에 실행되게 하는 역할을 한다.
@BeforeEach
public void beforeEach () {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
public class AppConfig {
//🥊 MemberService 역할이 메소드 명, 그것의 구현체가 리턴 값
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
//🥊 MemoryMemberRepository 역할이 메소드 명, 그것의 구현체가 리턴 값
private MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
//🥊 OrderService 역할이 메소드 명, 그것의 구현체가 리턴 값
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), new FixDiscountPolicy());
}
//🥊 DiscountPolicy 역할이 메소드 명, 그것의 구현체가 리턴 값
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
public class AppConfig {
// MemberService 역할
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
// MemoryMemberRepository 역할
private MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
// OrderService 역할
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), new FixDiscountPolicy());
}
// DiscountPolicy 역할
//🥊 기존의 FixDiscountPolicy를 RateDiscountPolicy로 바꾼 것
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
객체 지향 설계의 원칙 정리
지금까지 객체 지향의 원칙 중 SRP, DIP, OCP 원칙을 준수하여 개발 해왔다. 하나하나 다시 살펴 보자
SRP ( 단일 책임 원칙 )
IOC / DI / DI컨테이너
IOC
DI
IOC 컨테이너 / DI 컨테이너
스프링으로 전환
//🥊 먼저 Configuration 어노테이션을 붙여 준다. 설정 파일 이라는 의미
@Configuration
public class AppConfig {
//🥊 Bean 어노테이션을 붙여 주면 이것 들이 스프링 컨테이너에 등록이 된다.
// MemberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
// MemoryMemberRepository 역할
@Bean
public MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
// OrderService 역할
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), new FixDiscountPolicy());
}
// DiscountPolicy 역할
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
package springcore.core;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import springcore.core.member.Grade;
import springcore.core.member.Member;
import springcore.core.member.MemberService;
import springcore.core.member.MemberServiceImpl;
public class MemberApp {
public static void main(String[] args) {
//🥊 순수 자바로 되어 있던 것들을 일단 주석처리 한다.
// MemberService memberService = new MemberServiceImpl(memberRepository);
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
//🥊 ApplicationContext라는 것이 스프링 컨네이너 라고 보면 되고, 모든게 여기서 부터 시작 된다.
//🥊 아래와 같이 AppConfig클래스를 전달 하면 AppConfig에서 Bean으로 설정한 것들을 관리해 준다.
ApplicationContext applicationcontext = new AnnotationConfigApplicationContext(AppConfig.class);
//🥊 그럼 이제 기존에 AppConfig에서 직접 꺼내오던 (위의 주석처리 부분 참고) 메서드를 스프링 컨테이너에서 꺼내 오도록 해야 한다.
//🥊 MemberService를 꺼내와 보자.
//🥊 Bean으로 등록이 될 때 해당 메서드의 이름으로 등록이 된다. 따라서 getBean에 가져올 메서드 이름을 입력하고,
//🥊 두번째로 반환 타입을 입력한다.
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("find member = " + findMember.getName());
System.out.println("new member = " + member.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
// 멤버랑, 오더 서비스를 만든다.
// MemberService memberService = new MemberServiceImpl(memberRepository);
// OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
// 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);
}
}
스프링 컨테이너와 빈 저장소
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)

등록된 빈 확인하기
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean () {
//🥊 아래 메서드를 활용 하면 bean으로 정의 된 메서드 들을 객체 형태로 가져 올 수 있다.
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
//🥊 가져온 객체를 for문을 통해 하나하나 출력해 보자.
for (String beanDefinitionName: beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name =" + beanDefinitionName + " object = " + bean );
}
}
}
// 🥊 아래는 스프링에서 필요해서 자체적으로 가지고 있는 Bean말고, 순수 어플리케이션을 개발할 때 등록한 bean을
// 🥊 출력하는 코드
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean () {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName: beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//🥊 beanDefinition.getRole()은 3가지의 값이 있는데, 그 중 ROLE_APPLICATION은 스프링 내부에서 필요하여 알아서 등록하는 bean들이 아니라 애플리케이션을 개발하기 위해 등록한 bean들을 출력해 주는 것이라고 보면 된다.
//🥊 ROLE_INFRASTRUCTURE로 바꿔 보면 스프링 내부에서 알아서 등록한 bean들이 출력 될 것이다.
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name =" + beanDefinitionName + " object = " + bean );
}
}
}
다양한 상황에서의 빈 조회 방법
기본적인 빈 조회 방법
// 가져온 빈이 해당 클래스의 인스턴와 같은지 확인
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName () {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
@Test
@DisplayName("이름 없이 타입만으로 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2() {
MemberServiceImpl memberService = ac.getBean("memberService",
MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByNameX() {
//ac.getBean("xxxxx", MemberService.class);
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
ac.getBean("xxxxx", MemberService.class));
}
}
동일한 타입이 둘 이상일 경우 빈 조회
public class ApplicationContextSameBeanFindTest {
// 먼저 기존의 AppConfig는 중복된 빈이 없으므로 임시로 아래 처럼 중복된 빈이 있도록 Config클래스 설정
// 클래스 내부에 있는 특정 클래스는 부모 클래스 에서만 사용할 수 있다.
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
void findBeanByTypeDuplicate() {
//MemberRepository bean = ac.getBean(MemberRepository.class);
assertThrows(NoUniqueBeanDefinitionException.class, () ->
ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
void findBeanByName() {
MemberRepository memberRepository = ac.getBean("memberRepository1",
MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanByType() {
Map<String, MemberRepository> beansOfType =
ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " +
beansOfType.get(key));
}
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType.size()).isEqualTo(2);
}
}
상속 관계에 있는 빈 조회
public class ApplicationContextExtendsFindTest {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
void findBeanByParentTypeDuplicate() {
//DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class, () ->
ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy",
DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType =
ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value=" +
beansOfType.get(key));
}
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - 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));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
BeanFactory와 ApplicationContext
BeanFactory
ApplicationContext
다양한 설정 형식 지원
자바 코드 설정 사용
XML 설정 사용
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
<?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>
@Configuration
public class AppConfig {
// MemberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
// MemoryMemberRepository 역할
@Bean
public MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
// OrderService 역할
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), new FixDiscountPolicy());
}
// DiscountPolicy 역할
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer () {
AppConfig appConfig = new AppConfig();
//서로 다른 사용자가 똑같은 요청을 하면 객체가 2번 반환 되는지 확인 하는 코드
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
// 출력해 보면 참조값이 다른 것을 확인할 수 있다.
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
}
}
싱글톤 패턴
public class SingletonService {
//1. static 영역에 객체를 딱 1개만 생성해둔다. static으로 선언을 하면
클래스 레벨에 올라가기 때문에 딱 하나만 존재하게 된다.
private static final SingletonService instance = new SingletonService();
//2. public으로 열어서 객체 인스턴스가 필요하면 아래 static 메서드를 통해서만
조회하도록 허용한다. 앞으로 SingletonService class의 인스턴스는
아래 메서드를 통해서만 가져올 수 있기 때문에 항상 같은 인스턴스가 반환된다.
public static SingletonService getInstance() {
return instance;
}
//🥊 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을
못하게 막는다. 생성자를 private로 하면 다른 곳에서 해당 클래스의
인스턴스를 new 키워드로 만들 때 오류 발생
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
//🥊 아래 테스트를 정상적으로 통과하는 것을 확인할 수 있다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
//private으로 생성자를 막아두었다. 아래와 같이 생성하려고 하면 컴파일 오류가 발생한다.
//new SingletonService();
//1. 조회: 인스턴스를 2개 만들어서
호출할 때 마다 같은 객체를 반환 하는지 확인 하기 위해 인스턴스 2개 생성
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
//참조값도 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonService1 == singletonService2
// 검증
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
}
@Configuration
public class AppConfig {
// Bean 어노테이션을 붙여 주면 이것 들이 스프링 컨테이너에 등록이 된다.
// MemberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl.getInstance();
}
.......
}
싱글톤 패턴 문제점
싱글톤 컨테이너
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
// AppConfig를 가져온다.
AppConfig appConfig = new AppConfig();
// 스프링 컨테이너를 생성하고 가져온 AppConfig파일을 구성 파일로 넘긴다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 서로 다른 사용자가 똑같은 요청을 하면 객체가 2번 반환 되는지 확인 하는 코드
// 스프링 컨테이너에서 빈을 꺼낸다.빈 이름과 반환 타입 명시
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 출력해 보면 참조값이 같은 것을 확인할 수 있다.
// 참조값이 같으므로 싱글톤인 것.
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isSameAs(memberService2);
}
싱글톤 방식의 주의점
public class StatefulService {
private int price;
// 사용자가 name과 price를 전달하면 그 값을 필드에 저장
public void order(String name, int price) {
System.out.println("name =" + name + "price =" + price );
this.price = price;
}
// 저장된 price를 리턴
public int getPrice() {
return price;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton () {
// 🥊 스프링 컨테이너에 config파일 전달
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService = ac.getBean(StatefulService.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
// A사용자가 10000원 주문
statefulService.order("userA",10000);
// B사용자가 20000원 주문
statefulService1.order("userB", 20000);
// A사용자가 본인의 주문금액 조회 (10000원이 나와야 함)
int price = statefulService.getPrice();
System.out.println(price);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
public class StatefulService {
// 그냥 사용자가 값을 입력하면 그 값을 리턴하게 한다.
public int order(String name, int price) {
System.out.println("name =" + name + "price =" + price );
return price;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton () {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService = ac.getBean(StatefulService.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
// A사용자가 10000원 주문하고 값 받아옴
int userAPrice = statefulService.order("userA",10000);
// B사용자가 20000원 주문하고 값 받아옴
int userBPrice = statefulService1.order("userB", 20000);
// A사용자가 본인의 주문금액 조회하면
// A,B 가 각각의 지역변수로 price를 가지고 있기 때문에
// 10000원이 나온다.
System.out.println(userAPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
@Configuration
public class AppConfig {
// Bean 어노테이션을 붙여 주면 이것 들이 스프링 컨테이너에 등록이 된다.
// 그러면 스프링 컨테이너는 아래의 메서드 이름을 빈 이름으로, 해당 메서드의 리턴값을 빈 객체의 값으로 설정한다.
// 여기서 memberService도 리턴할 때 MemberRepository메서드를 호출 하면서 new MemberRepository를 하고, orderService도 마찬가지로 new MemberRepository를 한다.
// 그러면 싱글톤이 깨지는게 아닐까??
//🥊 확인해 보면 2개가 같은 인스턴스 인 것을 확인할 수 있는데 어떻게 그런걸까??
// MemberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
// MemoryMemberRepository 역할
@Bean
public MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
// OrderService 역할
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), new FixDiscountPolicy());
}
// DiscountPolicy 역할
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
컴포넌트 스캔이란 ?
// 설정 파일이니까 Configuration 붙여주고, ComponentScan을 붙여 준다.
// ComponentScan을 붙이면 Component 어노테이션이 붙은 클래스를 찾아서
자동으로 스프링 빈으로 등록해 준다.
// 아래 괄호 안에 내용은 @Configuration이 붙은 것은 컴포넌트 스캔에서
제외한다고 한 것인데 그 이유는 컴포넌트 스캔 사용시 @Configuration이
붙은 설정 정보도 같이 등록 되기 때문에 이전의 AppConfig예제와의
충돌을 막기 위한 것.
보통은 설정 정보를 컴포넌트 스캔 대상에서 제외하지 않는다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
@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);
}
}
@Component
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10; //10% 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
@Component
public class MemberServiceImpl implements MemberService{
// 회원 가입, 찾기를 하려면 데이터 저장소가 필요하다.
// 따라서 MemberRepository라는 인터페이스를 따르는 것들 중 MemoryMemberRepository라는 구현체의 인스턴스 생성
private final MemberRepository memberRepository;
// ❗memberServiceImpl은 생성자를 통해 의존성을 주입 받고 있었다.
하지만 @Component로 자동으로 빈을 등록하게 해 두면 이전의 AppConfig에서 직접
의존성을 주입 했던 것과 달리, 아무것도 없는 설정 파일에 @Componentscan
어토네이션을 붙여 사용하므로 이 경우 의존성을 직접 주입 받을 수 없다.
따라서 @Autowired 어노테이션을 통해 의존성을 주입 받는다.
즉 ComponentScan과 Autowired는 함께 사용 되는 것.
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 생성한 MemoryMemberRepository안의 메서드를 활용하여 저장하고 조회
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// @Autowired를 사용하면 여러 의존 관계도 한번에 주입 받을 수 있다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
// 설정 정보로 AutoAppConfig를 넘긴다.
// 결과를 보면 기존에 AppConfig와 같은 결과가 나오는 것을 확인할 수 있다.
public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
컴포넌트 스캔 정리
@Component("memberService2")
자동 의존관계 주입 정리
탐색 위치와 기본 스캔 대상
탐색 위치 설정
// basePackages는 탐색할 패키지의 시작위치를 지정한다.
// basePackages = {"hello.core", "hello.service"} 와 같이 여러개의
시작 위치를 지정할 수도 있다.
@ComponentScan(
basePackages = "hello.core",
}
// 프로젝트 구조가 아래와 같다면
// com.hello에 AppConfig 같은 메인 설정 정보를 두고 @ComponentScan을 붙이고,
basePackages 지정은 생략하는 것이다.
// 이렇게 하면 com.hello를 포함한 하위의 모든 클래스는 컴포넌트 스캔의 대상이 된다.
com.hello
com.hello.serivce
com.hello.repository
기본 스캔 대상
@Component : 컴포넌트 스캔에서 사용
@Controller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용
필터
필터를 사용하면 컴포넌트 스캔 대상을 추가하고 제외할 수 있다.
includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
다양한 의존관계 주입 방법
생성자 주입
// @Component가 붙어 있으므로 @ComponentScan에 의해 아래 클래스가 빈으로
등록될 것이다.
그러면 생성자가 호출되게 될 것이고, 이를 통해 의존관계를 주입 받게 될 것이다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// ❗만약 클래스 내부에 생성자가 1개라면 @Autowired를 생략해도 자동으로 주입된다. ( 스프링 빈 한정 )
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
수정자 주입 ( setter 주입 )
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
일반 메서드 주입
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
옵션 처리
@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드
자체가 호출 안됨
org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면
null이 입력된다.
Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
의존 관계 주입에서 생성자 주입을 사용해야 하는 이유
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
}
// 🥊 memberRepository와 discountPolicy 변수는 final 변수이기
때문에 아래 처럼 생성자가 아닌 메서드로 값을 주입할 수 없다.
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discounPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
lombok 라이브러리
기존 코드를 lombok을 활용하여 변경
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 지금 생성자가 하나 밖에 없기 때문에 아래의
Autowired는 생략해도 된다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
// lombok 라 이브러리가 제공하는 @RequireArgsConstructor를 활용하면
final이 붙은 필드를 모두 찾아 위의 코드 처럼 생성자를
자동으로 생성해 준다.
따라서 아래의 코드 처럼 깔끔하게 입력할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
조회 빈이 2개 이상인 경우
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 여기서 의존성 주입을 위해 매개변수로 들어 오는 DiscountPolicy를
컨테이너에서 꺼내올텐데 이때 타입으로 조회하여
ac.getBean(DiscountPolicy.class)과 유사하게 동작한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Autowired 필드명 매칭
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 아래의 매개 변수를 보면 구현체를 rateDiscouontPolicy로 바꾸었다.
// 이렇게 하면 DiscountPolicy로 등록된 것들 중에
rateDiscountPolicy를 특정해서 가져온다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@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;
}
그런데 만약 @Qualifier로 의존성 주입을 할 때@Qualifier("mainDiscountPolicy") 를 못찾으면 어떻게 될까?
그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다
이것도 없으면 에러가 발생한다.
@Primary
// DiscountPolicy로 빈 조회시 아래 2개가 조회가 될 텐데
이때 @Primary가 붙은 RateDiscountPolicy가 우선으로 조회 된다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
애노테이션 직접 만들기
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
// 이렇게 Qualifier를 등록을 하고
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
// 생성자 주입도 아래와 같이 한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
조회한 빈이 모두 필요할 때, List, Map