글 작성자: beaniejoy
Index

📌 새로운 요구사항의 추가
📌 관심사 분리
📌 AppConfig 리팩토링
📌 좋은 객체 지향 설계 5가지 원칙 적용
📌 IoC, DI, 컨테이너
📌 정리

 

해당 내용은 강의 내용을 기억하기 위한 정리글입니다. 자세한 내용은 강의에서 확인하실 수 있습니다.
(저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.)

스프링 핵심 원리 - 기본편(김영한님) #광고아님, #내돈내산, #적극추천

 

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

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com


 

📌 새로운 요구사항의 추가

주문서비스가 있고 주문에 따른 할인 정책을 적용해야 하는 비즈니스 요구사항이 있습니다. 할인 정책에는 고정할인 정책만 적용해보려합니다. 다음과 같이 서비스 클래스를 구성할 수 있습니다.

class OrderServiceImpl: OrderService {
    private val discountPolicy: DiscountPolicy = FixDiscountPolicy()
    
    //...
}

위와 같이 OrderServiceImpl 서비스 코드는 DiscountPolicy에 의존하고 있고 해당 정책에는 고정할인정책 구현체를 적용하였습니다.

여기서 객체지향 관점에서 생각해볼 지점이 몇 가지 있습니다.

  • 역할과 구현을 충실하게 분리
    DiscountPolicy라는 역할과 FixDiscountPolicy라는 구현을 분리하였습니다.
  • 다형성 활용, 인터페이스/구현체 분리
    역할과 구현 내용과 비슷하게 DiscountPolicy는 인터페이스 FixDiscountPolicy는 구현체입니다.

역할과 구현, 다형성 활용, 인터페이스 구현체 분리 등 객체 지향 원리를 잘 활용했다고 볼 수 있습니다. 하지만 또 생각해볼 것들이 있습니다.

class OrderServiceImpl: OrderService {
    //private val discountPolicy: DiscountPolicy = FixDiscountPolicy()
    private val discountPolicy: DiscountPolicy = RateDiscountPolicy()
    
    //...
}
  • DIP(의존 역전 원칙) 준수 X
    OrderServiceImpl은 추상(인터페이스, DiscountPolicy)과 구현 클래스(FixDiscountPolicy) 둘다 의존하고 있습니다.
  • OCP(개방 폐쇄 원칙) 준수 X
    기능을 확장하면(또 다른 할인 정책 적용) 이것을 사용하는 클라이언트 코드(OrderServiceImpl)에 영향을 주게 됩니다.

즉 새로운 요구사항의 추가로 인해 기능을 확장해야 하는 상황에서는 한계에 부딪히게 됩니다.
(확장성을 고려못한 설계, 객체지향스럽지 않은 냄새나는 코드라 할 수 있습니다.)


 

📌 관심사 분리

위의 서비스 코드(OrderServiceImpl)에서는 두 가지 책임과 역할을 수행하고 있습니다. 하나는 서비스 코드 자체의 비즈니스 로직 수행 역할, 또 하나는 객체 생성과 참조변수 할당의 역할입니다. 이러한 설계는 객체지향스럽지 못합니다. 객체 지향은 기본적으로 하나의 코드에 하나의 책임과 역할을 수행해야 한다는 원칙을 가지고 있습니다. 즉 여기서 관심사를 분리해야 될 필요가 생긴 것입니다.

여기서 설정파일을 통해 객체 생성과 할당하는 역할을 분리해보겠습니다.

@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(MemoryMemberRepository(), FixDiscountPolicy())
    }
}

class OrderServiceImpl(
    private val memberRepository: MemberRepository,
    private val discountPolicy: DiscountPolicy
) : OrderService {
	//...
}

스프링 IoC와 DI를 통해서 관심사 분리를 간단하게 구현할 수 있습니다. AppConfig 설정파일을 만들고 OrderService 구현체를 스프링 빈으로 등록합니다. 이 때 생성자를 통해 MemberRepositoryDiscountPolicy 인터페이스의 구현체를 주입합니다.

스프링 IoC는 AppConfig 설정 내용을 토대로 대신 객체를 생성해주고 필요한 구현체들을 주입시켜줍니다. 서비스 코드인 OrderServiceImpl에서는 바로 위 코드처럼 순수하게 비즈니스 로직만 수행하면 됩니다.

만약 FixDiscountPolicy에서 RateDiscountPolicy로 기능을 변경해야 한다면 AppConfig만 코드를 수정하면 됩니다. 서비스 코드에서는 변경지점이 사라지게 됩니다. (OCP, DIP 원칙을 준수하게 됩니다.)

  • AppConfig: 구현체 클래스 선택 및 전체 구성 책임만 수행
  • OrderServiceImpl: 기능을 실행하는 책임만 수행

이렇게 관심사를 확실하게 분리할 수 있습니다.


 

📌 AppConfig 리팩토링

지금은 Bean 생성시 필요한 구성 클래스들을 직접 생성자를 통해서 주입을 했습니다.

