Spring 기본

Park sang woo·2022년 8월 13일
0

인프런 공부

목록 보기
1/13
post-thumbnail

📓 스프링

의존 관계란 : 의존 대상 B가 변하면, 그것이 A에 영향을 미칠 때 A는 B와 의존관계라고 한다.

📓 스프링 애노테이션 정리

🏷️ 스프링 Bean이란
스프링 컨테이너에 의해 관리되는 재사용 가능한 소프트웨어 컴포넌트이다. 즉 스프링 컨테이너가 관리하는 자바 객체를 뜻한다.


🏷️ @Component 개발자가 생성한 Class를 Spring의 Bean으로 등록할 때 사용하는 Annotation이다. Spring은 해당 Annotation을 보고 Spring의 Bean으로 등록합니다.

🏷️ @ComponentScan
@Component 및 Stereo Type(@Service, @Repository, @Controller)이 부여된 클래스들을 자동으로 스캔하여 빈으로 등록해주는 역할을 한다.


🏷️ @Component, @Bean 차이

@Bean 은 개발자가 제어가 불가능한 외부 라이브러리와 같은 것들을 Bean으로 만들 때 사용합니다.
매서드에 붙일 수 있다. 클래스 위에는 @Configuration을 붙여줘야 한다.(객체를 생성할 때 싱글톤을 보장하기 위해)

반면 @Component는 클래스 레벨에서 선언함으로써 스프링이 런타임시에 컴포넌트스캔을 하여 자동으로 빈을 찾고(detect) 등록하는 애노테이션이다.
개발자가 직접 컨트롤이 가능한 Class들의 경우엔 @Component를 사용한다. (class에서만 사용이 가능하다.)


🏷️ @RequestMapping
@RequestMapping(value=”“)와 같은 형태로 작성하며, URI의 요청과 Annotation value 값이 일치하면 해당 클래스나 메소드가 실행됩니다.


🏷️ @RequestParam
URL에 전달되는 파라미터를 메소드의 인자와 매칭시켜, 파라미터를 받아서 처리할 수 있는 Annotation
그러면 URL이 https://naver.com?nickname=dog&old=10 이런식으로 파라미터가 붙는다.


🏷️ @RequestParam
Body에 전달되는 데이터를 메소드의 인자와 매칭시켜, 데이터를 받아서 처리할 수 있는 Annotation
URL에서 ?와 함께 있는 것이라 생각하면 된다.


🏷️ @RequestBody HTTP의 통신 방식은 기본적으로 '요청과 응답(request, response)'으로 이루어져 있다. body 부분은 JSON형식으로 되어 있는데 클라이언트와 서버 간의 HTTP 통신에서 요청과 응답을 보낼 때, 필요한 데이터를 담아서 보내는 공간이 body이다. HttpRequest의 본문 requestBody의 내용을 자바 객체로 매핑하는 역할을 해서 JSON 형태의 데이터를 JAVA 객체에 자동으로 값을 넣어주는 어노테이션이다.

🏷️ @ResponseBody
메소드에서 리턴되는 값이 View 로 출력되지 않고 HTTP Response Body에 직접 쓰여지게 됩니다. return 시에 json, xml과 같은 데이터를 return 합니다. 클라이언트가 보낸 json형식의 http 요청 본문(body)이 그대로 전달된다. 그래서 @RequestBody와는 반대로 JAVA 객체를 JSON 형태로 변환해서 body에 담는 애노테이션이다.


🏷️ @ModelAttribute
@ModelAttribute는 HTTP Body 내용과 HTTP 파라미터의 값들을 Getter, Setter, 생성자를 통해 주입하기 위해 사용한다.

컨트롤러의 메서드에서 모델에 속성을 추가하고 뷰로 전달하는 데 사용된다.
즉 사용자가 전달하는 값을 오브젝트 형태(객체 형태)로 매핑해주는 어노테이션.
폼을 통해 값을 전달할 때 자주 사용


🏷️ Model 객체
Controller에서 생성된 데이터를 담아 View로 전달할 때 사용하는 객체이다.
model.addAttribute("key", "value") 메서드를 이용해 view에 전달할 데이터를 key, value 형식으로 전달한다.


🏷️ @Autowired
스프링 컨테이너에 등록한 빈에게 의존관계주입이 필요할 때, DI(의존성 주입)을 도와주는 어노테이션.
Bean객체를 주입받기 위한 방법이다. Spring Framework가 Class를 보고 의존 객체의 타입에 해당하는 Bean을 찾아 주입한다.


🏷️ @AllArgsConstructor
Class 모든 필드 값을 파라미터로 받는 생성자를 추가합니다.

🏷️ @NoArgsConstructor
Class 기본 생성자를 자동으로 추가해줍니다.






📓 회원 도메인 개발

✩ 회원을 가입하고 조회.
✩ 회원은 일반과 VIP 2가지 등금이 있음.
✩ 회원 데이터는 자체 DB를 구출할 수 있고 외부 시스템과 연동 할 수 있음.


Grade (Enum)

public enum Grade {
    Basic,
    VIP
}

Member (class)

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 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;
    }
}


MemberRepository (Interface)

public interface MemberRepository {
    void save(Member member); //회원 저장하는 기능
    Member findById(Long memberId); //회원 아이디 찾는 기능
}


MemberService (Interface)

public interface MemberService {
    void join(Member member); //회원 가입
    Member findMember(Long memberId);//회원 조회
}


MemberServiceImpl (class)

public class MemberServiceImpl implements MemberService{
    //가입을 하고 회원을 찾으려면 Member Repository가 필요
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    @Override
    public void join(Member member) {
        memberRepository.save(member);
        //join 에서 save를 호출하면 다형성에 의해 MemoryMemberRepository에 있는 인터페이스가 아니라
        //MemoryMemberRepository에 있는 save가 호출됨 (오버라이드한 것이 호출됨)
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}


MemoryMemberRepository (class)

import java.util.*;

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);
    }
}





📓 회원 도메인 실행과 테스트

MemberApp(class)

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("findMember = " + findMember.getName());
        System.out.println("New member = " + member.getName());
    }
}

그러면 New member = memberA, find Member = memberA가 출력됨.



Test(검증)
MemberServiceTest(class)

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);
    }
}


☪ 문제점 -> (스프링 이용없이 자바로만)

의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
ex) private final MemberRepository memberRepository = new MemoryMemberRepository();
MemberServiceImpl에서 MemberRepository는 인터페이스를 의존하지만 실제 할당하는 부분인 MemoryMemberRepository가 구현체를 의존한다. 그래서 MemberServiceImpl은 MemberRepository와 MemoryMemberRepository 둘 다 의존하게 된다. 즉 추상화(인터페이스)에도 의존하고 구체화(구체 클래스)에도 의존한다. 그러면 나중에 코드를 변경하게 될 때 문제가 생겨서 DIP를 위반하는 것이다.






📓 주문과 할인 도메인 설계

✩ 회원은 상품을 주문할 수 있음.
✩ 회원 등급에 따라 할인 정책을 적용할 수 있음.
✩ 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용.
✩ 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하기 못했고, 오픈 직전까지 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있음.



클라이언트가 주문 서비스를 호출하면 주문 서비스가 메모리 회원 저장소, 정액 할인 정책을 호출하는데 만약 메모리 회원 저장소가 DB로 바뀌고 정액~이 정률 할인 정책으로 바뀌어도 주문 서비스를 변경하지 않아도 된다. (즉 역할들의 협력 관계 그대로 유지.)



package discount
DiscountPolicy(Interface)
할인 정책

import hello.core.Member.Member;

public interface DiscountPolicy {
    //return 할인 대상 금액
    //호출하고 나면 결과로 얼마가 할인됐는지를 리턴
    int discount(Member member, int price);
}


FixDiscountPolicy(class) 정액 정책

import hello.core.Member.*;

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountFixAmount = 1000; //천원 할인하겠다

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){ //ENUM은 ==으로 함.
            return discountFixAmount;
        }
        else{
            return 0;
        }
        //VIP면 1000원 할인 아니면 할인 안 함.
    }
}


order Package
OrderService(Interface) 역할

public interface OrderService {
    //주문 서비스 역할에서 클라이언트는 주문 생성할 때 createOrder로
    //회원 id, 상품명, 상품 가격을 파라미터로 넘기고 return으로 주문 결과를 반환한다.
    Order createOrder(Long memberId, String itemName, int itmePrice);
    
}


Order(class)

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 void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}


OrderServiceImpl(class) 구현

public class OrderServiceImpl implements OrderService {
    //order 서비스는 2개 필요
    //final :
    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);
        //orderService 입장에서는 할인에 대해서는 모르니 discountPolicy 에서
        //알아서 하라는 것으로 결과는 나한테 던져주라는 의미이므로 단일 체계 원칙을 잘 지킨 것이다.
        //할인에 대한 명령이 필요하면 할인 쪽만 수정하면 되고 OrderService는 수정할 필요없음.

        return new Order(memberId, itemName, itemPrice, discountPrice);
        //주문을 만들어서 반환해주면 OrderService의 역할 끝
    }
}

