Super Kawaii Cute Cat Kaoani
본문 바로가기
⚙️ Back-end/SpringBoot

[SpringBoot]스프링 기본편 - 객체 지향 원리 적용 (1)

by wonee1 2025. 9. 6.
728x90

 
🖊️ 본 포스트는 인프런 김영한 스프링 기본편을 보고 정리한 내용입니다. 
 
 
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

스프링 핵심 원리 - 기본편| 김영한 - 인프런 강의

현재 평점 5.0점 수강생 48,140명인 강의를 만나보세요. 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 스프링 기본 기능, 스프

www.inflearn.com

 
 
 

 

1. 새로운 할인 정책 적용과 문제점


 
 
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
 

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

 
이 코드는 OrderServiceImpl이 new 키워드로 할인 정책 구현체를 직접 생성해서 사용하는 것 ! 
 
 

  • OrderServiceImpl은 DiscountPolicy 인터페이스를 사용하고 있음 → "인터페이스에 의존한다" = 다형성 활용
  • FixDiscountPolicy, RateDiscountPolicy 같은 구현체를 갈아끼울 수 있음 → 확장 가능해 보임 

 

 
하지만 이 코드는 DIP 위반했다 
 

 
⭐ DIP란? 

더보기

  DIP (Dependency Inversion Principle, 의존관계 역전 원칙)

   "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상(인터페이스)에 의존해야 한다."

  • 고수준 모듈: 비즈니스 로직 (예: 주문 서비스)
  • 저수준 모듈: 세부 구현 (예: 할인 정책, 결제 방식 등)

 

  • DIP 원칙: 클라이언트(=OrderServiceImpl)는 추상(인터페이스)에만 의존해야 한다.
  • 그런데 지금 코드 보면?
    • 여기서 OrderServiceImpl은
      • 추상(인터페이스) DiscountPolicy에도 의존 
      • 동시에 구체 클래스 RateDiscountPolicy에도 직접 의존하고 있다

 
 
 
=>   따라서 확장할 때마다 기존 코드(OrderServiceImpl)가 깨지는 구조 → OCP 위반
 
 
OCP란?
 

더보기

OCP (Open-Closed Principle, 개방-폐쇄 원칙)

"확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."

  • 새로운 기능을 추가할 땐 기존 코드를 수정하지 않고
  • 기존 코드를 확장해서 처리해야 한다.

 
 
 
 
원래 기대했던 의존관계는 다음과 같지만   

OrderServiceImpl ---> DiscountPolicy (인터페이스)

 
 
 
현재 코드는 구현체에도 직접 의존해버린 관계가 된다 

OrderServiceImpl ---> DiscountPolicy (인터페이스)
       │
       └─> RateDiscountPolicy (구현체)   ← 직접 의존

 
 
 
 
 
인터페이스만 의존하도록 코드를 수정한다

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

 
하지만 다음 코드에도 문제가 발생한다. 
 
코드를 인터페이스만 의존하도록 바꿨지만, 이제는 구현체가 없으니까 실행 자체가 불가능해지는 문제가 생긴다

 
 
 
 
 
따라서 올바른 구조는 OrderServiceImpl은 인터페이스만 알고, 실제 구현체 선택은 외부(스프링 컨테이너)가 하도록 분리해야 한다!
 
 
 
 

2. AppConfig 리팩터링 


 
AppConfig는 애플리케이션의 전체 동작 방식을 구성(config) 하기 위해 만든 설정 클래스

  • 객체를 생성하고
  • 생성자를 통해 필요한 의존관계를 주입(연결) 하는 책임을 가짐

 
 
 
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다
 

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

 

public class AppConfig {

    public MemberService memberService() {
        // MemoryMemberRepository 객체 생성 → MemberServiceImpl에 주입
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        // MemoryMemberRepository와 FixDiscountPolicy를 생성해서 OrderServiceImpl에 주입
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy());
    }

}

 
 
현재 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩토링 하면 다음과 같다. 

 

    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() 중복을 개선한 코드이다 
 
 
 
 
 
 

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

 
AppConfig에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy에서 RateDiscountPolicy 객체로 변경했다 
할인 정책이 변경해도 AppConfig만 수정하면되고 클라이언트 코드는 수정할 필요가 없어진 것이다.  
 
 
 
 
 

3. IoC, DI, 그리고 컨테이너


 
 

제어의 역전 IoC(Inversion of Control)

 
 
예전에는 클라이언트 코드(예: OrderServiceImpl)가 직접 필요한 객체를 new로 만들고, 연결하고, 실행까지 담당했다. 즉, 프로그램 제어 흐름을 개발자가 직접 코드 안에서 제어했다. 
 
=> 자연스럽지만, 클라이언트 코드가 구체 구현체에 딱 달라붙게 되어 확장성이 줄어들고, 유지보수에 있어서 힘들다.
 
 

OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(),
                                                     new FixDiscountPolicy());

 
 
 
 

IoC 적용 (제어권 외부로 위임)
 
 

아까와 같은 단점으로 이제 AppConfig가 객체 생성 & 연결 제어를 담당한다.  OrderServiceImpl은 오직 본인의 비즈니스 로직만 수행한다. 
 
=> 따라서 어떤 객체가 주입될지는 AppConfig가 결정하고 관리 → 제어의 흐름이 역전됨(IoC)
 

 

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

 
 
 

프레임워크 vs 라이브러리 

 
 

  • 프레임워크: 내가 작성한 코드를 프레임워크가 제어하고 실행 (예: JUnit)
  • 라이브러리: 내가 작성한 코드가 직접 제어 (예: Java 유틸 라이브러리)

 
 
 

의존관계 주입 DI(Dependency Injection)

 
 
 
OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존한다.  하지만 실제로 어떤 구현체(FixDiscountPolicy, RateDiscountPolicy)가 들어올지는 모른다. 
 
=> 이 구현체 선택/주입을 외부(AppConfig, 스프링 컨테이너)가 대신 해주는 것이 DI
 
 

 

애플리케이션 실행 시점(런타임)외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다
 
=> 즉  FixDiscountPolicy냐 RateDiscountPolicy인가는 AppConfig/스프링 설정에 따라 달라진다.
 
 
⭐정적 의존관계 vs 동적 의존 관계 

더보기

 

  • 정적 의존관계: 코드(import)만 보고 알 수 있음
  • 동적 의존관계: 실행 시점에 실제 어떤 구현체가 들어올지 결정됨

 

 
 

IoC 컨테이너 
 
 

  • 기존에는 개발자가 직접 객체를 new 해서 관리한다 
  • 이제는 컨테이너(예: AppConfig)가 객체를 생성하고 관리한다 
  • 제어의 흐름을 개발자가 아닌 외부(컨테이너)가 담당 → IoC 원칙을 구현하는 것 

 

 
DI 컨테이너

 

  • IoC 컨테이너의 역할 중 특히 의존관계 주입(DI)에 초점을 맞춘 이름
  • 객체를 생성할 뿐 아니라, 누가 누구를 참조할지(연결)까지 관리
  • 요즘은 IoC라는 말보다 DI라는 용어가 더 자주 쓰임

 
 
 
 
 
 
 
 
 
 
 

728x90