[Spring] 핵심 원리

한대희·2023년 8월 26일

Spring

목록 보기
1/4

✅객체 지향 설계와 스프링

스프링의 핵심

  • 스프링은 자바 언어 기반의 프레임워크
  • 자바 언어의 가장 큰 특징은 < 객체 지향 언어 > 라는 것
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 해주는 프레임 워크

좋은 객체 지향 이란?

  • 먼저 객체 지향 프로그래밍이란 하나의 프로그램을 명령어들의 목록으로 보는 것이 아니라 여러개의 독립된 단위인 "객체"들의 모임으로 보는 것
  • 각각의 객체는 메시지를 주고 받고, 데이터를 처리할 수 있다.
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들어 준다.
  • 유연하고 변경이 용이하다 라는 것은 레고 블럭을 조립하고, 컴퓨터 부품을 갈아 끼우듯이 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 것을 의미한다.
  • 이것이 객체 지향에서 가장 중요한 개념인 "다형성"이다.

다형성이란 ?

  • 다형성은 하나의 역할을 따르는 다향한 구현체를 만들 수 있는 것이라고 생각할 수 있다.
  • 먼저 "역할(인터페이스)"과 "구현(인터페이스를 구현한 클래스)"으로 세상을 구분해 보자.
  • 운전자와 자동차를 이용해서 이해해 보자.
  • 여기서 역할을 보자면 운전자 역할이 있고, 자동차 역할이 있다.
  • 수많은 자동차들이 있는데 이건, 자동차 역할을 구현한 구현체 라고 볼 수 있다.
  • 🥊여기서 중요한 것은 자동차 즉, 구현체가 바뀌어도 운전자가 역할(인터페이스)을 수행하는 것에는 아무런 지장이 없다는 것이다. 차 종류가 바뀌어도 자동차 역할(인터페이스)은 똑같이 하는 것이기 때문에 운전자는 운전을 할 수 있기 때문이다.
  • 🥊이것을 좀 더 구체적으로 보면, 클라이언트(운전자)에게 영향을 주지 않고 새로운 기능을 얼마든지 제공할 수 있다는 것(새로운 차 제공, 확장성)
  • 따라서 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만들면 된다.
  • ❗어떤 역할이 필요하다면 역할을 내부로 가져오고, 해당 역할을 구현 하는 구현체를 선택하는 것으로 생각하면 된다.
// car라는 인터페이스 가 있고 bmw, porche라는 구현체가 있으면

 	public class CarService {
    	// 각각의 구현체의 인스턴스의 반환 타입이 car가 될 수 있다.
        private car car1 = new bmw()
        private car car2 = new porche()
    }

오버라이딩

  • 오버라이딩은 자바 기본 문법으로 메서드를 재정의 하는 것을 의미한다.
  • 예를 들어 Car라는 interface가 있고, 그 안에 start()라는 메서드가 있다고 가정해 보자.
  • 그리고 Car라는 interface의 구현체로 k3와 k5가 있다고 가정을 해 보면 k3와 k5에서 각각 start()메서드를 재정의 하여 해당 구현체에 맞게 기능을 메서드의 기능을 설정할 수 있는 것이다.
  • 이렇게 메서드가 오버라이딩이 되어 있으면, 오버라이딩이 된 메서드가 실행이 되게 된다.
  • 이렇게 해 두면 프로그램 실행 시점에 구현체를 변경하면서 원하는 start()메서드를 사용하게 할 수 있는 것이다.

다형성의 본질

  • 위의 예시 처럼 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있는 것이 다형성의 본질이다.
  • 다형성을 그냥 같은 인터페이스를 따르는 여러 인스턴스를 만들 수 있다고만 생각하지 말고, 항상 "클라이언트" 와 "서버"의 객체 사이의 "협력"이라는 관계를 생각해야 한다.
  • 즉, 🥊클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있는 것이다.
  • 하지만 만약 인터페이스를 변경을 해야 한다면 클라이언트 부터 해당 인터페이스 구현체들 모두 코드를 수정해야 한다.
  • 🥊따라서 인터페이스를 안정적으로 설계하는 것이 가장 중요하다.

객체 지향의 5가지 원칙(SOLID)

SRP: 단일 책임 원칙(single responsibility principle)

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 여기서 책임 이라는 개념은 모호할 수 있으므로 "변경"을 기준으로 이해해야 한다.
  • 즉 어떠한 변경이 있을 때 파급 효과가 적으면 단일 원칙 책임을 잘 따른 것이라고 볼 수 있다.
  • 예를 들면 ui하나를 변경 하는데 sql부터 시작해서 여러가지 코드를 전부 바꿔 줘야 하면 그건 단일 책임 원칙을 지키지 못 한 것이다.