//주문 생성 요청이 오면 회원 정보를 조회를 하고 할인 정책에다가 회원을 넘김


주문과 할인 도메인 실행과 테스트
OrderApp(class)

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);
        //memberService를 통해서 메모리 객체에 넣음. 그래야 주문해서 찾아 쓸 수 있다

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        System.out.println("order = " + order);
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}


결과
order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
order.calculatePrice = 9000



Test(검증)
OrderServiceTest(class)

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);
        //검증
    }
}


DiscountPolicy 인터페이스 있으니까 RateDiscountPolicy 추가만 하면 끝.
RateDiscountPolicy(class)

public class RateDiscountPolicy implements DiscountPolicy{
    private int discountPrice = 10; //10% 할인을 하겠다.
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPrice / 100;
            //만든 로직이 맞는지 test -> Ctrl+Shift+T -> create new Test
            //test에 빠르게 만들어줌
        }else{
            return 0;
        }
    }
}


Test
RateDiscountPolicyTest(class)

class RateDiscountPolicyTest {
    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
    @Test
    @DisplayName("Vip는 10% 할인이 적용되어야 한다.")
    void vip_o(){ //vip는 잘 적용된다는 함수
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        assertThat(discount).isEqualTo(1000);
        //할인한 금액 1000원이 적용됐는지 검증
    }

    @Test
    @DisplayName("Vip이 아니면 10% 할인이 적용되지 않아야 한다.")
    void vip_x(){
        //given
        Member member = new Member(1L, "memberBASIC", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        assertThat(discount).isEqualTo(1000);
        /*
        org.opentest4j.AssertionFailedError: 
expected: 1000
 but was: 0
Expected :1000
Actual   :0
        */
    }

}

Assertions는 import static으로 붙이는게 좋다. Alt+Enter



✅ 역할과 구현을 분리하고 다형성 활용, 인터페이스와 구현 객체를 분리했음.


💎 하지만 문제점.

구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.

private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

DIP 위반 -> OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다.


구체에 의존하지 않고 추상(인터페이스)에 의존해야 한다.
정액 할인 정책 -> 정률 할인 정책
OrderServiceImpl 에서 Fix -> Rate로 번경
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();


OCP 위반 -> 그래서 FixDiscountPolicy()를 RateDiscountPolicy() 로 변경하는 순간 OrderServiceImpl의 소스코드도 함께 변경해야 한다.



OrderServiceImpl 이 DiscountPolicy(Interface) 만 의존하도록 변경
-> private DiscountPolicy discountPolicy;
근데 NullPointerException 에러가 발생한다.
이유는 discountPolicy 가 NULL인데 discountPolicy.discount 해서 에러가 발생한다.


☪ DIP, OCP 문제 해결

DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경.
누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.


AppConfig : 애플리케이션의 전체 동작 방식을 구성(config) 하기 위해 구현객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.

public class AppConfig {
 
 	public MemberService memberService() {
 		return new MemberServiceImpl(new MemoryMemberRepository());
    }
 	public OrderService orderService() {
   		return new OrderServiceImpl(
   			new MemoryMemberRepository(),
   			new FixDiscountPolicy());
    }
}





📓 관심사 분리 (매우 중요!!!)

OrderServiceImpl

private final MemberRepository memberRepository = new MemoryMemberRepository();

위 코드는 애플리케이션에 인터페이스 x, y가 있다고 할 때 이 애플리케이션을 실행하면 누가 인터페이스 역할 x,y를 할지 정하는 것이 아니라 x역할을 하는 A가 y역할도 한다.
즉 OrderServiceImpl은 OrderService 관련된 로직만 수행해야 하는데 FixDiscountPolicy()역할도 자기가 선택을 해서 수행하는 것이다.
마치 로미오 역할을 하는 디카프리오가 줄리엣 역할을 하는 여자 주인공을 초빙하는 것처럼!!!
그래서 관심사를 분리하여 본인의 역할만을 수행하는 것에만 집중해야 한다.



☪ 생성자 주입

생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)해준다.

  • MemberServiceImpl 같은 경우 MemoryMemberRepository() 객체를 생성해서 참조값을 생성자의 파라미터 memberRepository에 넣어준다.
    MemberServiceImpl에 MemoryMemberRepository에 대한 코드가 존재하지 않는다. 오로지 MemberRepository 인터페이스만 존재한다. 그러면 추상화에만 의존하게 된다. (DIP 지킴.)
    구체적인 것에 대해서는 MemberServiceImpl은 전혀 모르고 AppConfig에서 생성하여 넣어준다. 이게 생성자 주입이다.

MemberServiceImpl ➛ MemoryMemberRepository
OrderServiceImpl ➛ MemoryMemberRepository, FixDiscountPolicy(파라미터로 들어감.)


MemberServiceImpl

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }//생성자를 통해서 memberRepository에 구현체가 뭐가 들어가는지 봄

    @Override
    public void join(Member member) {
        memberRepository.save(member);
        
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

}

✺ 이렇게 생성자 주입하면 MemberServiceImpl은 MemoryMemberRepository(구현체)를 의존하지 않는다. MemberRepository(추상화) 인터페이스만 의존한다.
✺ MemberServiceImpl 입장에서는 생성자를 통해 어떤 구현 객체가 들어오는지 알 수 없다.
✺ MemberServiceImpl의 생성자를 통해서 어떤 구현 객체가 들어올지는 외부인 AppConfig를 통해서만 알 수 있다.
✺ MemberServiceImpl은 의존관계에 대한 고민은 외부에 맡기고, 실행에만 집중하면 된다.


AppConfig
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스. 즉 Impl에서 추상화만 의존하게 하기 위해 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

//나의 애플리케이션 전체를 설정하고 구성하기
public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }//어디선가 AppConfig를 통해서 memberService를 불러다 쓸 때 MemberServiceImpl 구현체가
    //생성되는데 그 때 MemberServiceImpl에 대한 것은 여기서 알려준다.
    //생성자를 통해서 memberRepository에 구현체가 뭐가 들어갈지를 결정.

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }//생성자 주입
}
//AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
//AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)한다.
//->new MemoryMemberRepository() 객체를 생성해서 이거에 대한 참조값을 MemberServiceImpl에.
//생성자가 있는 파라미터에 넣어준다는 의미

OrderServiceImpl (class) (생성자 주입)

public class OrderServiceImpl implements OrderService {
    //DIP 잘 지키고 있음!!
    private final MemberRepository memberRepository; //뒤에 구체화 부분 삭제.
    private final DiscountPolicy discountPolicy;


	//생성자 생성해서 2개를 받음.
    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);
        //orderService 입장에서는 할인에 대해서는 모르니 discountPolicy 에서
        //알아서 하라는 것으로 결과는 나한테 던져주라는 의미이므로 단일 체계 원칙을 잘 지킨 것이다.
        //할인에 대한 명령이 필요하면 할인 쪽만 수정하면 됨

        return new Order(memberId, itemName, itemPrice, discountPrice);
        //주문을 만들어서 반환해주면 OrderService의 역할 끝
    }
}

그리고 AppConfig에서 생성자 파라미터에 들어갈 객체를 생성.
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());


✺ OrderServiceImpl은 FixDiscountPolicy를 의존하지 않고 DiscountPolicy(인터페이스)만을 의존한다.
✺ OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.
✺ OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할 것인지는 오직 외부 AppConfig를 통해서만 알 수 있다.
✺ OrderServiceImpl은 실행에만 집중.

클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것과 같다고 해서 DI(의존 관계 주입)라고 한다.






📓 AppConfig로 테스트

AppConfig 이용해서 개발
MemberApp(class)

public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        //기존에는 MemberServiceImpl을 직접 main에서 생성해주고 MemberServiceImple에서 다시
        //MemoryMemberRepositoy를 또 생성했었지만
        //이제는 AppConfig에서 이런 것들을 모두 결졍함.
        //AppConfig에서 memberService() 달라고 하면 memberService 인터페이스를 주고 memberService 안에는
        //memberServiceImpl 이 들어가 있다.
        //MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("findMember = " + findMember.getName());
        System.out.println("New member = " + member.getName());
    }
}

결과 : new member = memberA
find member = memberA



OrderApp

public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
        //appConfig에서 memberService가 필요하면 꺼내고 orderService가 필요하면 꺼낸다.
        

        //멤버 저장
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);
        //memberService를 통해서 메모리 객체에 넣음. 그래야 주문해서 찾아 쓸 수 있다

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        System.out.println("order = " + order);
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

결과 : order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=10000}



그러나 테스트 에러를 수정
AppConfig를 사용하도록 바꿔줌.

MemberServiceTest(class)

