Spring

스프링 기본편 2

woohap 2025. 1. 10. 22:20

노션 링크
https://earthy-grouse-d42.notion.site/1765723e06ee80adb19ddd059cde05e5

기존 코드 문제점

DIP 위반

OrderServiceImpl는 추상화인 MemberRepository와 DiscountPolicy에 의존하고 있지만 
또한 MemoryMemberRepository, RateDiscountPolicy 구현 객체에도 의존하고 있으므로 DIP를 위반하고 있다. 
-> 즉 구현 객체를 직접 참조하고 있으므로 DIP를 위반하고 있음

OCP 위반

할인 정책을 FixDiscountPolicy에서 RateDiscountPolicy를 변경할 때 
클라이언트 코드가 변경되었으므로 OCP 위반 
public class OrderServiceImpl implements OrderService{

    // 직접 참조하고 있으므로 추상화말고도 구현체에도 의존하고 있는 상황
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    // 할인 정책 역할의 구현체를 변경하는데 클라이언트 코드가 변경되었으므로 OCP 위반 
//  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);
    }
}

DIP 해결 하지만...

// 직접 참조하는 코드를 단순히 제거 
// private DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy; 

// 문제 발생 -> 인터페이스만으로는 아무것도 할 수 없음 (NullPointerException 발생)
// 이를 해결하기 위해서 누군가 구현 객체를 대신 생성해서 주입해줘야 한다.

AppConfig 등장

현재 코드의 경우 OrderServiceImpl이 다양한 책임을 가지고 있음 
1. 본인 역할(인터페이스)에 대한 로직 수행 (실행 영역)
2. 다른 역할(인터페이스)에 대한 구현체를 설정하고 있음 (구성 및 연결 영역)

관심사 분리 필요 !!!!
[AppConfig] 
애플리케이션의 전체 동작 방식을 구성하기 위해 
구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스 
즉, 애플리케이션 전체를 설정하고 구성함 

과정 
1. OrderServiceImpl 구현체 내부에서 MemberRepository, DiscountPolicy의 구현체를 
   생성자를 통해서 주입할 수 있도록 코드 변경 

   private final MemberReposiory memberRepository;
   private final DiscountPolicy discountPolicy;

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

2. AppConfig에서 구현체를 선택하여 주입하도록 구현 

   public OrderService orderService() {
       return new OrderServiceImpl(new MemoryMemberRepository, new RateDiscountRepository);
   }

** 해당 방식으로 의존성을 주입해줌으로써
   OrderServiceImpl는 자신의 핵심 로직에만 집중하고 
   구체적인 의존 대상에 대한 결정은 외부(AppConfig.class)에서 맡도록 함
   즉, OrderServiceImpl는 실행 역할에 책임을 AppConfig는 객체를 생성하고 의존관계 연결 책임을 가짐

** AppConfig 등장으로 애플리케이션은 크게 사용영역과 객체를 생성하고 구성하는 영역으로 분리
   할인 정책을 변경해도 구성 영역만 영향을 받고, 사용 영역은 영향을 받지 않게 됨 
   즉, OCP, DIP 모두 만족하게 됨 

정리

- AppConfig를 통해서 관심사를 확실하게 분리
- 배역, 배우를 생각
  AppConfig는 공연 기획자
  AppConfig는 구체 클래스를 선택 ( 배역에 맞는 담당 배우를 선택 )
  애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다. (구현체 선택하고 의존관계 연결을 통해)
  각 배우들은(각 역학들은) 담당 기능을 실행하는 책임만 지면 된다.

AppConfig 리팩토링

// new MemoryMemberRepository() 중복
public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
        // MemberRepository 역할이 잘 보이지 않음 
    }

public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
현재의 AppConfig를 보면 코드 중복이 존재하며, 역할에 따른 구현이 직관적이지 않다. 
역할에 따른 구현이 잘 들어나도록 리팩토링 수행 
- 코드만 봐도 역할을 알 수 있도록
- 메서드명만 봐도 역할이 다 들어나도록
- 역할에 어떤 구현체를 쓰는지 잘 들어나도록
public MemberService memberService() {
    return new MemberServiceImpl(memberRepository());
}

// 나중에 DB로 바꿀 때 return new MemoryMemberRepository(); 부분만 변경하면 됨
private MemberRepository memberRepository() {
    return new MemoryMemberRepository();
}

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

public DiscountPolicy discountPolicy() {
    return new FixDiscountPolicy();
}

IoC, DI, 컨테이너

제어의 역전 
- 간단하게 말하면 프레임워크가 내 코드를 대신 호출해주는 것 

- 기존 프로그램은 사용자 코드가 객체를 생성하고 연결하고 실행 
  즉, 사용자 코드가 프로그램의 제어 흐름을 관리

- AppConfig가 등장한 이후 구현 객체(사용자 코드)는 자신의 로직을 실행하는 역할만 담당 
  프로그램 제어 흐름은 이제 AppConfig가 담당 
  Ex) OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모름

- 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.
  심지어 OrderServiceImpl도 AppConfig가 생성
  AppConfig는 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 있다.
  그런 사실도 모른체 OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐이다.

프레임워크 VS 라이브러리

- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 (Junit)
  Junit의 경우 개발자가 로직을 작성하면 로직에 대한 실행과 제어권을 Junit가 가져감 (대신 실행)
- 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 라이브러리

의존관계 주입

- OrderServiceImpl은 DiscountPolicy(인터페이스)에 의존한다.
  실제로 어떤 구현 객체가 사용될지 OrderServiceImpl은 모른다.

- 의존관계는 정적인 클래스 의존관계와 런타임 시점에 결정되는 동적인 객체 의존관계들로 분리해서 생각

정적인 클래스 의존관계

- 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
  애플리케이션을 실행하지 않아도 분석 가능 

동적인 객체 의존관계

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

- 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서
  클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입(DI)라고 한다.

- 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결한다. 

- 의존관계 주입을 통해 클라이언트 코드를 변경하지 않고 
  클라이언트가 호출하는 대상의 타입 인스턴스 변경 가능 

- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고
  동적인 객체 인스턴스 의존관계를 쉽게 변경 가능 
  즉, 클래스 다이어그램 변경 없이, 객체 다이어그램만 변경하면 된다는 의미 

Ioc 컨테이너, DI 컨테이너

Ioc를 해주는 컨테이너 - Ioc 컨테이너
DI를 해주는 컨테이너 - DI 컨테이너 

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

스프링 컨테이너

- ApplicationContext를 스프링 컨테이너라고 한다.

- 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 수행

- 스프링 컨테이너는 @Configuration이 붙은 설정 클래스를 사용
  여기서 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록
  컨테이너에 등록된 객체를 스프링 빈이라고 한다.

- 스프링 빈은 기본적으로 @Bean이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다.
  이전에는 직접 필요한 객체를 AppConfig를 사용해서 조회했지만
  스프링 컨테이너를 통해서 필요한 스프링 빈을 찾아야 한다.
  스프링 빈은 getBean() 메서드를 사용해서 찾을 수 있다.

- 스프링이 환경 정보를 가지고 필요한 것들을 읽어서 스프링 컨테이너에 등록하고 관리
  필요하면 스프링 컨테이너에서 꺼내서 사용 

내용 출처

인프런 - 스프링 핵심 원리(기본편)
강사 - 김영한

'Spring' 카테고리의 다른 글

스프링 기본편 - 6  (1) 2025.01.19
스프링 기본편 5  (0) 2025.01.14
스프링 기본편 4  (0) 2025.01.13
스프링 기본편 3  (0) 2025.01.11
스프링 기본편 1  (0) 2025.01.09