[스프링 핵심 원리] 2

smj_716·2025년 1월 10일

스프링 완전 정복

목록 보기
5/16
post-thumbnail

1. 예제 만들기

비지니스 요구사항과 설계

💡지금은 스프링 없는 순수한 자바로만 개발을 진행한다.

➡️ 회원

  • 회원은 가입하고 조회 가능
  • 회원은 일반과 VIP 두 가지 등급이 있음
  • 회원 데이터는 자체 DB 구축과 외부 시스템 연동 가능 (미확정)
설명

➡️ 주문과 할인 정책

  • 회원은 상품을 주문할 수 있음
  • 회원 등급에 따라 할인 정책을 적용할 수 있음
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용
  • 할인 정책은 변경 가능성이 높음

회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다.
즉, 협력 관계를 그대로 재사용 할 수 있다.

코드는 깃허브에 기록한 것 참고하기! https://github.com/MinjiSeo16/inflearnSpring/tree/main/core


2. 객체 지향 원리 적용

1) 새로운 할인 정책 개발

이번에는 주문한 금액의 %를 할인해주는 새로운 정률 할인 정책추가하자
할인 정책을 변경하려면 OrderServiceTmpl 코드를 고쳐야 한다. (주석은 이전 코드)

public class OrderServiceImpl implements OrderService {
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 }

📌 문제점

  • 역할과 구현을 충실하게 구분 ? -> 🆗
  • 다형성도 활용, 인터페이스와 구현 객체를 분리 ? -> 🆗
  • OCP 준수 ? -> ⚠️OCP는 변경하지 않고 확장 가능해야하지만 지금 코드는 기능을 확장하면(새로운 할인 정책을 도입하면) 클라이언트 코드 (OrderServiceImpl)에 변경이 일어나 위반됨
  • DIP 준수 ? -> ⚠️ 주문 서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy인터페이스에 의존하면서 동시에 RateDiscountPolicy와 FixDiscountPolicy도 의존함.
    즉, 추상(인터페이스) 뿐만 아니라 구체(구현)클래스에도 의존하고 있어 위반됨

💡 해결방안?

인터페이스에만 의존하도록 코드를 변경한다.

public class OrderServiceImpl implements OrderService {
    //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
      private DiscountPolicy discountPolicy;
 }

그런데 구현체가 없는데 코드는 실행할 수 있을까?
⚠️ 실제 실행해보면 NPE(null pointer exception)이 발생한다.
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현체를 대신 생성하고 주입해주어야 한다.


2) 관심사의 분리

애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배우 역할이라 생각하자. 그런데 로미오 역할(인터페이스)을 하는 디카프리오(구현체,배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체,배우)를 직접 초빙한다면 디카프리오는 공연도 하고 초빙도 하는 다양한 책임을 가지게 된다.

배우는 본인의 역할인 배역에만 집중해야함 -> AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!

🖥️ AppConfig

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

AppConfig는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다. 아래는 MemberServiceImpl와 OrderServiceImpl에 생성자 코드를 추가한 것이다.

🖥️ MemberServiceImpl

 public class MemberServiceImpl implements MemberService {
 
     private final MemberRepository memberRepository;
 
     public MemberServiceImpl(MemberRepository memberRepository) {
         this.memberRepository = memberRepository;
    }
    ...
  • 설계 변경으로 MemberServiceImplMemoryMemberRepository를 의존하지 않음
  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
  • 이제부터 MemberServiceImpl은 의존관계 고민은 외부에 맡기고 오직 실행에만 집중하면 됨

🖥️ OrderServiceImpl

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
    }
    ...
  • OrderServiceImpl에는 MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입됨

3) AppConfig 리팩터링

⚠️ 위의 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.
또한 두 개의 서로 다른 MemoryMemberRepository 인스턴스가 만들어져서
memberServiceorderService가 같은 저장소를 공유하지 않게 되는 문제가 발생할 수 있다.