public class MemberServiceTest {
    MemberService memberService;
    //생성자 넣어줘야 함. (appConfig 사용하도록 바꿈)

    @BeforeEach // 각 테스트를 실행하기 전에 무조건 실행되도록 하는 부분.
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }//이걸 실행하기 전에 appConfig를 만들고 memberService를 할당
    //그리고 나서 join 테스트 실행

    @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);
    }
}

OrderServiceTest(class)

public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;

    @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);
        //검증
    }
}

구체 클래스에 대해서 고민하지 않고 중요한 인터페이스만을 보고 개발하면 된다. (실행만 고민!!)






📓 AppConfig 리팩터링

AppConfig(class)

public class AppConfig {
    public MemberService memberService(){
        //MemberServiceImpl 객체를 생성하면서 내가 만든 MemberServiceImpl은
        //MemoryMemberRepository()를 쓸 거라는 것을 주입해준다.
        return new MemberServiceImpl(MemberRepository());
    }

    private MemberRepository MemberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(MemberRepository(), discountPolicy());
    }

    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }//할인 정책은 Fix~를 쓰겠다.
    //new Fix~ 를 DiscountPolicy 메서드에서 대체해줌
}//장점 : 역할이 모두 다 드러남, JDBC나 DB를 할 때 각 메서드 부분만 수정하여 사용한다. 

AppConfig는 중복이 있고 역할에 따른 구현이 잘 보이지 않는다.
new MemoryMemberRepository가 2번 중복하여 생성했었다.
control + Alt + M 해서 MemberService 역할, MemberRepository 역할, OrderService 역할 만듦.
Discount 역할 빠져서 넣어줌.

중복된 부분을 제거해서 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
AppConfig를 보면 역할과 구현이 한 눈에 들어온다.
애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수가 있다.
ex) DiscountPolicy()는 Fix~를 쓰는구나. 라는 것을 금방 알아챌 수 있다.






📓 할인 정책, 새로운 구조 적용

FixDiscountPolicy -> RateDiscountPolicy로 수정

AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리되었다. RateDiscountPolicy로 바꿀 때 구성 영역인 AppConfig만 변경하면 된다. (사용 영역은 건드릴 필요 없음.)

AppConfig(class)


@Configuration //설정 정보(스프링에서는 이런 설정 정보를 이렇게 해줌)
public class AppConfig {
    @Bean //이렇게 Bean을 쓰면 자동으로 스프링 컨테이너에 등록이 된다.
    public MemberService memberService(){
        return new MemberServiceImpl(MemberRepository());
    }
    
    @Bean
    private MemberRepository MemberRepository() {
        return new MemoryMemberRepository();
    }
    
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(MemberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }
}
  • 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다.
    클라이언트 코드인 OrderServiceImpl를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.

  • 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하고 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.






📓 IoC, DI, 컨테이너

🏷️ IoC (제어의 역전)

프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 IoC라고 한다.
AppConfig로 구현 객체는 자신의 로직을 실행하는 역할만 담당하는데 프로그램 흐름은 이제 AppConfig가 가져간다.
ex) OrderServiceImpl은 필요한 인터페이스만 호출하고 어떤 구현 객체들이 실행될지 모름.

OrderServiceImpl도 AppConfig가 생성되고 AppConfig는 OrderServiceImpl이 아닌 OrderService 인터페이스도 다른 구현 객체를 생성하고 실행할 수 있다.
OrderServiceImpl은 그런 사실을 모른 체 묵묵히 자신의 로직만 실행할 뿐이다.

참고
프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크고 내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리다.



🏷️ DI

예제를 보면 OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다.
의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체 의존 관계가 있다.


정적 클래스 의존 관계

그림에서 OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 볼 수 있다.
하지만 이 클래스 의존 관계로는 실제 어떤 객체가 OrderServiceImpl에 주입될지 알 수 없다.


동적 객체 인스턴스 의존 관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을
의존관계 주입이라 한다.

  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다
  • 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 클래스 의존 관계를 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.


🏷️ IoC 컨테이너, DI 컨테이너

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






📓 스프링으로 전환

@Bean : 스프링 컨테이너에 등록
MemberApp(class)

public class MemberApp {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        //스프링은 ApplicationContext (스프링 컨테이너) 으로 시작한다.
        //AppConfig에 있는 환경 설정 정보를 가지고 스프링이 @Bean이 있는 것들을 모두 스프링 컨테이너에 넣어서 관리해준다.
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);
        //getBean(메서드 이름, 타입);
        //찾아올 때는 스프링 컨테이너로부터 찾아온다.

        Member findMember = memberService.findMember(1L);
        System.out.println("findMember = " + findMember.getName());
        System.out.println("New member = " + member.getName());
    }
}

OrderApp(class)

public class OrderApp {
    public static void main(String[] args) {
        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);
        //memberService를 통해서 메모리 객체에 넣음. 그래야 주문해서 찾아 쓸 수 있다

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        System.out.println("order = " + order);
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}


🏷️ 스프링 컨테이너

✽ ApplicationContext를 스프링 컨테이너라고 한다.
✽ 기존에는 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만 이제는 스프링 컨테이너를 통해서 사용한다.
✽ 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다. @Bean이라 적힌 메서드를 모두 호출해서 반환한 객체를 스프링 컨테이너에 등록한다. 이렇게 등록된 객체를 스프링 빈이라 한다.
✽ @Bean이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다. @Bean(name = "xxx") 로 해서 이름을 바꿀 수도 있긴 함.
✽ 이제는 스프링 컨테이너를 통해서 필요한 스프링 빈을 찾아야 한다. 스프링 빈은 applicationContext.getBean()을 통해서 찾을 수 있다.
✽ 스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아서 사용한다.






📓 스프링 컨테이너 생성

✲ ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
✲ ApplicationContext를 스프링 컨테이너라 한다. ApplicationContext는 인터페이스이다.
✲ 애노테이션 기반의 자바 설정 클래스로 만든다. (XML로도 가능은 함.)
✲ AppConfig를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.
✲ AnnotationConfigApplicationContext(AppConfig.class) 이것이 구현체.


주의 -> 빈 이름은 항상 다른 이름을 부여해야 한다. (빈 이름 중복X)
같은 이름을 부여하면 다른 Bean이 무시되거나, 기존 Bean을 덮어버리거나 설정에 따라 오류가 발생한다. 실무에서는 무조건 단순하고 명확하게 개발해야 한다.

스프링 Bean 등록하고 나면 의존관계를 넣어준다.

스프링은 빈을 생성하고 의존관계를 주입하는 단계가 나누어져있다.
자바 코드로 스프링 Bean을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.






📓 스프링 컨테이너에 실제 스프링 빈들이 잘 등록되었는지 확인

ApplicationContextInfoTest(class)

public class ApplicationContextInfoTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName); //타입 시전 안해서 모르므로 Object가 뜸
            System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){ //ac : applicationContext
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);//Bean 하나하나에 대한 정보들
            //내가 주로 애플리케이션 개발을 하기 위해서 등록한 Bean들
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
                //Application 등록한 빈 들만 출력이 된다. (5개)
            }
        }
    }
}
//애플리케이션 빈 출력하기
//내부에서 사용하는 빈은 제외하고 내가 등록한 빈만 출력
//내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.
//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈

//모든 빈 출력
//스프링에 등록된 모든 빈 정보를 출력
//ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회
//ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회





📓 스프링 빈 조회

ac.getBean("빈 이름", 타입);
ac.getBean(타입);
ApplicationContextBasicFindTest(class)

public class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); //스프링 컨테이너

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName(){
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        System.out.println("memberService = " + memberService);
        System.out.println("memberService.getClass() = " + memberService.getClass());

        //검증
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
        //memberService 가 MemberServiceImpl 의 인스턴스면 성공
    }

    @Test
    @DisplayName("이름없이 타입으로만 조회")
    void findBeanByType(){
        MemberService memberService = ac.getBean(MemberService.class);
        //검증
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }//memberService 는 MemberServiceImpl의 인스턴스이다. 

    @Test
    @DisplayName("구체 타입으로 조회") //Impl 구현체를 의존하므로 유연성이 떨어짐
    void findBeanByName2(){
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        //스프링 컨테이너에 객체가 등록이 되어 있으면 조회가 가능하다.
        //검증
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX(){
        //ac.getBean("xxxx", MemberService.class); 이름 xxxx로 없는 경우
        //MemberService xxxx = ac.getBean("xxxx", MemberService.class);
        //No bean named 'xxxx' available 에러 발생

        // 이 예외가 터져야 테스트가 성공
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxx", MemberService.class));
        //오른쪽 로직을 실행하면 왼쪽에 예외가 터진다.
    }
}





📓 스프링 빈 조회 동일한 타입 둘 이상

타입으로 조회하는데 같은 타입이 둘 이상이면 오류가 발생하므로 빈 이름을 지정.