OCP: 개방-폐쇄 원칙 (Open/closed principle)

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 하는 원칙이다.
  • 논리적으로 말이 안된다고 생각할 수 있지만 "다형성"의 개념을 떠올려 보면 이해할 수 있다.
  • 예를 들어 기존의 코드에서 interface를 따르는 새로운 객체 인스턴스를 만들어서 연결했다고 가정을 하면 기존의 코드를 수정하지 않았기 때문에 변경 했다고 볼 수 없고, 새로운 기능을 추가한 것이므르 확장은 된 것으로 볼 수 있다.

LSP: 리스코프 치환 원칙 (Liskov substitution principle)

  • 다형성에서 인터페이스를 따르는 하위 클래스는 인터페이스 규약을 모두 지켜야 한다는 것이다.
  • 예를들어 자동차 라는 인터페이스에서 엑셀을 누르면 앞으로 가라고 했는데, 하위 클래스 에서 뒤로 가게 구현을 해도 컴파일 오류는 나지 않을 것이지만 리스코프 치환 원칙은 깨지게 된다.
  • 따라서 해당 원칙은 컴파일 성공을 넘어서는 원칙이고, 인터페이스를 구현한 구현체를 믿고 사용하기 위해 꼭 필요한 원칙이다.

ISP: 인터페이스 분리 원칙 (Interface segregation principle)

  • 하나의 인터페이스에 너무 많은 기능이 들어가면 안되는 원칙이다.
  • 예를 들어 자동차 인터페이스 하나만 존재하는 것이 아니라 해당 인터페이스를 운전 인터페이스, 정비 인터페이스로 나누는 것이다.
  • 이렇게 하면 클라이언트도 운전자 클라이언트, 정비사 클라이언트로 나뉘게 되고, 만약 정비 인터페이스가 변경이 되면 정비사 클라이언트만 수정하면 되지 운전자 클라이언트는 수정하지 않아도 되어서 인터페이스가 명확해 지고 대체 가능성이 높아지게 된다.

DIP: 의존관계 역전 원칙 (Dependency inversion principle)

  • 이 원칙은 추상화에 의존해야 하고, 구체화에 의존하면 안된다 는 의미이다. 의존성 주입이 이 원칙을 따르는 것 중 하나다.
  • 쉽게 말하면 클라이언트 코드가 구현 클래스를 바라보지 말고 인터페이스만 바로 보게 해야 한다는 것이다.

✅예제

비즈니스 요구 사항

회원

  • 회원을 가입하고 조회할 수 있다.
  • 회원은 일반과 VIP 두 가지 등급이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

주문과 할인 정책

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

회원 도메인 설계

  • 먼저 클라이언트가 서비스를 호출을 할 것이다.
  • 서비스는 회원 가입과 조회 기능을 제공한다.
  • 회원 저장소를 만들어서 데이터에 접근하는 계층을 만든다. 그 이유는 아직 db를 어떻게 할지 정해지지 않았기 때문에 일단 interface를 만들어 놓고, 해당 interface를 따르는 여러 구현체를 만들어서 구현체중 하나를 선택해서 하면 되기 때문이다.
  • 클래스로 표현해 보면 아래와 같다.
MemberService( interface ) ---> MemberServiceImpl( 구현체 )
MemberRepository( interface ) --> MemoryMemberRepository(구현체) 
                              --> DbMemberRepository(구현체)
               
* 일단 메모리에 저장하는 방식으로 개발을 하고 추후에 변경 계획 *

회원 도메인 개발

  • 도메인 별로 폴더를 만들고 그 안에 필요한 클래스, interface를 만든다.
  • 회원 등급을 나타내기 위해 Grade를 enum으로 만든다. enum은 관련 있는 상수들의 집합을 의미한다.
public enum Grade {
    BASIC,
    VIP
}
  • 회원 entity인 Member클래스를 만든다.
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;
    }
}
  • MemberRepository interface를 만든다.
// 회원 저장 기능과 id로 회원을 찾는 기능을 구현할 것이라고 명시하는 것

package springcore.core.member;

public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}
  • MemberRepository interface를 따르는 구현체를 만든다.
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);
    }
}
  • MemberService interface를 만든다.
package springcore.core.member;

public interface MemberService {
    // 회원 가입
    void join(Member member);
    // 회원 조회
    Member findMember(Long memberId);
}
  • MemberService interface를 따르는 구현체를 만든다.
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 테스트 프레임워크를 통한 회원 도메인 테스트

  • test폴더 아래 MemberServiceTest라는 클래스 추가한다.
  • 그리고 junit을 import한다.
  • 아래와 같이 입력 후 실행 시키면 테스트 결과를 콘솔에서 확인할 수 있다.
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);
    }
}

회원 도메인의 문제점

  • 여기까지 하면 문제가 없는 것 같지만 사실 문제가 있다.
  • 문제는 DIP( 의존 관계 역전 원칙 )이 지켜지지 않는 것이다.
  • MemberServiceImpl을 살펴 보자.
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);
    }
}

