<Spring> DI(Dependency Injection)?

라모스·2021년 9월 24일
0

Spring☘️

목록 보기
2/18
post-thumbnail

좋은 객체지향 설계의 5가지 원칙(SOLID)

  • 단일 책임 원칙(SRP): 한 클래스는 하나의 책임만 가져야 한다.
  • 개방-폐쇄 원칙(OCP): 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 리스코프 치환 원칙(LSP)
  • 인터페이스 분리 원칙(ISP)
  • 의존관계 역전 원칙(DIP): 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."

객체 지향의 핵심은 다형성이다. 프로그램을 유연하고 변경이 용이하게 설계하는게 주된 목적이고 목표인데 다만, 다형성 만으로는 OCP, DIP 원칙을 지킬 수 없다. 스프링의 DI와 DI 컨테이너는 이러한 한계점을 해결해 준다. 다시 말해, 클라이언트 코드의 변경 없이 기능을 자유롭게 확장할 수 있다는 것이다.

구현 객체를 생성하고, 연결하는 책임을 담당하는 AppConfig의 역할을 생각해본다면 IoC(제어의 역전) 과 함께 어떤 동작 원리인지 생각할 수 있을 것이다.

DI?

Dependency Injection의 약자로 의존성 주입이라고 한다.

import java.time.LocalDateTime;

public class MemberRegisterService {
    // MemberRegisterService 클래스가 MemberDao와 의존관계에 있다.
    private MemberDao memberDao = new MemberDao();
    
    public void regist(RegisterRequest req) {
    	// 이메일로 회원 데이터(Member) 조회
        Member member = memberDao.selectByEmail(req.getEmail());
        /**
           코드 생략
        **/
    }
}

위 코드를 보면, MemberRegisterService 클래스가 MemberDao 클래스에 의존하는 것을 확인할 수 있다. 이는 MemberDao가 변경된다면, 연쇄적으로 영향을 받을 수 있음을 의미한다. 이런 관계에 놓이면 앞서 언급한 SOLID 원칙이 깨진다. (특히 OCP, DIP 원칙)

클래스 내부에서 직접 의존 객체를 생성하는 것이 쉽지만, 장기적으로 본다면 기능 확장이나 유지보수의 측면에서 문제점을 유발할 수 있는 코드다.

DI는 이와 달리 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다.

import java.time.LocalDateTime;

public class MemberRegisterService {
    private MemberDao memberDao;
    // 생성자 주입
    public MemberRegisterService(MemberDao memberDao) {
    	this.memberDao = memberDao;
    }
    
    public void regist(RegisterRequest req) {
    	// 이메일로 회원 데이터(Member) 조회
        Member member = memberDao.selectByEmail(req.getEmail());
        /**
           코드 생략
        **/
    }
}

위 코드를 보면 이전과 달리 의존 객체를 직접 생성하지 않는 대신 생성자를 통해 의존 객체를 전달받는다. 이렇게 DI를 사용하면 변경의 유연함을 유지할 수 있다. MemberDao 클래스가 회원 데이터를 DB에 저장하고 있는데, 빠른 조회의 목적으로 캐시를 적용해야 하는 상황이 발생했다 치면 이 클래스를 상속받아 새로운 클래스를 만들어야 한다. 이 과정에서 보통 MemberDao와 연관관계에 있는 클래스가 있다면 모두 동일하게 코드를 변경해야 하지만, DI를 적용하면 수정할 코드가 줄어든다. 바로 객체를 주입하는 코드 한 곳만 변경하면 되는 것이다.

여기서 좀 더 나아가 객체 생성 및 의존 객체 주입하는 부분을 AppConfig로 따로 작성해보자.

import test1.ChangePasswordService;
import test1.MemberDao;
import test1.MemberRegisterService;

public class AppConfig {
    private MemberDao memberDao;
    private MemberRegisterService regSvc;
    private ChangePasswordService pwdSvc;
    
    public AppConfig() {
    	memberDao = new MemberDao();
        regSvc = new MemberRegisterService(memberDao);
        pwdSvc = new ChangePasswordService();
        pwdSvc.setMemberDao(memberDao);
    }
    
    public MemberDao getMemberDao() {
    	return memberDao;
    }
    
    public MemberRegisterService getMemberRegisterService() {
    	return regSvc;
    }
    
    public ChangePasswordService getChangePasswordService() {
    	return pwdSvc;
    }
}

이렇게 따로 관리하면 클래스의 변경에도 유연하게 대처할 수 있게 된다. Spring framework는 이러한 AppConfig를 특정 타입의 클래스만이 아닌 범용으로 사용하도록 하는 구조로 되어 있다.

Spring에서의 DI

다음 코드를 보면 AppConfig와 큰 차이는 없다고 느낄 것이다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import test1.ChangePasswordService;
import test1.MemberDao;
import test1.MemberRegisterService;

@Configuration
public class AppCtx {
    
    @Bean
    public MemberDao memberDao() {
    	return new MemberDao();
    }
    
    @Bean
    public MemberRegisterService memberRegSvc() {
    	return new MemberRegisterService(memberDao());
    }
    
    @Bean
    public ChangePasswordService changePwdSvc() {
    	ChangePasswordService pwdSvc = new ChangePasswordService();
        pwdSvc.setMemberDao(memberDao());
        return pwdSvc;
    }
}

스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 위와 같이 작성하면 된다.

  • @Configuration : 스프링 설정 클래스를 의미
  • @Bean : 해당 메서드가 생성한 객체를 스프링 빈이라고 설정.
  • AnnotationConfigApplicationContext : 객체를 생성하고 의존 객체를 주입하는 스프링 컨테이너
  • @Autowired : 스프링 빈에 의존하는 다른 빈을 자동으로 주입하고 싶을 때 사용

📌 Spring에서의 DI 방식?

  • 생성자를 통한 DI: 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
  • 자바빈의 Getter/Setter 프로퍼티 설정 메서드를 통한 DI: 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.

@Bean 설정과 싱글톤?

@Configuration
public class AppCtx {
    
    @Bean
    public MemberDao memberDao() {
    	return new MemberDao();
    }
    
    @Bean
    public MemberRegisterService memberRegSvc() {
    	return new MemberRegisterService(memberDao());
    }
    
    @Bean
    public ChangePasswordService changePwdSvc() {
    	ChangePasswordService pwdSvc = new ChangePasswordService();
        pwdSvc.setMemberDao(memberDao());
        return pwdSvc;
    }
}

memberRegSvc()changePwdSvc()는 매번 memberDao()를 실행하고 있고, 그 결과 매번 새로운 MemberDao 객체를 생성해서 리턴하고 있다.

🤔 그러면 MemberDao 객체 하나하나 모두 서로 다른 객체가 아닐까????

아니다

스프링 컨테이너가 생성한 빈은 싱글톤 객체이다. 스프링 컨테이너는 @Bean이 붙은 메서드에 대해 한 개의 객체만 생성하는데 위 코드에서 memberDao()를 여러 번 호출해도 항상 같은 객체를 리턴하는 것을 의미한다.

이는 스프링이 런타임에 생성한 설정 클래스 내의 memberDao() 메서드는 한 번 생성한 객체를 보관했다가 이후에는 동일한 객체를 리턴하기 때문이다.

References

profile
Step by step goes a long way.

0개의 댓글