ApplicationContextSameBeanFindTest(class)

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    //중복을 할려면 AppConfig를 수정해야 해서 AppConfig.class를 지움

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 중복 오류가 발생한다.")
    void findBeanByTypeDuplication(){
        //MemberRepository bean = ac.getBean(MemberRepository.class);
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(MemberRepository.class));
    }//여기가 터진다. 이유는 타입만 지정하고 SameBeanConfig만을 가지고 실행을 한다.
    //스프링 컨테이너가 스프링빈 1(),2() 만을 등록을 한다.
    //그러면 스프링 입장에서 1(), 2() 중 뭘 선택해야 하는지 인지를 못하게 되어 에러가 발생한다.

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean("memberRepository1",MemberRepository.class);
        //예외 터지므로 빈 이름을 지정
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }//memberRepository는 MemberRepository 의 인스턴스이다.

    @Test
    @DisplayName("특정 타입을 모두 조회하기") //autowird 같은 것을 쓸 때 유용
        //즉 1(), 2() 둘 다 꺼내오기 -> ac.getBeansOfType
    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);
        //2 인 이유는 1(), 2() 로 2개이기 때문
    }
    @Configuration
    //static 쓰는 이유는 class 안에 class를 쓰면 SameBeanConfig는 여기 클래스에서만 쓰겠다는 의미
    static class SameBeanConfig{
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }// 다른 파라미터를 넣어서 다른 의미를 부여할 수 있기 때문에 return 부분은 잘못된 것이 아니다.
    }
}





📓 스프링 빈 조회 - 상속관계

부모 타입으로 조회하면 자식 타입도 함께 조회한다.
그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
(눈에 보이지 않을 뿐 자바는 기본적으로 모든 Object(최상위 부모)를 상속하고 있다.

ApplicationContextExtendsFindTest

public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 중복 오류가 발생한다.")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }


    @Test
    @DisplayName("그래서 빈 이름을 지정한다.")
    void findBeanByParentTypeBeanName(){
        //타입은 DiscountPolicy 지만 구현 객체는 rateDixcountPolicy
        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));
        }//스프링 내부적으로 쓰는 빈까지 모두 나온다. 자바 객체는 모든게 Object 타입이기 때문이다.
    }

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}





📓 BeanFactory, ApplicationContext

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


ApplicationContext
✲ BeanFactory의 기능을 모두 상속받아서 제공.
✲ 애플리케이션 개발할 때는 빈 관리, 조회 기능 + 수 많은 부가 기능 필요.


ApplicationContext(Interface)가 제공하는 부가 기능 (모두 인터페이스임.)
MessageSource - 메시지소스를 활용한 국제화 기능으로 한국에서 들어오면 한국어, 영어권에서 들어오면 영어로 출력.

EnvironmentCapable(환경 변수) - 로컬, 개발, 운영 등을 구분해서 처리

ApplicationEventPublisher(애플리케이션 이벤트) - 이벤트를 발행하고 구독하는 모델을 편리하게 지원.

ResourceLoader - 편리한 리소스 조회로 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회.

BeanFactory


ApplicationContext는 BeanFactory의 기능을 상속받는다.
ApplicationContext는 빈 관리기능 + 편리한 부가 기능 제공.
BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.






📓 웹 애플리케이션과 싱글톤

AppConfig에서 각각의 클라이언트가 memberService를 요청하면 new 해서 객체를 여러 개 생성하여 반환한다. 웹 애플리케이션은 고객이 계속 요청을 하는 것인데 요청을 할 때마다 객체를 계속 생성해서 반환해줘야 한다는 것이 문제다.


SingletonTest(class)

public class SingletonTest {
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();
        //1. 조회: 호출할 때마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        //2. 조회: 호출할 때마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();
        
        //참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        //memberService1 != memberService2
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
}

즉 스프링이 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 새로운 객체를 계속 생성한다.
고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생서오디고 소멸되는 것이다.
그래서 해당 객체가 1개만 생성되고 공유하도록 설계한다. -> 싱글통 패턴.



싱글톤 패턴 : 클래스의 인스턴스가 딱 1개만 생성되도록 보장한다.
객체 인스턴스를 2개 이상 생성하지 못하도록 막는다.
SingletonService(class)

public class SingletonService {
    //static 영역에 객체 딱 1개만 생성
    private static final SingletonService instance = new SingletonService();
    //static으로 해주면 클래스 레벨에 올라가기 때문에 인스턴스 하나만 존재하게 된다
    //내부적으로 실행을 해서 객체 자기 자신을 생성해서 인스턴스에 참조를 넣는다.
    //그러면 자기 자신 객체 인스턴스만 생성해서 여기 안에만 들어간다.

    public static SingletonService getInstance() {
        return instance; //instance의 참조를 꺼내는 방법
    }//public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    //객체 인스턴스가 필요하면 getInstance를 통해서만 조회할 수 있다.
    //항상 같은 인스턴스를 반환한다.

    //private 생성자
    private SingletonService() {
    //생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다. (1개의 객체 인스턴스만 존재해야 하기 때문에)
    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        //new SingletonService(); //이렇게만 하면 private access 불가라고 에러 발생
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);
        //isSameAs : 주소값을 비교하는 메서드
        //isEqual : 대상의 내용 자체를 비교하는 메서드
    }//같은 객체라고 나온다.
}

//instanceOf구문
//객체가 어떤 클래스인지, 어떤 클래스를 상속받았는지 확인하는데 사용하는 연산자.

스프링 컨테이너를 쓰면 기본적으로 객체를 전부 싱글톤으로 관리해준다.
싱글톤 패턴을 적용하면 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진
객체를 공유해서 효율적으로 사용할 수 있다.


☪ 싱글톤 문제점

  1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감.
  2. 의존 관계상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반.
    클라이언트가 구체 클래스에 의존 -> OCP 위반.
  3. 테스트 하기 어려움.
  4. 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);

        assertThat(memberService1).isSameAs(memberService2);
    }

스프링 컨테이너 덕분에 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.






📓 싱글톤 패턴 주의점

싱글톤이든 스프링같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안 된다. (즉 무상태로 설계)


✲ 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
✲ 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
✲ 가급적 읽기만 가능해야 한다. (값을 수정 X)
✲ 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
✲ 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.



너무 중요 (실무에서도 자주 쓰임)
싱글톤 문제점 예시
StatefulService

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;
    }
}

StatefulServiceTest

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA : A 사용자가 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB : B 사용자가 20000원 주문
        statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);
        //price는 20000원이 나온다. (1 이 10000원을 주문했으니 10000원이 나와야 함)
        //이유는 statefulService1 이든 statefulService2든 같은 인스턴스이기 때문이다.

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig{
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

StatusfulService의 price 필드는 공유되는 필드로 특정 클라이언트가 값을 변경한다.
진짜로 공유필드는 조심해야 한다.



수정 -> 무상태로 설계
상태 유지하는 필드 price를 제거하고 지역변수, 파라미터, ThreadLocal을 사용한다.
그리고 그냥 return 해버리면 끝.
StatefulService(class)

public class StatefulService {
    //private int price; //상태를 유지하는 필드
    public int order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        //this.price = price; //여기가 문제
        return price; //price를 넘겨버리면 해결됨
    }
}

StatefulServiceTest

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

		//지역변수이기 때문에 사용자 A,B의 값이 다르게 나옴.
        //ThreadA : A 사용자가 10000원 주문
        int userAPrice = statefulService1.order("userA", 10000);
        //ThreadB : B 사용자가 20000원 주문
        int userBPrice = statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        
        System.out.println("price = " + userAPrice);
        

    static class TestConfig{
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}





📓 Configuration과 싱글톤

@Configuration은 사실 싱글톤을 위해 존재하는 것이다.
Appconfig(class)를 보면 @Bean에 memberService()가 memberRepository()를 호출하고 new MemoryMemberRepository() 객체가 생성이 된다.
orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출하고 new MemoryMemberRepository() 객체를 생성한다.
-> 그러면 각각 다른 2개의 MemoryMemberRepository() 가 생성되면서 싱글톤이 깨지게 된다.

☪ 스프링 컨테이너가 문제 해결

ConfigurationSingletonTest(class)

@Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);
        //3개 스프링 빈이 모두 같다
        //이유 :

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

memberRepository1,2, memberRepository 3개가 모두 같은 인스턴스라는 결과가 나온다.

AppConfig(class)

@Configuration
public class AppConfig {
    @Bean //이렇게 Bean을 쓰면 자동으로 스프링 컨테이너에 등록이 된다.
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    //생성자 주입
    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }
}
  1. call AppConfig.memberService 를 호출한다.
  2. new 하면서 call AppConfig.memberRepository를 호출
  3. call AppConfig.memberRepository 출력
  4. call AppConfig.orderService 출력
  5. call AppConfig.memberRepository

-> call AppConfig.memberRepository 이 3번 호출 되어야 하는데 1번만 호출됨
(스프링 컨테이너가 싱글톤을 보장하는 것이다.)






