스프링 DI와 IoC

zunzero·2022년 8월 9일
0

스프링, JPA

목록 보기
2/23

객체지향과 다형성

SOLID

SOLID는 좋은 객체 지향 설계의 5가지 원칙을 모아놓은 것이다.

1. SRP 단일 책임원칙: 한 클래스는 하나의 책임만 가진다. 
2. OCP 개방-폐쇄 원칙: 확장에는 열려있으나, 변경에는 닫혀 있어야 한다.
3. LSP 리스코프 치환 원칙: 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다.
4. ISP 인터페이스 분리 원칙: 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
5. DIP 의존관계 역전 원칙: 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다.

다형성

다형성의 객체 지향 프로그래밍의 핵심으로, 역할과 구현으로 세상을 구분한다.
이렇게 되면 세상이 단순해지고 유연해지며 변경도 편리해진다.
장점은 아래와 같다.

1. 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
2. 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
3. 구현 대상의 내부 구조가 변경되어도 클라이언트는 영향을 받지 않는다.
4. 구현 대상 자체를 변경해도 영향을 받지 않는다.


객체 지향의 핵심은 다형성이지만, 다형성만으로는 OCP와 DIP를 완전히 지킬 수 없다. 이를 지킬 수 있게 해주는 것이 스프링 이다.
스프링은 DI(Dependency Injection) 컨테이너를 제공함으로써 위의 기능을 지원한다.

프로젝트 예시

회원 저장소나 할인 정책의 경우 아직 구체적인 방향이 정해지지 않았다.
따라서 아직 구현체가 확정되지 않았기 때문에 인터페이스로 구현하여 언제든 유연하게 변경 가능하도록 인터페이스로 작성한다.

클라이언트와 서버
클라이언트는 요청을 보내는 쪽, 서버는 요청을 받아 응답하는 쪽이다.

주문 서비스와 할인 정책의 경우, 할인 적용이라는 요청에 대해 클라이언트와 서버가 나뉜다고 볼 수 있다.

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

만약 할인 정책을 '정액 할인 정책'에서 '정률 할인 정책'으로 변경한다면 코드가 위와 같이 바뀌게 된다.
위 소스 코드의 문제점은 DIP와 OCP를 지키지 않았다는 것이다.

1. DIP: 인터페이스(추상화)에 의존하는 것 뿐만 아니라, 직접 구현체(구체화)를 선택하는 모습으로 구현체 클래스에도 의존하고 있다.
2. OCP: 확장에는 열려있으나, 변경에는 닫혀 있어야 한다는 원칙이다. 구현체를 새로 만들어 확장하는 것은 좋았으나, 요청을 보내는 클라이언트(OrderServiceImpl)의 소스 코드 또한 변경해야 하므로 원칙이 지켜지지 않았다.

관심사의 분리

위의 소스코드를 보면, OrderServiceImpl은 OrderService 인터페이스를 구현할 뿐만 아니라 직접 서버(요청을 받는 쪽, DiscountPolicy)의 구현체를 결정하고 있다.
하나의 클래스가 너무 많은 책임을 지고 있다.

배우와 공연 기획자의 역할을 분리해야 한다.

따라서 우리는 공연 기획자의 역할을 하는 클래스를 AppConfig라는 이름으로 만들어 보겠다.

public class AppConfig {
	public MemberService memberService() {
    	return new MemeberServiceImpl(memberRepository());
    }
    
    public OrderService orderService() {
    	return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();	// 구현체 결정
    }
    
    public DiscountPolicy discountPolicy() {
    	return new FixDiscountPolicty();
    }
}    


AppConfig는 각 인터페이스들에 구현 객체를 생성하여 주입한다.
이 때 만약 구현체에 변화가 있다면, AppConfig라는 설정 파일의 내용만 바꿔주면 애플리케이션 동작에는 문제가 없어진다.
이로 인해, 각 인터페이스들은 서로의 인터페이스들만 의존하고, 구현체는 전혀 의존하지 않게 되었다.

또한 프로그래머는 구체화에 의존하지 않고 추상화에 의존함으로써 DIP를 잘 지켰다.
AppConfig 코드의 변경 외에, 클라이언트 자체의 코드는 변화 없이 새로운 기능을 만들 수 있으므로 OCP 또한 잘 지켰다고 볼 수 있다.

IoC와 DI

IoC: Inversion of Control 제어의 역전

프로그램 제어의 흐름은 AppConfig가 가져가게 된다. 개발자의 입장에서 자연스러운 흐름은 구현 객체가 스스로 나머지 구현 객체를 선택하여 실행하는 것인데, 제어의 흐름을 빼앗겼기 때문에 구현 객체는 협력 관계에 있는 인터페이스의 구현체의 정체도 모른 채 묵묵히 자신의 로직을 실행할 뿐이다.
이렇듯 프로그램 제어의 흐름을 외부에서 관리하는 것을 IoC라고 한다.

DI: Dependencty Injection 의존관계 주입

의존 관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
동적인 의존관계는 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다.
애플리케이션 실행시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 DI라고 한다.