주문과 할인 도메인 설계

  • 클라이언트는 회원 id, 상품명, 상품 가격등의 정보를 주문 서비스에 전달 하여 주문을 한다.
  • 주문 서비스는 회원 저장소에 가서 해당 회원이 vip인지 아닌지 검증 후 일반 회원이면 원래 가격을, vip면 1000원 할인 된 가격이 적용된 주문 결과를 반환 한다.
  • 일단 정액 할인이지만 추후 정률 할인으로 변경될 수 있다.
  • 클래스 레벨로 살펴 보면 OrderService라는 인터페이스를 따르는 OrderServiceImpl이 있고, MemberRepository라는 interface를 따르는 MemoryMemberRepository가 있고, DiscountPolicy라는 interface를 따르는 구현체가 있을 것이다.

주문과 할인 도메인 개발

  • 먼저 할인 정책 즉, DiscountPolicy interface를 만든다.
  • 나중에 정률 할인으로 변경될 수 있기 때문에 할인 금액을 반환하는 interface가 필요하다.
package springcore.core.discount;

import springcore.core.member.Member;

public interface DiscountPolicy {
    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);

}
  • DiscountPolicy를 따르는 구현체를 만든다.
//❗ 정액 할인 구현체 이므로, 고정 할인 금액을 정하고 회원 등급에 따라 할인 금액을
   리턴

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;
        }
    }
}
  • Order 객체를 만든다.
// 주문을 할 때 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 +
                '}';
    }
}
  • 주문 즉, OrderService 인터페이스를 만든다.



public interface OrderService {
// 사용자가 주문을 하면 Order 객체를 반환하는 메소드
    Order createOrder(Long memberId, String itemName, int itemPrice);
}
  • OrderService를 따르는 구현체를 만든다.
//🥊 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);
    }
}

주문과 할인 도메인 테스트

1. 자바 코드로 테스트

  • OrderApp 클래스를 만든다.
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클래스에 toString 메서드를 넣었기 때문이다.
order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}

2. junit 테스트

  • test폴더에 OrderServiceTest 클래스를 만든다.
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);
    }
}

✅예제에 객체 지향 원리 적용

새로운 할인 정책 개발

  • 위의 예시에서는 vip들에서 고정적으로 1000원을 할인해 주는 정책을 적용하고 있다.
  • 그런데 만약 vip가 주문을 하면 10%를 할인해 주는 방식으로 갑자기 바꿔 달라고 하면 어떻게 해야 할까?
  • 별거 없다. 우리는 그 동안 DiscountPolicy interface를 따르는 FixDiscountPolicy로 할인 금액을 적용했는데 그냥 똑같이 DiscountPolicy interface를 따르는 RateDiscountPolicy라는 구현체를 만들어서 갈아 끼우기만 하면 된다.

RateDiscountPolicy라는 새로운 구현체 생성

  • 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);
    }
}
  • 이제 기존의 FixDiscountPolicy를 RateDiscountPolicy로 대체해 보자.
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);
    }
}

객체 지향 원칙을 지킬 수 있는 방법

  • 그러면 interface에만 의존하게 코드를 수정해 보자.
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);
    }
}
  • ❗ 이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DisCountPolicy라는 interface를 따르는 구현 객체를 대신 생성해서 주입 해 줘야 한다.

생성자 주입

  • 그 방법은 AppConfig라는 설정 클래스를 만드는 것이다.
  • AppConfig에서 구현체를 선택해서 주입시켜 준다.
  • 기존에는 클라이언트 코드에서 사용하고자 하는 Repository나 DiscountPolicy를 new 키워드를 통해 직접 선택 했다면, 이제는 AppConfig에서 new 키워드를 통해 구현체를 선택하고 이걸 클라이언트 코드의 생성자를 통해 주입 하는 것.
  • 그럴려면 먼저 해당 구현체를 사용하는 클래스를 아래와 같이 수정해 줘야 한다.
  • 먼저 MemberServiceImpl을 보자
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);
    }
}
  • 그럼 이제 AppConfig를 보자.
public class AppConfig {
    
    //🥊 여기서 구현체를 선택하여 주입
          MemberServiceImpl 클래스에 생성자를 생성해 두었으므로
          아래와 같이 호출 할 때 전달한 구현체가 MemberServiceImpl에서 
          의존하고 있는 인터페이스에 할당 될 것이다.
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

}
  • OrderServiceImpl의 코드도 수정해 보자.
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);
    }
}
  • AppConfig도 수정한다.
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

의존관계 주입 ( Dependency Injection )

  • AppConfig의 역할을 다시 한번 살펴 보자.
  • AppConfig에서는 어플리케이션에 필요한 구현 객체를 생성 하고 생성자를 통해 주입을 해주는 역할을 하는 것이다.
  • 이렇게 하면 MemberServiceImpl과 OrderServiceImpl은 interface에만 의존하게 되고, 생성자를 통해 어떤 구현 객체가 주입 될 지 알 수 없게 된다.
  • 어떤 구현 객체가 주입 될 지는 오직 외부(AppConfig)에서만 결정이 되기 때문에 클라이언트 코드인 MemberServiceImpl과 OrderServiceImpl는 의존관계에 대한 고민은 외부에 맡기고 그저 실행에만 집중하면 된다.
  • 즉, 의존 관계 주입이란 어떤 구현체를 사용할지 new 키워드를 통해 직접 선택하는 것이 아니라 외부에서 구현체를 주입 받는 것!