🖥️ AppConfig

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
                                    // ✅ 같은 인스턴스
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
                                   // ✅ 같은 인스턴스 
    }
    
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
                  // ✅ 여기만 수정하면 됨
    }

    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
  • new MemoryMemberRepository() 부분이 중복 제거되어 다른 구현체로 변경할 때 한 부분만 변경하면 됨
  • 구성 정보에서 역할과 구현을 명확하게 분리하고 역할이 잘 들어나도록 함

4) 새로운 구조와 할인 정책 적용

다시 정액 할인 정책을 %할인 정책으로 변경해보자! 어떤 부분이 변경될까?

  • AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리됨
  • 그래서 FixDiscountPolicy를 RateDiscountPolicy로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않음
  • 변경된 코드는 아래와 같음

🖥️ AppConfig

public class AppConfig {
     ...
     public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();
          return new RateDiscountPolicy();
       }
   }

🌟5) 좋은 객체 지향 설계의 원칙 적용

➡️ SRP 단일 책임 원칙 - 한 클래스는 하나의 책임만

  • 이전에는 클라이언트 객체가 직접 구현 객체를 생성, 연결, 실행함
  • SRP원칙을 따르면서 관심사를 분리함 -> 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당하며 클라이언트 객체는 실행만 담당

➡️ DIP 의존관계 역전 원칙 - 추상화에 의존, 구체화에 의존 X

  • 이전에는 새로운 할인 정책을 적용하면 클라이언트 코드도 같이 변경함. 기존 클라이언트가(OrderService) 추상화 인터페이스(DiscountPolicy)와 구체화 인터페이스(FixDiscountPolicy)를 같이 의존했기 때문
  • 클라이언트 코드가 DiscountPolicy에만 의존하도록 했지만 실행 오류
  • AppConfig가 객체 인스턴스(FixDiscountPolicy)를 클라이언트 코드 대신 생성하고 의존관계를 주입함

➡️ OCP - 확장에는 열려있으나 변경에는 닫혀있음

  • 애플리캐이션을 사용 영역과 구성 영역으로 나눔
  • AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있음

6) IoC, DI, 그리고 컨테이너

📌 제어의 역전 IoC(Inversion of Control)

프로그램의 제어 흐름을 직접 제어 하는 것이 아니라 외부에서 관리하는 것

  • AppConfig가 프로그램에 대한 제어 흐름의 권환을 모두 가짐
  • OrderServiceImpl는 인터페이스를 호출하지만 어떤 구현 객체들이 실행될지 모르고, OrderServiceImpl가 아닌 OrderService의 다른 구현 객체를 실행할 수도 있음
  • 내가 작성한 코드를 대신 제어하고 실행하면 프레임워크(JUnit)이고, 직접 제어의 흐름을 담당한다면 라이브러리

📌 의존관계 주입 DI(Dependency Injection)

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

  • 정적인 클래스 의존관계 : 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있음
  • 동적인 객체 인스턴스 의존 관계 : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존 관계를 쉽게 변경 가능

📌 IoC 컨테이너, DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것

  • 어셈블러, 오브젝트 팩토리 등으로 불리기도 함

7) 스프링으로 전환하기

💡이제부터 스프링을 사용해보자

🖥️ AppConfig

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

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

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        // return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

🖥️ MemberApp

public class MemberApp {

    public static void main(String[] args){
    
        // AppConfig appConfig = new AppConfig();
        // MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        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("new member = " + member.getName());
        System.out.println("find member = " + findMember.getName());
    }
}
  • ApplicationContext스프링 컨테이너라고 함
  • 기존에는 주석처럼 개발자가 AppConfig를 사용해 직접 객체를 생성하고 DI를 했지만 이제부터는 스프링 컨테이너를 통해 사용함
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용하고, @Bean이라 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록하며 이것을 스프링 빈이라고 함
  • applicationContext.getBean() 메서드를 사용하여 스프링 빈을 찾음

코드가 더 복잡해진 거 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까? 다음 글에서 다루어 볼 것이다.

0개의 댓글