이를 통해, 클라이언트 코드의 변경 없이 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있고, 정적인 클래스 의존관계 변경 없이 (인스턴스 의존관계) 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.(구현체 의존관계)
AppConfig를 DI 컨테이너 혹은 IoC 컨테이너 라고 한다.

스프링 컨테이너 ApplicationContext

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
    	return new MemeberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService() {
    	return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();	// 구현체 결정
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
    	return new FixDiscountPolicty();
    }
}  

@Configuraion 애노테이션은 설정을 구성한다는 뜻이다. 해당 애노테이션 내부에는 @Component 애노테이션이 포함되어 있어서 컴포넌트 스캔의 대상이 된다.

@Bean 애노테이션은 스프링 컨테이너에 스프링 빈으로 등록한다는 의미이다.

public static void main(String[] args) {
	// AppConfig appConfig = new AppConfig();
    // MemberService memberService = appConfig.memberService();
    // OrderServcice orderService = appConfig.orderService();
    
    ApplicationContext applicationContext 
    = new AnnotationConfigApplicationContext(AppConfig.class);

기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링의 도움을 받아 스프링 컨테이너를 사용한다.
스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 이 때, @Bean 애노테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.

ApplicationContext는 인터페이스이면서 스프링 컨테이너이다. 위의 소스코드를 보면 해당 인터페이스의 구현체로 AppConfig.class가 선택된 것을 볼 수 있다.

1. 스프링 컨테이너를 AppConfig.class로 생성하면 스프링 컨테이너가 생성되고 그 안에 빈 이름과 객체를 담을 스프링 빈 저장소가 담긴다.
2. @Bean이 붙은 메서드 이름으로 빈 이름이 저장되고, 그에 맞는 빈 객체가 싱글톤으로 생성된다.
3. 후에 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다. (DI)

BeanFactory와 ApplicationContext

위 두개 모두 스프링 컨테이너라고 부른다.
ApplicationContext는 BeanFactory를 상속 받은 것으로, 주로 ApplicationContext를 사용한다.
ApplicationContext는 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계되어 있다. (Java 언어, xml 등등)
ApplicationContext는 인터페이스일 뿐, 각각의 다양한 형식의 설정 정보를 받아들일 구현체에 따라 다르게 작동한다. (다형성이 굉장히 잘 지켜진 코드)

싱글톤 컨테이너

앞서 말했듯, 빈 객체는 싱글톤으로 생성되어 스프링 빈 저장소에 저장된다.
싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되어 공유되는 것을 보장하는 디자인 패턴이다. 싱글톤 패턴에는 위와 같은 여러 문제점들이 있는데, 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
컨테이너는 @Bean이 붙은 메서드 이름으로 객체를 하나만 생성해서 관리한다.
스프링 컨테이너가 싱글톤 컨테이너의 역할을 하는 것이다.
이러한 기능 덕분에 싱글톤 패턴을 유지하면서 DIP, OCP 등의 원칙을 유지할 수 있다.
참고로 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니고 요청마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

컴포넌트 스캔

@ComponentScan

@ComponentScan이 붙은 클래스 이하 모든 패키지에 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
스프링 프로젝트의 메인 클래스에 붙은 @SpringBootApplication 애노테이션 안에 @ComponentScan이 들어있어, 일반적으로 프로젝트의 모든 패키지에 @Component가 붙은 클래스를 스프링 빈으로 등록하는데, scanBasePackages = "~"와 같이 탐색 시작 패키지를 설정할 수도 있다.

@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
	public static void main(String[] args) {
    	SpringApplication.run(ProxyApplication.class, args);
    }
    
    @Bean
	public LogTrace logTrace () { ~ }   

위와 같은 경우 @Bean 애노테이션이 붙은 logTrace와 hello.proxy.app 하위의 패키지에 있는 @Component 클래스를 스프링 빈으로 등록한다.

@Import

@SpringBootApplication이나 @ComponentScan의 범위에서 벗어난 경우네는 @Import 애노테이션을 사용할 수도 있다.
@Import 애노테이션을 통해 해당 클래스를 컴포넌트 스캔의 대상으로 만들고, @Component 애노테이션이 있는 경우, @Bean이 붙은 메서드를 스프링 빈으로 등록한다.

@Import(AopConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {
}
@Configuration
public class AopConfig {
	@Bean
    public LogTraceAspect logTraceAspect() {
    }![업로드중..](blob:https://velog.io/cb369f92-ba19-49ee-82f5-a1cb509c0840)

@Autowired -> 의존관계 자동 주입

생성자의 파라미터가 한 개인 경우 @Autowired를 생략할 수도 있지만 굳이 생략해야 하나 싶다.

생성자 주입은 코드가 길어지고 더러워질 수 있다.
생성자에서 별다른 로직을 구현하지 않는다면 @RequiredArgsConstructor를 사용하는 것이 좋다.
lombok을 통해 @RequiredArgsConstructor를 사용하면 final 키워드가 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

참고

인프런 김영한 강사님의 스프링 핵심원리 - 기본편 강의를 참고하여 포스팅하였습니다.

profile
나만 읽을 수 있는 블로그

0개의 댓글