AppConfig 실행

  • 기존의 MemberApp을 아래와 같이 수정해 보자.
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());
    }
}
  • OrderApp도 아래와 같이 수정해 준다.
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);
    }
}
  • OredrServiceTest
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);
    }
}

AppConfig 리팩토링

  • 먼저 기존의 AppConfig파일을 보면 역할이 뭐고, 구현이 뭔지 한눈에 알아보기가 힘들다.
  • MemoryMemberRepository라는 구현체는 있지만 이거의 역할 즉, MemberRepository라는 인터페이스는 없다.
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
  • 따라서 아래와 같이 수정해 주면 어떤 역할들이 있고, 그 역할을 구현하고 있는 것은 무엇인지 한눈에 알 수 있다.
  • 또한 new MemoryMemberRepository() 부분이 중복 되어 있었는데 이걸 제거 함으로써 MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
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();
    }
}

새로운 할인 정책 적용 하기

  • FixDiscountPolicy를 RateDiscountPolicy로 바꿔 보자
  • 기존에 AppConfig를 만들어 두었기 때문에 AppConfig만 수정하면 된다.
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();
    }
}
  • 이렇게 구성영역(AppConfig)과 사용영역(Service)을 구분해 두면 사용영역의 어떠한 코드 변경도 없이 AppConfig만 변경을 해주면 된다.

객체 지향 설계의 원칙 정리

  • 지금까지 객체 지향의 원칙 중 SRP, DIP, OCP 원칙을 준수하여 개발 해왔다. 하나하나 다시 살펴 보자

  • SRP ( 단일 책임 원칙 )

    • 한 클래스는 하나의 책임만 가져야 한다.
    • 구현 객체를 생성하고 연결하는 책임은 AppConfig에서담당 하게 하고, 클라이언트 객체는 실행하는 책임만을 담당 하게 하여 SRP원칙을 준수 하였다.
  • DIP ( 의존관계 역전 원칙 )
    • 추상화에만 의존 해야 하며, 구체화에 의존하면 안된다는 원칙
    • 의존성 주입이 이 원칙을 따르는 방법 중 하나다.
    • 클라이언트에서는 interface에만 의존하게 하고, interface만으로는 아무것도 할 수 없으니 AppConfig에서 인스턴스를 클라이언트 코드에 주입하게 하여 DIP원칙을 준수 하였다.
  • OCP ( 개방 - 폐쇄 원칙 )
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다는 원칙
    • AppConfig에서 구현 객체를 클라이언트로 바꿔 가면서 넘기기 때문에 클라이언트 코드의 어떠한 수정도 없이 기능을 확장 할 수 있었고, 클라이언트 코드 변경이 없으니 변경에는 닫혀 있게 하여 OCP원칙 준수 하였다.

IOC / DI / DI컨테이너

IOC

  • IOC는 Inversion of Controll의 약자로 제어의 역전을 의미한다.
  • 보통은 개발자가 직접 원하는 대로 객체를 생성 하고, 호출 하면서 직접 제어를 하게 되는데 제어의 역전 이라는 것은 개발자가 호출하지 않았지만 프레임워크 같은 것이 내 코드를 대신 호출해 주는 것을 말한다. 이렇게 제워권이 뒤바뀌었다 하여 제어의 역전 이라고 한다.
  • 기존의 프로그램에서는 클라이언트 구현 객체 즉, 클라이언트 코드인ServiceImpl에서 스스로 필요한 구현 객체를 생성, 연결, 실행 했다. 다시 말해 구현 객체가 프로그램의 제어 흐름을 스스로 조종 했다.
  • 하지만 AppConfig가 등장한 이후 클라이언트 코드에서는 interface만을 의존하게 하기 때문에 어떤 구현 객체가 실행될지 전혀 모른다. 프로그램에 대한 제어의 흐름은 모두 AppConfig에서 조종하는 것이다.
  • 이렇게 프로그램의 제어 흐름을 AppConfig 즉, 외부에서 관리하게 하는 것을 제어의 역전 IOC라고한다.