📓 Configuration과 바이트코드 조작

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
ConfigurationSingletonTest(class)

@Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //Annotation~Context() 해서 넘기면 AppConfig도 스프링 빈으로 등록이 된다.
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }

순수한 클래스라면 class hello.core.AppConfig 로 출력이 되어야 한다.

생각한 것과 다르게 클래스 명에 xxxCGLIB 가 붙으면서 상당히 복잡해진다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고 그 임의의 클래스를 스프링 빈으로 등록한 것이다.
즉 스프링 컨테이너에 이름은 AppConfig이지만 인스턴스 객체가 AppConfig@CGLIB~~ 가 들어가 등록되는 것이다. 이 임의의 클래스가 싱글톤이 보장되도록 해준다.


@Bean이 붙은 메서드마다 이미 스프링 빈이 존재한다면 빈을 반환하고(스프링 컨테이너에 등록되어 있으면 스프링 컨테이너에서 찾아서 반환!!) 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.(기존 로직 호출해서 메서드 생성하고 스프링 컨테이너에 등록.)
덕분에 싱글톤이 보장된다.

AppConfig@CGLIB~~는 AppConfig의 자식타입이므로 AppConfig타입으로 조회 가능.


*@Configuration 적용하면 바이트코드를 조작하는 CGLIB 기술을 사용하여 싱글톤이 보장된다.
그냥 스프링 설정 정보(빈 등록, 싱글톤)는 항상 @Configuration 을 사용하자!!






📓 컴포넌트 스캔, 의존관계 자동 주입

스프링 빈이 수백개가 되면 일일이 등록하기에 번거로움이 있기 때문에 설정 정보 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 사용한다.
의존관계 자동 주입으로 @Autowired 사용.

@Configuration
@ComponentScan(//모두 끌어모아서 자동으로 스프링 빈으로 등록한다

    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
            classes = Configuration.class) 
)

public class AutoAppConfig {

}

excludeFilters - 모두 스프링 빈으로 등록하기 전에 뺄 것을 선정. AppConfig는 @Configuration으로 수동으로 등록을 하는데 자동으로 등록이 되면 충돌이 일어난다.
Configuration.class 를 빼준다. (AppConfig도 @Configuration으로 컴포넌트 스캔의 대상이 되어 충돌이 일어난 것.)

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다.
보면 기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.
컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록이 된다.
AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록이 되고 실행되어 버린다. 그래서 excludeFilters를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외한다.
(근데 일반적으로, 실무에서는 지우지 않음. 그냥 예제 코드를 남기기 위해서 빼준 것.)




Impl 있는 곳에 클래스에다가 @Component를 사용해서 자동으로 스프링 빈에 등록을 한다.
이렇게만 하면 의존 관계 주입을 못하므로 자동 의존 관계 주입 @Autowired를 사용하여 여러 의존 관계를 한 번에 주입받을 수 있다.

@Autowired //의존관계 자동 주입 (생성자에 붙여줌) / MemberRepository 타입에 맞는 얘를 찾아서
    //의존 관계 주입을 자동으로 연결해서 주입해준다.
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }//생성자를 통해서 memberRepository에 구현체가 뭐가 들어가는지 봄



☪ 스프링 컨테이너에 등록이 되는 방법.

스프링 컨테이너가 @Component있는 클래스를 뒤져서 싱글톤으로 컨테이너에 등록을 한다.
이때 스프링 빈의 기본 이름은 맨 앞글자를 무조건 소문자로 한다. (스프링 빈의 이름을 직접 지정하고 싶으면 @Component("memberService~~"). )



☪ @Autowired 의존 관계 자동 주입

@Component
public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

스프링이 컨테이너에 있는 memberRepository를 뒤지는데 MemberRepository 타입으로 조회해서 memoryMemberRepository를 찾아서 주입을 한다. (같은 타입이 여러 개 있으면 충돌.)
생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. (타입이 같은 빈을 찾아서 주입.)
getBean(MemberRepository.class)와 동일하다고 생각.






📓 탐색 위치와 기본 스캔 대상

@ComponentScan(//모두 끌어모아서 자동으로 스프링 빈으로 등록한다
    basePackages = "hello.core.member", //모든 자바 클래스를 다 컴포넌트 스캔하면
        //시간이 오래 걸리므로 필요한 위치부터 탐색하도록 시작 위치를 지정
    excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
            classes = Configuration.class) 
)

위치 지정해주지 않으면 이 패키지 포함해서 하위 패키지를 모두 탐색

권장 하는 방법
패키지 위치를 지정하지 않고 설정 정보 클래스의 위치를 프로젝트 최상단에 둔다.



☪ 컴포넌트 스캔 기본 대상

스프링이 객체를 제어하기 위해서는 객체들이 Bean으로 등록이 되어 있어야 한다.
메서드 레벨에서 선언하며, 반환되는 객체(인스턴스)를 개발자가 수동으로 빈으로 등록한다.

@Component : 클래스 레벨에서 선언하며, 스프링이 런타임 시 컴포넌트 스캔을 하여 자동을 빈을 찾고 등록을 한다. 컴포넌트 스캔에서 사용.

Bean : 개발자가 컨트롤이 불가능한 외부 라이브러리 사용 시
Component : 개발자가 직접 컨트롤이 가능한 내부 클래스에 사용

생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 칮아서 주입한다.
자동 등록하면 의존 관계를 주입할 수 있는 방법이 없다.
@Component 사용한 경우 설정 정보(@Configuratint)를 아예 안 쓰기 때문에 의존 관계 자동 주입을 선택

@Controller : 스프링 MVC 컨트롤러로 인식
@Repository : 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해준다.
@Service : 특별한 처리를 하는 것은 아니고 개발자들이 핵심 비즈니스 로직이 여기에 있구나 라고 비즈니스 계층을 인식하는데 도움이 된다.

@Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지할 수 있도록 추가 처리를 한다. 스프링 설정 정보에서 사용.

@Componentscan : 빈으로 동록 될 준비를 마친 클래스들을 스캔하여, 빈으로 등록해준다.
(빈으로 등록 될 준비가 되었다는 것은 @Controller, @Service, @Component, @Repository 어노테이션을 붙인 클래스들이 빈으로 등록될 준비를 한 것이다.)
@Controller나 @Service가 @Component를 포함하고 있어서 @Controller나 @Service를 인식한다.

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데 그 이름이 같은 경우 스프링은 오류를 발생시킨다. (충돌)
만약 수동 빈 등록과 자동 빈 등록에서 빈 이름이 충돌되면 수동 빈 등록이 우선권을 가지면서 수동 빈이 자동 빈을 오버라이딩 해버린다.
그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.






📓 필터

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {

}

BeanA는 @MyIncludeComponent이지만 BeanB는 @MyExcluedComponent로 함.

@MyIncludeComponent
public class BeanA {
}

@Target(ElementType.TYPE) - 사용할 어노테이션을 적용할 대상을 지정하는 데 사용. ElementType.TYPE 은 해당 Annotation 은 타입 선언 시 사용한다는 의미.

ElementType.PACKAGE : 패키지 선언
ElementType.TYPE : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR : 생성자 선언
ElementType.FIELD : 멤버 변수 선언
ElementType.LOCAL_VARIABLE : 지역 변수 선언
ElementType.METHOD : 메서드 선언
ElementType.PARAMETER : 전달인자 선언
ElementType.TYPE_PARAMETER : 전달인자 타입 선언
ElementType.TYPE_USE : 타입 선언

@Retention(RetentionPolicy.RUNTIME) - RUNTIME말고도 CLASS, SOURCE가 있음.
Annotation이 실제로 적용되고 유지되는 범위를 의미

@Documented - 어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 어노테이션 설정.


@Component와 @ComponentScan 차이
@Component는 개발자가 직접 작성한 Class를 Bean으로 등록할 수 있도록 해주는 어노테이션이다.
@ComponentScan은 스프링이 직접 클래스를 검색해서 Bean으로 등록해주는 기능이다. 설정 클래스에 빈으로 등록하지 않아도 원하는 클래스를 빈으로 등록할 수 있으므로 설정 코드가 크게 줄어든다.



@Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class); //include 여서 null이 아님
        assertThat(beanA).isNotNull();

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{

    }

includeFilters에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록이 됐지만 excludeFilters에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않았다.

ANNOTATION : 기본값, 애노텡션을 인식해서 동작.
ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작.

잘 쓸일이 없어 일단 스프링의 본 설정에 최대한 맞춰서 사용하는 것을 권장.






📓 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록할 경우 -> 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록됐는데 그 이름이 같다면 스프링은 오류를 발생시킨다.


