Spring

스프링 IOC와 DI

martial 2022. 5. 1. 15:58

안녕하세요!

오늘 알아볼 개념은 바로 Spring의 3대요소 IOC, PSA, AOP중 IOC와 이에 밀접한 관련이 있는 DI에 대해 알아볼거에요.

그럼 지금 바로 시작해볼게요.

 

자 먼저 IOC란 무엇일까요?

IOC 

IOC란 Inversion of control, 제어의 역전 이에요

만약 IOC없이 인스턴스를 생성한다고 해볼게요! 그렇다면

MemberService memberService = new MemberService()

이런식으로 인스턴스를 생성할거에요!

객체를 생성하는법은 해당 객체의 생성자의 스펙에따라 달라지지만 이 예제에서는 기본생성자로 생성을 할게요!

 

이렇게 객체를 생성할때 new Class부분을 개발자가 직접 주입하게 되는데요

이럴경우의 단점이 무엇일까요? 바로 객체지향의 SOLID원칙중 OCP, DIP에 위반하는 내용이에요

그렇다면 OCP와 DIP가 무엇인지 살펴볼까요?

 

OCP - 개방폐쇄의 법칙 

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

 

DIP - 의존관계 역전의 법칙

추상화에 의존해야지 구체화에 의존하면 안된다.

 

혹시 두개의 원칙의 공통점을 느끼셨나요? 바로 확장성인데요,

SOLID 원칙을 처음 들어보시거나, 공부중이신 예비개발자 분들은 "DIP는 알겠는데 OCP는 뭔 뚱딴지같은 소리야?"라고 하실 수 있어요

그러신 분들을 위해 제가 추가설명을 드릴게요.

 

위에서 인스턴스를 생성한 코드를 그대로 가져와 사용할게요

만약 MemberService를 구현하는 클래스가 한가지만 있다면 구현체를 개발자가 직접 주입해주어도 됩니다

하지만 이는 확장성을 고려한 선택이 되지 못해요

 

만약 프로젝트내에서 비즈니스 요구사항에 변동이 있어 해당 구현체가 쓸모가 없어진다면 이제까지 선언한 인스턴스를

하나하나 구체화를 바꿔 주어야 해요.

// 기존 클래스의 인스턴스화
MemberService memberSerivce = new MemberService

// 비즈니스 로직이 바뀌고 구현체를 바꾸었을때 인스턴스화
MemberService memberService = new CustomMemberService

이렇게 말이에요 (해당 코드에서 새로운 구현체를 CustomMemberService 로 생성했다고 가정할게요)

그렇다면 기존의 controller계층에서 생성한 인스턴스들을 하나하나씩 다 바꾸어 줘야 해요, 만약 이게

엄청 유명한 오픈소스거나, 대형 프로젝트라면 와닿을거에요!

 

그렇다면 OCP와 DIP를 위반하지 않으면서도 구현체를 주입할 수 있는방법은 무엇일까요?

 

DI

바로 그 방법은 Dependency Injection, 의존성 주입인데요 DI에는 여러가지 방법이 있지만,

오늘 보여드릴 방법은 테스트코드를 이용해 보여드릴거기 때문에 필드주입 방법을 이용할게요!

(테스트 케이스가 아닌, Application에 영향을 주는 클래스에서는 생성자 주입을 스프링에서 권유합니다.)

 

자 MemberService라는 인터페이스와

public interface MemberService {
    void signup();
    void login();
}

 

MemberService를 구현한 구현체 MemberServiceImpl 

@Service
public class MemberServiceImpl implements MemberService {
    @Override
    public void signup() {
        System.out.println("회원가입");
    }

    @Override
    public void login() {
        System.out.println("로그인");
    }
}

를 작성해줄게요~

 

여기서 포인트는 MemberService를 구현한 클래스는 무조건 @Component 어노테이션이 붙어있어야 해요

왜냐하면 Application Run을 담당하는 클래스에서 @SpringBootApplication 어노테이션이 @ComponentScan을 상속받기 때문에

@Component를 통해 컴포넌트로 등록된 클래스들을 모두 스캔하여 IOC컨테이너에 등록하기 때문이에요

 

자 그러면 이제 테스트를 해볼까요?

@Autowired
MemberService memberService;

@Test
void dependencyInjection() {
    memberService.signup();
    memberService.login();
}

해당 테스트케이스의 결과는

으로 성공!

 

하지만 만약 구현체를 컴포넌트로 등록하지 않았다면?