DI

  • DI는 Dependency Injection의 약자로 의존관계 주입을 의미한다.
  • 의존 관계는 정적인 클래스 의존관계 와 동적인 객체(인스턴스) 의존 관계를 분리해서 생각해야 한다.
  • 정적인 클래스 의존 관계
    • 정적인 의존 관계는 그냥 클래스 내부에서 뭐를 불러와서 사용하고 있냐를 의미한다.
    • 쉽게 말하자면 무엇을 import 하고 있냐를 보면 정적인 의존 관계를 파악 할 수 있다.
    • 예를 들면 이전의 클라이언트 코드에서 인터페이스만 불러와 사용하고, 해당 인터페이스를 따르는 구현체는 생성자를 통해 주입 받았다.
    • 따라서 애플리케이션이 실행되지 않으면 어떤 구현체가 들어 오는지 알 수가 없다.
    • 즉, 정적 클래스 의존 관계는 애플리케이션을 실행하지 않아도 알 수 있는 현재 import 코드만 보고 확인할 수 있는 것 이라고 생각하면 된다.
  • 동적인 객체 (인스턴스) 의존 관계
    • 동적 객체 의존 관계 라는 것은 애플리케이션 실행 시점에 실제 생성된(주입된) 인스턴스(구현체)의 참조가 연결된 의존 관계다.
    • 애플리케이션이 실행 되면 구현 객체가 생성 되고, 클라이언트에게 주입 하여 클라이언트와 서버의 실제 의존 관계가 연결 되는 것을 의존 관계 주입이라고 한다.
    • 이렇게 객체 인스턴스가 생성 되고, 참조값이 전달 되어 연결 되는 것이 동적인 의존 관계다.

IOC 컨테이너 / DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 의존 관계를 연결해 주는 것을 IOC컨테이너 또는 DI컨테이너 라고 한다.
  • 의존 관계 주입에 초점을 맞춰 최근에는 주로 DI 컨테이너 라고 한다.
  • 어샘블러, 오브젝트 팩토리 라고 불리기도 한다.

✅ 스프링으로 전환하기

  • 지금 까지는 순수 JAVA 코드로 작업을 해왔다. 이제 스프링으로 전환해 보자.

스프링으로 전환

  • 먼저 AppConfig 파일을 수정해 보자. (@Configuration)
//🥊 먼저 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();
    }
}
  • MemberApp을 수정해 보자. (ApplicationContext)
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());
    }
}
  • OrderApp을 수정해 보자.
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 applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)
  • ApplicationContext는 인터페이스 이고, 그것을 따르는 구현체 중 AnnotationConfigApplication 이 있는 것이다.
  • 이전에 AppConfig에 @Configuration 어노테이션을 붙였기 때문에 Annotation기반의 설정 파일이다 라는 의미.
  • 이렇게 만들어진 스프링 컨테이너 안에는 스프링 빈 저장소 라는 것이 있다.
  • 스프링 컨테이너를 만들 때 전달한 AppConfig.class라는 구성정보를 스프링 컨테이너가 아래와 같이 빈 객체로 저장 한다.

  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 해줬지만, 이제 부터는 스프링 컨테이너를 통해서 사용하게 된다.
  • 스프링 컨테이너는 @Configuration이 붙은 것을 구성 정보로 사용하게 되고, @Bean이라고 적힌 메서드들 모두 호출 해서 반환 된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨네이너에 등록된 객체를 스프링 빈이라고 한다.
  • 주의할 점은 빈 이름은 항상 다른 이름을 부여 해야 한다는 것이다. 만약 같은 이름을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어 버리는 오류가 발생한다.
  • 스프링 빈으로 등록 될 때는 @Bean이 붙은 메서드의 이름으로 등록이 되고 각각의 메서드가 반환하는 값이 등록된 빈의 값이 된다.
  • 또한 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다.
  • 이렇게 스프링 컨테이너에 스프링 빈으로 등록이 된 다음 부터는 스프링 컨네이너를 통해서 필요한 객체 즉, 스프링 빈을 찾아야 한다.
  • 스프링 빈은 getBean 메서드를 통해 찾을 수 있다.

등록된 빈 확인하기

  • 위에서 스프링 컨네이너에 빈이 잘 등록이 되었는지 확인하기 위해 테스트 코드를 작성해 보자.
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 );
            }
        }
    }

빈 조회 메서드 정리

  • getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
  • getBean() : bean으로 등록된 이름을 인자로 넣어 해당 bean의 값을 가져온다.
  • getRole() : 해당 메서드를 통해 스프링 내부에서 사용하는 빈과 사용자가 정의한 빈을 구분하여 출력할 수 있다.
    • ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE : 스프링 내부에서 사용하는 빈

다양한 상황에서의 빈 조회 방법

기본적인 빈 조회 방법

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
  • 테스트 코드를 만들어 보자.
   // 가져온 빈이 해당 클래스의 인스턴와 같은지 확인
    @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);
    }
}

상속 관계에 있는 빈 조회

  • 빈 조회를 할 때 만약 빈들이 상속관계로 되어 있다면, 예를들어 어떤 부모 타입으로 조회를 했는데 자식이 여러개 있다면 자식 빈들이 모두 조회가 된다.
  • 따라서 모든 자바 객체의 최고 부모인 Object타입으로 조회하면 모든 스프링 빈을 조회하게 되는 것.
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

  • BeanFactory는 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • 지금까지 우리가 사용했던 대부분의 빈과 관련된 기능은 BeanFactory가 제공하는 기능이다.