@Configuration
public class AutoAppConfig { //이름을 똑같이 해서 수동으로 빈 등록함.
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

만약 @Bean으로 수동 등록하고 MemoryMemberREpository(빈 이름은 memoryMemberREpository)는 @Component로 자동 등록함.
그런데 이름이 같은데도 에러가 발생하지 않고 테스트를 통과한다. 이유는 수동 빈 등록이 우선권을 가지기 때문이다.


수동 우선권을 가지기 위해 같은 이름의 빈 이름을 자동과 수동으로 등록할 수도 있지만 이러면 개발자들마다 여러 설정들이 꼬여서 의도치 않게 잘못된 결과가 나와 정말 잡기 어려운 버그가 만들어진다. 그래서 최근에는 수동 빈 등록과 자동 빈 등록이 충돌나면 스프링 부트를 실행할 때 에러가 발생하도록 기본 값을 바꾸었다.
에러 이름 : "Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true"


자동 등록은 @Component, @Autowired / 수동 등록은 @Configuration이 있는 class 안에다가 위 예제처럼 @Bean 주석이 들어간 메서드를 넣으면 된다.






📓 의존 관계 주입 방법

☪ 1. 생성자 주입

✿ 생성자를 통해서 의존 관계를 주입 받는 방법 (위에서 했던 주입 방식)
✿ 중요 > 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 그래서 값을 세팅했으면 그 다음부터는 세팅을 못하게 막을 수 있다는 것이다.

✿ 중요 > 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다. (스프링 빈에만 해당)
불변, 필수 의존관계에 사용. 개발 때 불변이라는 것이 중요하다.

스프링 컨테이너는 크게 스프링 빈을 다 등록하고 @Autowired가 걸린 것들의 연관 관계를 다 주입해준다.


☪ 2. 수정자 주입 (setter 주입)

-> setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법
@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다.
(스프링 빈에 등록이 되지 않았을 경우에도 사용 가능. 선택적이거나 변경할 가능성이 있는 의존 관계에 사용)


☪ 3. 필드 주입 (안 사용하는게 나음!!!)

이름 그대로 필드에 바로 주입하는 방법
외부에서 변경이 불가능하여 테스트하기 힘들다는 큰 단점이 있다.


☪ 4. 일반 메서드 주입

한 번에 여러 필드를 주입 받을 수 있다. (잘 사용 안 함.)



의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.






📓 옵션처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있는데 @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법이 있다.
@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안됌.
org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

public class AutowiredTest {
    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    static class TestBean{
        @Autowired(required = false)
        public void setNoBean1(Member noBean1){
            System.out.println("noBean1 = " + noBean1);
        }//false로 하면 의존 관계가 없어서 여기 메서드 자체가 호출이 안 된다.

        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println("noBean2 = " + noBean2);
        }//호출은 되지만 null로 출력된다.

        @Autowired
        public void setNoBean3(Optional<Member> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }//값이 없으면 Optional.empty가 된다. 
    }
}





📓 생성자 주입 권장

불변 : 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.
수정자 주입을 사용하면 set~ 메서드를 public으로 열어두어야 한다. 그러면 누군가가 호출해서 실수로 바꿔버릴 수도 있다.
변경하면 안 되는 메서드를 열어 두는 것은 좋은 설계 방법이 아니다.
생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 그래서 불변하게 설계할 수 있다.

누락 : 프레임워크 없이 순수한 자바 코드를 단위 테스트를 하는 경우가 많다. (수정자 의존 관계 일 때)

public class OrderServiceImpl implements OrderService {
    //DIP 잘 지키고 있음!!
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired //수정자 의존 관계
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Autowired//수정자 의존 관계
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);
        //orderService 입장에서는 할인에 대해서는 모르니 discountPolicy 에서
        //알아서 하라는 것으로 결과는 나한테 던져주라는 의미이므로 단일 체계 원칙을 잘 지킨 것이다.
        //할인에 대한 명령이 필요하면 할인 쪽만 수정하면 됨

        return new Order(memberId, itemName, itemPrice, discountPrice);
        //주문을 만들어서 반환해주면 OrderService의 역할 끝
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
class OrderServiceImplTest {
    @Test
    void createOrder() {
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L, "itemA", 10000);
    }
}

실행결과가 NullPointerException 에러 발생한다.
이유는 OrderServiceImpl에 memberRepository, discountPolicy 값을 세팅을 해줘야 하는데 안 했기 때문에 발생한 것이다.
@Autowired가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생

private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
이거의 값을 세팅 해주지 않아서 누락이 된다.

다시 생성자 주입으로

public class OrderServiceImpl implements OrderService {
    //DIP 잘 지키고 있음!!
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

Test

class OrderServiceImplTest {
    @Test
    void createOrder() {
        MemoryMemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "name", Grade.VIP));

        OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
        Order order = orderService.createOrder(1L, "itemA", 10000);
        assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

☪ final 쓰면

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.


☪ final 장점

생성자에서만 값을 세팅할 수 있다. (아니면 초기값을 넣어주던지)
상수로 값을 바꿀 수가 없다.
생성자를 만들었는데 개발자가 실수로 생성자에서 this.~~ 를 잘못 정의하거나 누락 시 에러를 밝혀준다.

  1. 생성자 주입을 선택하는 이유 : 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기 때문

  2. 항상 생성자 주입을 선택하자. 가끔 옵션이 필요하면 수정자 주입을 선택 (필드 주입은 사용 X)






📓 롬복

개발을 해보면 대부분 다 불변이고 생성자에 final 키워드를 사용한다.
그런데 생성자도 만들어야 하고 주입 받은 값을 대입하는 코드도 만들어야 하는 불편함이 있다.

@Getter
@Setter
@ToString
//getter, setter 를 자동으로 만들어줌
//다른 것도 가능
public class Hellolombok {
    private String name;
    private int age;

    public static void main(String[] args) {
        Hellolombok helloLombok = new Hellolombok();
        helloLombok.setName("가나다라");

        System.out.println("helloLombok = " + helloLombok);
    }
}

결과
helloLombok = Hellolombok (name=가나다라, age=0)



최종 결과 코드

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

	//@RequiredArgsConstructor 로 인해 여기 생성자가 필요가 없음
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    } 

@RequiredArgsConstructor를 쓰면 final 붙은 상태의 생성자를 만들어준다.
그래서 생성자를 굳이 만들지 않아도 된다.

최근에는 생성자를 딱 1개 두고, @Autowired를 생략하는 방식을 주로 사용했다. 여기에 Lombok 라이브러리를 사용하면 기능은 다 제공하면서 코드는 깔끔하게 사용할 수 있다.






📓 조회 빈이 2개 이상 - 문제

@Autowired는 타입으로 조회한다.
조회 빈이 2개 이상있을 때 NoUniqueBeanDefinitionException 에러가 발생한다.
즉 하나의 빈을 기대했는데 2개가 발견됐다고 알려준다.
이름만 다르고 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안 된다.

fixDiscountPolicy, rateDiscountPolicy 둘 다 @Component로 등록해서 스프링 빈으로 선언해봄. 그러면 에러가 발생한다.

@Component
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;
        }
    }
}

☪ 해결 방법

1. @Autowired 필드 명 매칭
@Autowired는 타입 매칭을 시도하고(타입이 하나면 Bean이름 아무것도 안 보고 무조건 주입), 이 때 여러 빈이 있으면 필드 이름 또는 파라미터 이름으로 빈 이름을 추가 매칭한다.
타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭한다.

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }

(스프링은 본인과 같은 타입 또는 그 타입의 자식들을 다 끌고와 매칭한다.)



2. @Quilifier -> @Quilifier끼리 매칭 -> 빈 이름 매칭
추가 구분자를 붙여주는 방법으로 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

사용할 의존 객체를 선택해준다.

@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{
}

생성자 자동 주입 예시

public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

@Qualifier로 주입할 때 @Quilifier("mainDiscountPolicy")를 못 찾는 경우 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
<하지만 경험상 @Qualifier는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다. > Qualifier는 Qualifier 쓰는 얘들이랑만 같이 쓴다고 기억


☪ @Qualifier 정리

@Qualifier 끼리 매칭
Qualifier가 없으면 빈 이름 매칭
컨테이너가 여러 개의 빈을 찾았을 때 추가적으로 판단할 수 있는 정보를 줌.
그래도 안되면 NoSuchBeanDefinitionException 예외 발생


3. @Primary 사용 (편하고 자주 사용 but 한계점 존재)
우선 순위를 정하여 빈을 찾는 방법으로 @Autowired 시에 여러 번 매칭되면 @Primary가 우선권을 가진다.






📓 애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy") 에서 문자를 적으면 컴파일시 타입 체크가 안 됨. 그래서 애노테이션을 직접 만든다. (MainDiscountPolicy 애노테이션을 만듦.)

@Target() ~~

@MainDiscountPolicy 애노테이션을 적용했을 때 만약 애노테이션 문자를 잘못 입력했다면 컴파일 오류를 발생시킨다.






📓 조회한 빈이 모두 필요할 때, List, Map