@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(MemoryMemberRepository(), FixDiscountPolicy())
    }
    
    @Bean
    fun memberService(): MemberService {
        return MemberServiceImpl(MemoryMemberRepository())
    }
}

위와 같이 MemberRepository 구현체가 여러 bean 생성 때 필요로 한다면 생성자를 통한 객체 생성 부분이 중복이 될 수 있습니다.
이를 하나로 묶을 수 있습니다.

@Configuration
class AppConfig {
    @Bean
    fun orderService(): OrderService {
        return OrderServiceImpl(memberRepository(), discountPolicy())
    }
    
    @Bean
    fun memberService(): MemberService {
        return MemberServiceImpl(memberRepository())
    }
    
    @Bean
    fun memberRepository(): MemberRepository {
        return MemoryMemberRepository()
    }
    
    @Bean
    fun discountPolicy(): DiscountPolicy {
    	return FixDiscountPolicy()
    }
}
  • @Configuration: 스프링에게 해당 클래스가 설정파일이라는 것을 알려줍니다. (Bean들의 설계도)
  • @Bean: 스프링 컨테이너에 스프링 빈으로 등록을 시켜줍니다.

 

📌 좋은 객체 지향 설계 5가지 원칙 적용

5가지 중 SRP, DIP, OCP에 대해서 생각해볼 수 있습니다.

  • SRP(단일 책임 원칙)
    - 구현 객체 생성 및 연결은 AppConfig, 실행에 대한 책임은 클라이언트 객체(OrderServiceImpl)가 담당
    - 관심사 분리
  • DIP(의존 관계 역전 원칙)
    - 의존성 주입을 통해 추상화에 의존하고 구체화에 의존하지 않는 구조를 따름
    - OrderServiceImpl > DiscountPolicy(인터페이스)에 의존, FixDiscountPolicy에 의존 X
  • OCP(개방 폐쇄 원칙)
    - 확장에는 열려있고 변경에는 닫혀있음
    - FixDiscountPolicy -> RateDiscountPolicy
    - 다른 할인정책으로 확장할 때 클라이언트 코드(OrderServiceImpl)는 변경지점이 없음

위의 사례를 통해 객체 지향 설계를 잘 준수하고 있음을 확인할 수 있었습니다.


 

📌 IoC, DI, 컨테이너

IoC, DI, 컨테이너는 강의 내용을 다음과 같이 요약할 수 있습니다.

  • IoC, Inversion of Control, 제어의 역전
    - 기존에는 클라이언트 코드에서 구현 객체 생성, 연결, 실행 모든 역할을 수행
    - 클라이언트 코드가 제어의 흐름을 컨트롤한다고 볼 수 있음
    - AppConfig 등장으로 클라이언트 코드에서는 로직을 실행하는 역할만 담당
    - 제어의 흐름에 대한 권한은 AppConfig가 가지게 됨
    - 프로그램 제어의 흐름을 외부에서 관리하는 것이 IoC 핵심

 

  • DI, Dependency Injection, 의존관계 주입
    - OrderServiceImplDiscountPolicy에 의존
    - 의존관계는 정적인 클래스 의존관계, 동적인 객체 의존 관계 둘로 나뉨
      > 정적인 클래스 의존관계
          import만 보고 쉽게 파악 가능, 실제 실행시점에서 어떤 구현체가 주입되는지 알 수 없음
          OrderServiceImpl > MemberRepository, DiscountPolicy
          두 개 인터페이스의 실제 구현체가 어떤게 주입되는 지 알 수 없음
      > 동적인 객체 의존관계
          
    애플리케이션 실행시점에 실제 생성된 객체 참조가 연결된 의존관계

 

  • IoC, DI 컨테이너
    - AppConfig 같이 객체 생성, 관리, 의존관계 연결해주는 역할 수행
    - 최근에는 DI 컨테이너라 불림(어샘블러, 오브젝트 팩토리 등으로도 불림)

 

의존관계 주입(DI)을 사용하면 정적인 클래스 의존관계 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경 가능

이게 이번 챕터 강의 내용 핵심인 것 같습니다.


 

📌 정리

이번 강의 내용은 스프링 핵심 개념인 IoC, DI를 코드로 예시를 보여주며 잘 습득할 수 있어서 좋았습니다. 강의 핵심은 다음과 같습니다.

  • 스프링은 객체지향 5가지 원칙을 잘 준수하는 프레임워크 그 중 SRP, DIP, OCP에 주목
  • 관심사 분리를 통해 SRP 준수
    - 객체 생성, 연결하는 제어 관점 & 기능을 실행하는 사용 관점
    - AppConfig (설정파일), IoC 개념 등장
  • 의존성 주입(DI)를 통해 DIP 준수
    - 추상화에 의존하고 구체화에 의존하지 않는 코드 구성 (OrderServiceImpl과 DiscountPolicy 관계 생각)
  • IoC와 DI를 통해 OCP 준수
    - 다른 할인 정책을 적용할 때(확장) AppConfig 코드만 수정하면 됨
  • 의존관계 주입(DI)을 사용하면 정적인 클래스 의존관계 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경 가능