ApplicationContext

  • ApplicationContext는 BeanFactory의 기능을 모두 상속받은 인터페이스이다.
  • BeanFactory에서 더 기능이 추가된 것이라고 보면 된다.
  • 이것이 필요한 이유는 애플리케이션을 개발할 때 빈을 관리하고 조회하는 기능 뿐 아니라 수 많은 부가기능이 필요하기 때문이다.
  • ApplicationContext는 BeaqnFactory 뿐 아니라 MessageSource, EnvironmentCapable등 여러가지 인터페이스를 상속 받고 있다.
  • BeanFactory와 ApplicationiContext를 보통 스프링 컨테이너라고 하는데, 그냥 ApplicationContext를 사용한다고 보면 된다.

다양한 설정 형식 지원

  • 스프링 컨테이너는 자바, XML, Groovy 등 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.

자바 코드 설정 사용

  • 지금까지 했던 것 처럼 AnnotationConfigApplicationContext에 AppConfig.class를 넘기면 된다.

XML 설정 사용

  • XML을 사용할 경우 GenericXmlApplicationContext에 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 설정 파일
<?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>

✅싱글톤 컨테이너

  • 스프링은 기업용 온라인 서비스를 지원하기 위해 탄생 하였다.
  • 온라인 서비스를 사용하는 사용자는 여러명일 것이고, 그렇게 되면 여러 사용자가 동시에 요청을 하게 될 것이다.
  • 사용자가 memberService에 요청을 하면 아래의 기존에 만들어 둔 AppConfig에서 객체를 반환을 하여 의존성 주입을 해줄 텐데, 이때 서로 다른 사용자가 같은 요청을 한다면 똑같은 객체를 여러번 반환하게 되는 일이 발생 한다.
@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);
    }
}
  • 이처럼 스프링 없는 순수한 DI컨테이너인 AppConfig는 같은 요청임에도 요청을 할 때마다 새로운 객체를 생성하여 반환 한다.
  • 이것을 해결하기 위해서는 해당 객체가 1개만 생성이 되게 하고, 이걸 공유하도록 하면 된다.
  • 이것이 싱글톤 패턴이다.

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • private 생성자를 사용해서 외부에서 임의로 new 키워드를 통해 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
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();
    }
}
  • 이렇게 싱글톤 패턴을 구현을 하고 AppConfig에 가서 getinstance메서드롤 통해 객체를 가져와 리턴하여 의존성 주입을 해주면 된다.
@Configuration
public class AppConfig {


    // Bean 어노테이션을 붙여 주면 이것 들이 스프링 컨테이너에 등록이 된다.
    // MemberService 역할
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl.getInstance();
    }
                   .......
}
  • 하지만 사실 이렇게 할 필요가 없다. 그 이유는 스프링 컨테이너가 기본적으로 객체를 다 싱글톤으로 관리해 주기 때문이다.
  • 싱글톤 컨테이너를 알아보기 전에 싱글톤 패턴의 문제점에 대해 알아보자.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다.위의 SingletoneService클래스를 보면 싱글톤 패턴으로 구현하기 위해 내부에
    new SingletoneService()로 인스턴스를 생성한 것을 확인할 수 있다.
  • 인스턴스를 미리 생성해버러기 때문에 유연한 테스트가 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자를 사용하기 때문에 자식 클래스를 만들기 어렵다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
  • 지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리 라고 한다.
  • 스프링 컨테이너의 이러한 기능 덕분에 싱글턴 패턴의 단점을 해결 하면서 객체를 싱글톤으로 유지할 수 있다.
  • 따라서 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
  • 스프링 컨테이너를 사용하는 테스트 코드를 만들어 보자.
    @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);
    }
  • 이처럼 스프링 컨테이너를 사용하면 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용 할 수 있다.
  • 스프링 컨테이너를 사용하면 기본적으로 싱글톤으로 동작한다고 이해할 수 있다.

싱글톤 방식의 주의점

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

상태를 유지할 경우 발생하는 문제점

  • 먼저 서비스를 만들어 보자
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();
        }
    }
}
  • 위의 결과로 price를 받아 보면 20000원이 나온다.
  • 그 이유는 스프링 컨테이너는 싱글톤으로 객체를 관리하기 때문에 statefulService.order와 statefulService1.order는 같은 인스턴스를 사용한다.
  • 따라서 A사용자가 주문을 하고, 주문금액을 조회 하는 사이에 B사용자가 주문을 하여 필드의 price가 변경이 되었기 때문이다.
  • 이처럼 클라이언트가 값을 변경할 수 있는 필드가 있으면 큰 문제가 발생할 수 있다.

무상태로 설계

  • 무상태로 설계하기 위해 필드 대신 공유되지 않는 지역변수, 파라미터, ThreadLoacal 등을 사용해야 한다.
  • 간단하게 지역변수로 해결해 보자.
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 과 싱글톤

  • 이전에 App Config 클래스에 사용 했던 @Configuration은 사실 싱글톤을 위해 존재하는 것이다.
  • App Config 파일을 살펴보자.