해당 타입의 스프링 빈이 다 필요할 때로 할인 서비스가 2종류로 fix, rate가 있었는데 고객이 이 2종류를 모두 필요로 한다면 Map이나 List로 받을 수 있다.

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);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        @Autowired
        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);
            return discountPolicy.discount(member, price);
        }
    }
}

Map으로 DiscountPolicy를 주입받아 fixDiscountPolicy와 rateDiscountPolicy를 주입 받는다. discount() 메서드는 fixDiscountPolicy가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다.

map의 키에 스프링 빈의 이름, 값은 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담는다.
List<>는 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 해당하는 타입의 스프링 빈이 없으면 빈 컬렉션이나 Map을 주입.






## 📓 자동, 수동 실무 운영 기준 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고 어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고 의존 관계를 수동으로 주입할지는 고민이 되지만 **현재 자동을 선호하는 추세**이다. **자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.**

☪ 수동 빈 사용의 경우

애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 레포지토리 등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
데이터베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

업무로직처럼 숫자도 많고 어느 정도 유사한 패턴들이 있는 경우에 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 알 수가 있기 때문이다.

기술 지원 로직은 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미치기 때문에 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다.
그래서 이런 기술지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
-> 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록 (딱 설정 정보에 바로 나타나게 하는 것이 유지보수하기 좋다.)
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자. 자동 등록으로는 특정 패키지에 같이 묶어두는게 좋다. (한 눈에 보고 빈 이름은 무엇인지 어떤 빈들이 주입되는지 쉽게 파악하기 위해!!!)






📓 빈 생명주기 콜백

데이터베이스 커넥션 풀 : 애플리케이션은 보통 관계형 DB를 사용하는데 애플리케이션 서버가 올라올 때 데이터베이스와 연결(커넥션)을 미리 맺어 놓는다. 그러면 고객 요청이 올 때 연결해 놓은 것으로 그대로 재활용하여 사용한다.

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 에플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 안전하게 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 초기화 작업은 의존관계 주입이 모두 완료되고 나서 호출해야 한다.

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 이 때 초기화 작업을 해주면 된다.

초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백 : 빈이 소멸되기 직전에 호출

외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보면 NetworkClient는 애플리케이션 시작 시점에 connect()를 호출해서 연결을 맺어두어야 하고, 애플리케이션이 종료되면 disConnect()를 호출해서 연결을 끊어야 한다.

그 완료된 시점 -> 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 잇다.

스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료



☪ 참고 : 객체의 생성과 초기화를 분리하자!!

객체를 생성할 때는 객체를 생성하는데만 초집중해야 한다. 필요한 필수 값들을 넣어서 객체 인스턴스가 new해서 생성되는 데까지에만 집중해야 한다. 실제 객체가 초기화 작업을 한다는 것은 객체가 동작하는 것이기 때문에 별도의 초기화 메서드로 분리하도록 설계해야 한다.

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 지닌다. 반면 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다. (객체를 생성하는 부분과 초기화하는 부분을 명확하게 나누는 것이 유지보수 관점에서 훨씬 더 좋다)

객체 생성 후 실제 외부 커넥션 연결하는 것을 최초의 행위가 올 때까지 미룰 수 있다. 생성만 해놓고 기다리다가 최초의 행위가 오면 그 때 초기화를 호출해서 동작을 지연시킬 수 있는 장점도 있다.

참고 : 싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.






📓 빈 생명주기 콜백 지원 3가지 방법

스프링은 이제 의존관계 주입이 끝나서 초기화해도 되거나 스프링 빈이 소멸되기 직전에 커넥션이 미리 닫도록 알려줄 수 있는 방법.

☪ 1. 인터페이스를 통해 지원

InitializingBean 초기화 해주는 빈으로 의존관계 주입이 끝나면 호출해주는 것이다.

public class NetworkClient implements InitializingBean, DisposableBean {
	@Override
    public void afterPropertiesSet() throws Exception {
		//의존관계 주입 끝나면 호출
        connect();
        call("초기화 연결 메시지");
    }
    
    
    @Override
    public void destroy() throws Exception {
    	// 빈 종료될 때 호출
		disconnect();
    }
} 

InitializingBean, DisposableBean 사용함.
지금은 잘 사용하지 않음. 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없고 초기화, 소멸 메서드의 이름을 변경할 수 없다. 해당 코드가 스프링 전용 인터페이스에 의존함.



☪ 2. @Bean(initMethod="init", destroyMethod="close") 처럼 초기화, 소멸 메서드를 지정한다.

public class NetworkClient{
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출 url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

    public void setUrl(String url) {
        this.url = url;
    }

    //서비스 시작 시 호출
    public void connect() {
        System.out.println("connect = " + url);
    }

    public void call(String message) {
        System.out.println("call = " + url + " message = " + message);
    }

    //서비스 종료 시 호출
    public void disconnect() {
        System.out.println("close = " + url);
    }

    public void init() {
        //의존관계 주입이 끝나면 호출하기
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }// 빈이 종료될 때 호출됨
}

public class BeanLifeCycleTest {
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);//빈 조회
        ac.close();
        //ApplicationContext 로 했을 때 close() 지원을 해주지 않으므로
        //ConfigurableApplicationContext 로 바꿔줌 (ConfigurableApp~~ 가 부모, Annota~~ 가 자식)
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        } //호출된 결과물이 스프링 빈으로 등록됨. 빈 이름은 networkClient
    }
}

여기서 장점
-> 1. 메서드 이름을 자유롭게 줄 수 있다.
2. 스프링 빈이 스프링 코드에 의존하지 않는다.
3. 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다. (가장 큰 장점.)

종료 메서드 추론
@Bean의 destroyMethod는 기본 값이 (inferred)로 등록되어 있다.
라이브러리는 대부분 close, shutdown이라는 이름의 종료 메서드를 사용하는데 이 추론 기능은 close, shutdown이라는 메소드를 자동으로 호출해준다.
따라서 직접 스프링 빈으로 등록하면 종료메소드는 따로 적어주지 않아도 잘 작동한다.
(자동으로 종료 메서드를 호출해주지 않았는데 호출이 된다면 @Bean(destroyMethod)를 떠올리면 된다.)



☪ 3. PostConstruct(초기화), PreDestroy(종료) (주로 사용함)

@PostConstruct
    public void init() {
        //의존관계 주입이 끝나면 호출하기
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }

최신 스프링에서 가장 권장
컴포넌트 스캔과 잘 어울린다.
유일한 단점은 외부 라이브러리에는 적용하지 못한다. 그래서 외부 라이브러리를 초기화, 종료해야 하면 @Bean의 기능을 사용하자.
-> init 메서드 사용 @Bean(initMethod="init", destroyMethod="close")






📓 빈 스코프

스프링 빈은 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. (스코프는 빈이 존재할 수 있는 범위.)

싱글톤 : 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.

웹 관련 스코프
request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프이다.
session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다.



☪ 싱글톤 스코프

매우 중요 -> 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
싱글톤 스코프의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 자기가 관리하는 스프링 빈을 반환하는데 이후에 같은 요청이 더 들어와도 같은 객체 인스턴스의 스프링 빈을 반환한다.


☪ 프로토타입 스코프

프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
프로토타입 스코프 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고(들어오는 요청마다 계속 생성함.) 필요한 의존관계를 주입한다. 그리고나서 생성된 빈을 반환을 한다. 이 때 더 이상 스프링 컨테이너에서 관리하지 않는다.


☪ 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.

프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화까지만 관여하고, 더는 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있다. 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.


싱글톤 빈

public class SingletonTest {
    @Test
    void singletonBeanFine() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2);
		//singletonBean1 과 singletonBean2는 같다.
        
        ac.close(); //스프링 컨테이너 종료
    }//파라미터로 class를 넣어주면 SingletonBean 이 자동으로 Componentscan되서 등록이 된다.
    //스프링 컨테이너가 종료될 때 close 종료까지 된다.

    //싱글톤 빈이 있는 설정정보
    @Scope("singleton")
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("Singleton.destroy");
        }
    }
}

프로토타입 빈

public class PrototypeTest {
    @Test
    void prototypeBeanFine() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        ac.close(); //스프링 컨테이너 종료
    }//find prototypeBean1 출력하고 PrototypeBean.init 을 호출한다.
    //find prototypeBean2 출력하고 PrototypeBean.init 을 호출한다. (이 때 새로운 객체 생성되면서 init 호출)
    // 이 2개의 init은 서로 다른 것이다.

    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}
  1. 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행되었지만, 프로토타입 스코프 빈은 스프링 컨테이너 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.

  2. 싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만 프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화까지만 관여하고 더는 관리하지 않는다.
    따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드가 전혀 실행되지 않는다.

  3. 프로토타입 빈을 2번 조회했기 때문에 완전히 다른 스프링 빈이 생성되고 초기화도 2번 실행된다.


그래서 종료 메서드 실행시키고 싶다면 수동으로 직접 호출한다. (해야 할 경우)