이렇게 IDE레벨에서부터 빨간줄이 그어져요, 그말은 즉슨 compileJava가 실패한다고 말할 수 있고 즉 컴파일 실패를 뜻해요!

 

근데 이쯤되면 궁금하게 생기실 수 있어요

만약 구현체가 두개라면 IOC컨테이너는 어떤 구현체를 주입해주어야 하는걸까? 라고요!

그럼 한번 저희 이 문제를 코드로 직접 구현해서 어떻게 동작하는지 알아볼까요?

 

그럼 예제에 이용되는 클래스들을 보여드릴게요!

 

MemberService

public interface MemberService {
    void signup();
    void login();
}

NewMemberServiceImpl

@Service
public class NewMemberServiceImpl implements MemberService{

    @Override
    public void signup() {
        System.out.println("새로운 회원가입 방법이에요!");
    }

    @Override
    public void login() {
        System.out.println("새로운 로그인 방법이에요!");
    }
}

LegacyMemberServiceImpl

@Service
public class LegacyMemberServiceImpl implements MemberService {

    @Override
    public void signup() {
        System.out.println("구식 회원가입 방법이에요!");
    }

    @Override
    public void login() {
        System.out.println("구식 로그인 방법이에요!");
    }
}

이렇게 MemberService 인터페이스와 MemberServie를 구현 LegacyMemberServiceImpl, NewMemberServiceImp이

있다고 가정할게요 만약 여기서 비즈니스 로직의 변경으로 인해 NewMemberServiceImpl로 인스턴스를 생성한다고 가정할때

어떻게 인스턴스를 생성해볼게요

 

@Autowired
MemberService memberService;

@Test
void dependencyInjection() {
    memberService.signup();
    memberService.login();
}

이 테스트케이스를 돌렸을때 결과는 어떨까요?

바로 

 

available: expected single matching bean but found 2

해당 exception 메세지가 뜨면서 컴파일에 실패해요, 한번 오류 메세지를 읽어볼까요?

"해당 타입의 빈이 한개여야 하지만 두개가 찾아졌다."

 

이 포스트와 관련은 없지만 제가 좋아하는 말이 있어요

"코드는 개발자가 어떤 생각을 가지고 있는지 전혀 알 수 없다. 그들은 개발자가 작성한대로 로직을 수행할 뿐이다."

한번 우리가 Spring의 IOC컨테이너가 되어볼까요? 

"개발자가 무슨 Bean을 원하는지 모르겠어." 라는 생각이 드실거에요.

 

그렇다면 구현체가 두개일때는 의존성을 주입해주지 못할까요?

정답은, 아니에요 바로 네이밍 규칙을 이용하면 돼요.

 

바로 이런식으로요

@Autowired
    MemberService newMemberServiceImpl;

    @Test
    @DisplayName("만약 구현체가 2일때 네이밍 규칙을 통해 DI")
    void TwoImplements() {
        newMemberServiceImpl.signup();
        newMemberServiceImpl.login();
    }

NewMemberServiceImpl이란 구현체는 카멜 케이스로 변환되어 Bean으로 등록되기 때문에 

IOC컨테이너 에서는 newMemberServiceImpl이란 Bean이 등록되어 있을테니 타입은 MemberService그대로 두되

인스턴스명을 newMemberServiceImpl로 선언을 하면 정확히 들어맞기 때문에 해당 Bean이 주입되는 거에요!

 

한번 테스트 케이스를 실행시켜 볼까요?

성공했어요! 

 

이렇게 IOC와 DI에 알아봤는데요!

모두들 찾으신 내용이길 바라고 유익한 내용이길 바래요.

 

참고로!

IOC컨테이너는 Spring 컨테이너와 같은 말이에요

그리고 IOC컨테이너를 통해 의존성을 주입할때 의존성을 주입하는 클래스와, 해당 의존성 클래스

모두 IOC 컨테이너에 등록되어있어야 해요. 둘중 한개라도 등록되지 않을시 DI를 할 수 없어요.

 

또한 IOC 컨테이너의 기본 Scope는 Singleton Scope이기 때문에

IOC 컨테이너를 통해 생성한 인스턴스는 Singleton Pattern을 사용할 수 있어요.

 

그리고 DI는 외부에서 주입받는게 핵심입니다.

추상 클래스와 구현 클래스를 한 line안에서 생성하면 그것은 DI가 아니에요.

 

그럼 모두들 객체지향적인 개발하기 바래요!