@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();
    }
}
  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해 주어야 한다.
  • 하지만 스프링이 자바 코드까지 어떻게 하기는 어렵기 때문에 위의 AppConfig에서 new MemberRepository가 2번 호출 되면 2개가 서로 다른 인스턴스 여야 한다.
  • 이 문제를 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 통해 해결한다.
  • 과정을 살펴 보면 AnnotationConfigApplicationContext에 AppConfig.class를 파라미터로 넘겼었다.
  • 사실 이때 파라미터로 넘긴 값도 스프링은 빈으로 등록한다.
  • 이때 스프링이 그냥 빈으로 등록하는 것이 아니라 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 파라미터로 전달 받은 클래스를 상속받은 임의의 클래스를 만들고, 그 클래스를 빈으로 등록한다.
  • 여기서 @Bean이 붙은 메서드 마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 빈으로 등록하고 반환하는 코드가 동적으로 만들어 지는 것.
  • 이렇게 싱글톤이 보장 되는 것이다.
  • 그렇다면 만약 @Configuration을 적용하지 않고 @Bean만 적용하면 어떻게 될까?
  • @Bean만 사용해도 스프링 빈으로 등록이 되지만 내부의 자바 코드가 그대로 실행되어 싱글톤을 보장하지 않는다.
  • 크게 고민 할 것 없이 스프링 설정 정보에는 항상 @Configuration을 붙이자.

✅컴포넌트 스캔

컴포넌트 스캔이란 ?

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 bean 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • AppConfig에서 메서드 이름을 key이름으로 하고, 메서드의 리턴 값을 value로 하여 빈을 등록 했었다.
  • 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생한다.
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.
  • 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.
  • @ComponentScan을 붙이면 @Component 어노테이션이 붙은 클래스를 스캔해서 자동으로 스프링 빈으로 등록해 준다.
// 설정 파일이니까 Configuration 붙여주고, ComponentScan을 붙여 준다.
// ComponentScan을 붙이면 Component 어노테이션이 붙은 클래스를 찾아서 
   자동으로 스프링 빈으로 등록해 준다.
// 아래 괄호 안에 내용은 @Configuration이 붙은 것은 컴포넌트 스캔에서 
   제외한다고 한 것인데 그 이유는 컴포넌트 스캔 사용시 @Configuration이 
   붙은 설정 정보도 같이 등록 되기 때문에 이전의 AppConfig예제와의 
   충돌을 막기 위한 것. 
   보통은 설정 정보를 컴포넌트 스캔 대상에서 제외하지 않는다.

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

}
  • 위의 코드를 보면 이전의 AppConfig와 달리 설정 정보가 하나도 없다.
  • 이제 빈으로 등록하고 싶은 대상에 @Component 어노테이션을 붙여주자.
  • MemoryMemberRepository @Component 추가
@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);
    }
}
  • RateDiscountPolicy @Component 추가
@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;
        }
    }

}
  • MemberServiceImpl @Component, @Autowired 추가
@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);
    }
}
  • OrderServiceImpl @Component, @Autowired 추가
@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);
    }
}

컴포넌트 스캔 정리

  • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 빈으로 등록되는 key값은 클래스 명을 사용하되 맨 앞글자만 소문자로 변환되어 등록된다.
  • 만약 빈 이름을 직접 지정하고 싶으면 아래와 같이 괄호에 입력해 주면 된다.
@Component("memberService2") 

자동 의존관계 주입 정리

  • 생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 생성자에 파라미터가 많아도 전부 찾아서 자동으로 주입한다.

탐색 위치와 기본 스캔 대상

탐색 위치 설정

  • @ComponentScan을 붙이면 모든 자바 클래스를 검사해서 @Component가 붙은 클래스를 빈으로 등록하게 되는데, 모든 클래스를 검사 하다보니 시간이 오래 걸린다.
  • 따라서 꼭 필요한 위치부터 탐색하도록 아래와 같이 basePackages를 통해 시작 위치를 지정할 수 있다.
// basePackages는 탐색할 패키지의 시작위치를 지정한다.
// basePackages = {"hello.core", "hello.service"} 와 같이 여러개의
   시작 위치를 지정할 수도 있다.

@ComponentScan(
 	basePackages = "hello.core",
}
  • basePackages 대신 basePackageClasses를 사용하면 지정한 클래스가 속해 있는 패키지를 탐색 위치로 지정한다.
  • 만약 아무것도 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
  • 권장되는 방법으로는 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 둔 다음 아무것도 지정하지 않은 채 사용하는 것이다.
  • 예를 들면 아래와 같다.
// 프로젝트 구조가 아래와 같다면
// com.hello에 AppConfig 같은 메인 설정 정보를 두고 @ComponentScan을 붙이고,
   basePackages 지정은 생략하는 것이다.