protoypeBean1.destroy();
protoypeBean2.destroy();

프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.






📓 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 Provider로 문제 해결

프로토타입 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환했는데 싱글톤 빈과 함께 사용할 때는 의도대로 동작하지 않는 경우가 있다.

@Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2  = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }
    
@Scope("singleton")
    static class ClientBean{
        private final PrototypeBean prototypeBean; //생성 시점에 주입되버림.
        //생성된 것이 할당이 된다.

        @Autowired //이 때 PrototypeBean 요청을 하고 프르토타입 빈을 생성해서 반환한다.
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
        //clientBean1이 logic을 호출하면 이미 생성 시점에 주입된 prototypeBean을 쓴다.
        //clientBean2도 logic을 호출하면 똑같이 prototypeBean을 쓴다.

그러면 clientBean1에서 count=1 clientBean2은 count=2가 된다.
스프링은 일반적으로 싱글톤 빈을 사용하기 때문에 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 새로 생성되기는 하지만 싱글톤 빈과 함께 계속 유지되는 것이 문제다.


프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라 사용할 때마다 새로 생성해서 사용하는 것으로 하고 싶다면 ObjectProvider를 사용하면 된다.

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다

@Scope("singleton")
    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다. getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)


Provider inject 사용
build.gradle에 implementation 'javax.inject:javax.inject:1' 하고나서 Provider로 해주면 된다.

@Scope("singleton")
    static class ClientBean{
        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

get() 메서드 하나로 기능이 매우 단순하지만 별도의 라이브러리가 필요하다.


☪ 프로토타입 빈 언제 사용?

프로토타입 빈은 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용한다.
그런데 대부분 실무에서 싱글톤 빈으로 문제가 해결이 되서 프로토타입 빈을 직접 사용하는 일은 드물다.



싱글톤은 스프링 컨테이너의 시작부터 끝까지 함께하는 매우 긴 스코프이고, 프로토타입은 생성과 의존관계 쥥ㅂ, 그리고 초기화까지만 진행하는 스코프.






📓 웹 스코프

프로토타입은 요청할 때마다 새로 생성됐지만 웹 스코프는 웹 환경에서만 동작하고 스프링이 해당 스코프의 종료시점까지 관리한다.

☪ 웹 스코프 종류

request : HTTP 요청 하나에 맞춰서 들어오고 나갈 때까지의 라이프 사이클 동안 유지되는 스코프. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
session, application, websocket


HTTP request 요청 당 각각 할당되는 request 스코프
클라이언트 A가 요청을 하면 Controller에서 request xcope와 관련있는 객체를 조회한다. 그러면 A의 전용 객체가 생성된다. 만약에 Service에서 객체를 조회했을 때 HTTP request가 같으면 같은 객체 인스턴스를 바라보게 된다. 클라이언트 B가 A와 동시에 들어오면 스프링이 다른 HTTP request임을 인지하고 완전히 별도의 B 전용 객체를 생성한다.
클라이언트 B의 요청이 서비스까지 넘어가서 Service로직에서 B전용 객체를 조회하면 이미 만들어놓은 객체를 반환해준다.
HTTP request에 맞춰서 각각 할당이 된다.
프로토타입과 완전 다름 -> 프로토타입은 요청할 때마다 생성되었지만 HTTP request에 딱 요청이 들어오고 나갈 때까지의 라이프 사이클동안만 같은 것이 관리된다.

정리 -> 클라이언트 A가 요청하면 A전용 스프링 빈이 만들어져서 운영되다가 A의 응답이 나가면 destroy되고 클라이언트 B가 요청하면 B전용 객체가 만들어져서 또 운영되다가 B의 응답이 나갈 때 destroy가 된다.


web 환경이 동작하도록 라이브러리 추가
implementation 'org.springframework.bot:spring-boot-starter-web'

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴 때 request 스코프를 사용한다.
같은 request인 경우 UUID를 남겨서 요청을 구분. requestURL 정보도 추가해서 어떤 URL을 요청해서 남은 로그인지 구분.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);

    }

    @PostConstruct
    public void init() {
        String uuid = UUID.randomUUID().toString();
        //절대 겹치지 않는 유니크한 id가 생성
        System.out.println("[" + uuid + "]" + "request scope bean create: " + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "]" + "request scope bean close: " + this);
    }//request 스코프로 지정을 했고 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다.
}
@Controller
@RequiredArgsConstructor //생성자 만들지 않아도 자동으로 주입되는 어노테이션
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;
    //MyLogger를 주입받는 것이 아니라 MyLogger를 찾을 수 있는 dependency 로거가 주입이 된다.

    @RequestMapping("log-demo") //뷰 화면이 없는 상태
    @ResponseBody //그래서 문자만 반환하도록 함
    public String logDemo(HttpServletRequest request) {//자바에서 제공하는 표준 서블릿 규약으로 request 정보를 받을 수 있다.
        String requestURL = request.getRequestURI().toString(); //고객이 어떤 url로 요청을 했는지 알 수 있다.

        System.out.println("requestURL = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL); //여기서 호출할 때 진짜를 찾아서 동작한다.

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

HttpServletRequest를 통해서 요청 URL을 받았고 받은 requestURL 값을 myLogger에 저장했다. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.


☪ 에러 발생 -> No thread-bound request found**

Scope 'request' is not activate ~~
스프링 컨테이너가 뜰 때 Controller를 스프링 빈으로 등록을 하면 의존관계 주입이 일어난다.
스프링 컨테이너한테 MyLogger를 요청하면 MyLogger는 request scope이기 때문에 request가 존재하지 않는다.
request scope의 생존범위는 HTTP request(고객 요청)이 들어와서 나갈 때까지인데 스프링을 띄우는 단계에서는 HTTP request가 나오지 않았다. 지금 요청이 없는 상테에서 스프링 컨테이너에게 요청을 하니 에러가 발생!!!

스프링 컨테이너에게 빈을 요청하는 단계를 의존관계 주입 단계가 아닌 실제 고객이 왔을 때로 지연시키면(미루면) 된다. (Provider 사용하면 해결 가능)






📓 스코프와 Provider()

컨트롤러 요청이 온 상태에서(HTTP request가 살아있는 상태) myLogger를 호출한다. ObjectProvider로 바꾸기.

@Controller
@RequiredArgsConstructor //생성자 만들지 않아도 자동으로 주입되는 어노테이션
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    //MyLogger를 주입받는 것이 아니라 MyLogger를 찾을 수 있는 dependency logger가 주입이 된다.
    //proxyMode = ScopedProxyMode.TARGET_CLASS 때문에 가짜 MyLogger가 주입됨

    @RequestMapping("log-demo") //뷰 화면이 없는 상태
    @ResponseBody //그래서 문자만 반환하도록 함
    public String logDemo(HttpServletRequest request) {//자바에서 제공하는 표준 서블릿 규약으로 request 정보를 받을 수 있다.
        MyLogger myLogger = myLoggerProvider.getObject(); //필요한 시점에 myLogger 받음

        String requestURL = request.getRequestURI().toString(); //고객이 어떤 url로 요청을 했는지 알 수 있다.

        System.out.println("requestURL = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL); //여기서 호출할 때 진짜를 찾아서 동작한다.

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

LogDemoService에도 Provider 설정.






📓 프록시

가짜 프록시 클래스를 만들어서 주입시켜준다. Provider를 사용하는 것과 똑같이 실행된다.
HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)//가짜 MyLogger를 만든다.


public class MyLogger {
    private String  uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

@Controller
@RequiredArgsConstructor //생성자 만들지 않아도 자동으로 주입되는 어노테이션
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;
    //MyLogger를 주입받는 것이 아니라 MyLogger를 찾을 수 있는 dependency 로거가 주입이 된다.
    //proxyMode = ScopedProxyMode.TARGET_CLASS 때문에 가짜 MyLogger가 주입됨

    @RequestMapping("log-demo") //뷰 화면이 없는 상태
    @ResponseBody //그래서 문자만 반환하도록 함
    public String logDemo(HttpServletRequest request) {//자바에서 제공하는 표준 서블릿 규약으로 request 정보를 받을 수 있다.
        String requestURL = request.getRequestURI().toString(); //고객이 어떤 url로 요청을 했는지 알 수 있다.

        System.out.println("requestURL = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL); //여기서 호출할 때 진짜를 찾아서 동작한다.

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
CGLIB라는 라이브러리를 사용해서 MyLogger를 내 클래스를 상속 받은 가짜 프록시 객체를 생성해서 주입한다.
스프링 컨테이너에 myLogger라는 이름으로 진짜 대신에 클래스로 만들어진 객체가 대신 등록된다.
그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.


가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
가짜 프록시 빈은 내부에 실제 MyLogger를 찾는 방법을 가지고 있다.

Provider를 사용하든 프록시를 사용하든 가장 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 것이다.
프록시 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope을 사용할 수 있다.

profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글