스프링 #2 DI

함형주·2022년 3월 2일
0

spring

목록 보기
2/12

질문, 피드백 등 모든 댓글 환영합니다.

스프링이 제공하는 개념인 DI에 대해 정리하려 합니다.
스프링을 찍먹하며 느낀 것을 정리한거라 내용이 부실하고 틀린 부분이 있을 수 있습니다.
앞으로 공부하며 추가하겠습니다.


DI란?

Dependency Injection, 의존성(의존관계) 주입으로 여기서 의존성은 '알고 있는 것'이라고 생각할 수 있습니다. 예를 들어 Service 클래스에서 Member 클래스의 객체를 생성한다면 이런 형태가 될 것입니다. 이 때 Service 클래스가 Member 클래스를 의존한다고 합니다.

import .Member;

pubic class Service() {
	Member member = new Member();
    ...
}

Service 클래스에서 객체를 직접 생성하지 않고 외부의 클래스를 통해서 Member 객체를 사용하게 된다면 이를 DI라고 합니다.

DI는 스프링 #1에서 언급했듯이 객체 지향 개발을 돕기 위해 사용합니다.
DI는 객체 지향 원칙 중 SRP, DIP, OCP를 가능하게 해줍니다.

  • SRP(단일 책임 원칙)

    SRP : 한 클래스는 하나의 책임만 가져야한다.

위의 Service 클래스는 객체를 생성하는 역할도 하지만 Service만의 기능도 담당할 것이므로 SRP를 위반하게 됩니다. 만약 객체의 생성을 다른 클래스에게 위임하고 의존성을 주입 받을 수 있다면 SRP를 준수할 수 있을 것입니다.

  • DIP(의존관계 역전 원칙)

    DIP : 구현체에 의존하지 말고 추상화된 인터페이스에 의존

만약 Member를 상속받은 XxxMember 객체를 사용해야 하는 상황이라면 Member member = new XxxMember(); 이와 같이 코드를 작성할 것이고 DIP를 위반하게 됩니다. 외부 클래스로부터 XxxMember 에 대한 의존성을 주입받을 수 있다면 이 문제도 해결할 수 있습니다.

  • OCP(개방-폐쇄 원칙)

    OCP :소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다

만약 Service 클래스에서 XxxMember가 아닌 OooMember 객체를 사용해야 한다면 Service 클래스의 수정이 불가피할 것입니다.
Member member = new XxxMember(); -> Member member = new OooMember();
이 점도 외부의 클래스로부터 의존성을 주입 받게 된다면 필요에 따라 XxxMember 객체 대신 OooMember 객체를 주입받을 수 있을 것이므로 OCP를 준수할 수 있게 됩니다.


스프링에서 DI 구현 방법

순수한 자바 코드를 이용할 경우

이 부분은 간단히 정리하겠습니다. 의존성을 주입해줄 별도의 클래스가 필요합니다.

// 객체를 생성하고 연결하는 설정 클래스
public class AppConfig {
	
    public Service xxxMemberService() {
    	return new Service(new XxxMember);
    }
    
    public Service oooMemberService() {
    	return new Service(new oooMember);
    }
   
}

그리고 생성자를 통해 의존성을 주입받습니다.

public class Service{
	Member member;
    
    public Service(Member member) {
    	this.member = member
    }
    ...
}

main 메소드에서 실행해봅시다.

public class Main {
	public static void main(String[] args) {
		AppConfig appConfig = new AppConfig();
		Service service = appConfig.XxxMemberService();
		...
        }
    }

굉장히 초라하지만 SRP, DIP, OCP을 준수한 코드를 만들었습니다.
스프링은 이런 DI 기능을 제공하므로 훨씬 간단하게 구현할 수 있습니다.

  1. @Configuration를 설정한 클래스에서 @bean을 이용한 spring bean 수동 등록

@Configuration 이 어노테이션이 달린 클래스는 스프링에서 설정정보를 담당하는 클래스가 됩니다. 메소드에 @bean을 달아주면 반환된 객체를 스프링 컨테이너에 등록됩니다. 스프링 컨테이너에 등록된 객체를 스프링 빈이라 합니다. 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용합니다.

@Configuration
public class AppConfig {
	@bean
    public Service xxxMemberService() {
    	return new Service(new XxxMember);
    }
}

Main 클래스에도 스프링 컨테이너를 사용할 수 있게 수정해줍니다. 스프링 빈을 사용하려면 AnnotationConfigApplicationContext를 사용해줍니다. (스프링을 사용하면 springApplication에서 main 메소드를 제공하므로 여기선 main 메소드를 빼주겠습니다.)

public class Main {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    Service service = ac.getBean(Service.class);
	...
    }

AnnotationConfigApplicationContext의 getBean 메소드를 사용하면 자동으로 스프링 컨테이너에 등록된 Service 객체(new Service(new Xxxmember)가 호출이 되면서 의존성이 주입됩니다.

  1. @ComponentScan을 이용한 spring bean 자동 등록

더 쉬운 방법으로 @ComponentScan을 사용하면 @Component 가 붙은 클래스를 자동으로 스프링 컨테이너에 등록해줍니다.

@Configuration
@ComponentScan
public class AppConfig {
	
}

그리고 Service 클래스에 @Component, 생성자에 @Autowired를 달아줍니다.

@Component
public class Service{
	Member member;
    
    @Autowired
    public Service(Member member) {
    	this.member = member
    }
    ...
}

생성자에 @Autowired를 지정해주면 스프링이 자동으로 컨테이너의 타입이 같은 빈을 찾아서 주입해줍니다. (getBean(Service.class)과 비슷한 방식)
이제 Main 클래스를 실행시키면 같은 결과가 나옵니다.

여기서 Lombok 라이브러리를 적용하고 클래스에 @RequiredArgsConstructor를 달아주면 final 키워드가 붙은 필드를 모아 자동으로 생성자를 만들어 줍니다.(코드가 간결해지고 객체가 불변해지고 접근을 막을 수 있습니다.)
최종코드

@Component
@RequiredArgsConstructor
public class Service{
	private final Member member;
    
    ...
}

의존성 주입 방법

의존성 주입 방법은 크게 4가지가 있습니다.
1. 생성자 주입
2. 수정자(setter) 주입
3. 필드 주입
4. 일반 메서드 주입

지금까지 했던 방식이 생성자 주입 방법입니다.
여러 방법이 있지만 생성자 주입 방법을 사용하면 되고 대부분의 DI 프레임워크가 이 방법을 권장합니다.
그 이유는

불변성, 누락

대부분의 의존성 주입은 애플리케이션 종료 전까지 불변해야 합니다. 생성자 주입 방식은 객체를 생성할 때 딱 한 번 호출되고 private final 키워드를 통해 불변하게 설계할 수 있습니다.
수정자 주입 방식을 사용하면 setter 메소드를 public으로 설정해야 하는데 그러면 누군가 실수로 변경할 수 있는데 변경하면 안되는 메소드를 열어두는 것은 좋은 설계 방법이 아닙니다.
또한 생성자 주입시 의존성 주입이 누락된 경우에 컴파일 오류가 발생합니다. 만약 수정자 방식을 사용했다면 실행은 되지만 의존성 주입이 일어날 때 NullPointException 이 발생합니다.(컴파일 오류는 최고의 오류!)

즉 일반적으론 생성자 주입 방식을, 가끔 옵션이 필요하면 수정자 방식을 사용하면 됩니다.

profile
평범한 대학생의 공부 일기?

0개의 댓글