// 이렇게 하면 com.hello를 포함한 하위의 모든 클래스는 컴포넌트 스캔의 대상이 된다.
com.hello
com.hello.serivce
com.hello.repository

기본 스캔 대상

  • 컴포넌트 스캔은 @Component뿐만 아니라 아래 내용도 추가로 포함한다.
@Component : 컴포넌트 스캔에서 사용
@Controller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용
  • 참고로 useDefaultFilters 옵션이 기본적으로 켜져 있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.

필터

  • 필터를 사용하면 컴포넌트 스캔 대상을 추가하고 제외할 수 있다.

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.

  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.

✅ 의존관계 자동 주입

  • 의존 관계 주입 이라는 것은 애플리케이션이 실행될 때 구현체들을 정하는 것이라고 생각하면 된다.

다양한 의존관계 주입 방법

  • 의존관계 주입은 크게 4가지 방법이 있다.
  • 생성자 주입, 수정자 주입(setter),필드 주입, 일반 메서드 주입이 그것이다.

생성자 주입

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
  • 지금까지 우리가 진행헸던 방법이 바로 생정자 주입이다.
  • 생성자 주입은 생성자 호출시점에 딱 한번 호출되는 것이 보장되어 불변, 필수 의존관계에 사용된다.
  • 그 이유는 생성자를 통해서 의존관계를 주입 받으면 외부에서 수정할 수 있는 방법이 없기 때문이다.
// @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 주입 )

  • 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;
   }
}
  • 참고로 @Autowired는 주입할 대상이 없으면 오류가 발생한다. 만약 주입할 대상이 없어도 동작하게 하려면 @Autowired( required=false )로 지정하면 된다.

필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법이다.
@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 옵션의 기본값이 true이기 때문에 자동 주입 대상이 없으면 오류가 발생한다.
  • 자동 주입 대상을 옵션으로 처리하는 방법은 아래와 같다.
@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으로 열려 있기 때문에 누군가 실수로 변경할 수도 있게 된다.
  • 이처럼 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 따라서 생성자 주입을 통해 객체가 생성이 될 때 딱 한번만 호출하여 불변한 상태를 유지하게 하는 것이 좋다.
  • 또한 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그 이유는 final 변수는 변하지 않는 값이기 때문에 직접 값을 넣어주거나 생성자를 통해서만 넣을 수 있다.
  • setter와 같은 메서드는 객체가 생성된 다음 호출 되기 때문에 메서드로는 final 변수에 의존 관계를 주입할 수 없다.
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;
 }

}
  • 또한 생성자 주입을 통해 의존 관계 주입을 하면 뭔가 코드상 문제가 생겼을 때 컴파일 오류를 발생시키기 때문에 애플리케이션 실행 단계 에서 오류가 발생 하는 것을 막을 수 있다.
  • 정리하면 생성자 주입을 통해 의존관계를 주입하면 final 키워드를 사용할 수 있고, 컴파일 오류를 받아 볼 수 있으며, 외부에서 수정이 불가능 하게 할 수 있는 것이다.

lombok 라이브러리

  • 개발을 진행하다 보면 대부분의 변수들이 불변이다.
  • 따라서 필드에 final 키워드를 많이 사용하게 되는데 이때 하나하나 생성자를 통해 값을 주입해야 하다 보니 조금 귀찮은 부분이 있다.
  • 이것을 간편하게 해주는 것이 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 활용
// lombok 라 이브러리가 제공하는 @RequireArgsConstructor를 활용하면
   final이 붙은 필드를 모두 찾아 위의 코드 처럼 생성자를
   자동으로 생성해 준다.
   따라서 아래의 코드 처럼 깔끔하게 입력할 수 있다.
  

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
 private final MemberRepository memberRepository;
 private final DiscountPolicy discountPolicy;
}

조회 빈이 2개 이상인 경우

  • 이전에 @Autowired를 통해 자동으로 의존성이 주입 된다고 배웠다.
  • 하지만 @Autowired의 경우 타입으로 빈을 조회해서 의존성 주입을 한다.
  • 이전의 코드를 다시 보자.
@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;
   }
}
  • 이렇게 타입으로 조회를 하기 때문에 만약 같은 타입의 빈이 2개 이상이라면 문제가 발생한다.
  • 방법은 크게 3가지가 있다.

@Autowired 필드명 매칭

  • @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 {

  • 이제 의존성 주입을 할 때 @Qualifier를 붙여 등록된 @Qualifier와 매칭한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
 @Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}
  • 그런데 만약 @Qualifier로 의존성 주입을 할 때@Qualifier("mainDiscountPolicy") 를 못찾으면 어떻게 될까?

  • 그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다

  • 이것도 없으면 에러가 발생한다.

@Primary

  • @Primary는 우선순위를 정하는 방법이다.
  • @Autowired 시에 여러 빈이 매칭되면 @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

  • 의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.

✅ 빈 생명주기 콜백

profile
개발 블로그

0개의 댓글