본문 바로가기

Spring

Chapter 3. 스프링 DI

스프링 입문5

3장

1. 의존이란?

  • 객체 간의 의존을 의미한다.
  • 한 클래스가 다른 클래스의 메서드를 실행할 때 이를 '의존'한다고 표현
  • 의존은 변경에 의해 영향을 받는관계를 의미한다.

2. DI를 통한 의존 처리

  • DI(Dependecy Injection, 의존 주입)는 의존하는 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다.
  • 예를들어, 아래 코드는 생성자를 통해 MemberRegisterService가 의존(Dependency)하고 있는 MemberDao 객체를 주입(Injection) 받은 것이다.
package chap03.spring;

import java.time.LocalDateTime;

/**
 * 
 */
public class MemberRegisterService {
    private MemberDao memberDao;

    public MemberRegisterService(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public Long regist(RegisterRequest req) {
        Member member = memberDao.selectByEmail(req.getEmail());
        if(member != null) {
            throw new DuplicateMemberException("dup email" + req.getEmail());
        }
        Member newMember = new Member(req.getEmail(), req.getPassword(), req.getName(), LocalDateTime.now());
        memberDao.insert(newMember);
        return newMember.getId();
    }
}

3. DI와 의존 객체 변경의 유연함

  • 의존 객체를 직접 생성하는 방식이 아닌 의존하는 객체를 주입하는 방식을 사용하는 이유는?
  • 직접 객체를 생성한 코드가 여러 클래스에 분포되어 있는 경우 생성하려고 하는 객체가 변경 되었을 때 분포되어 있는 생성 코드를 다 변경해야 하는 번거로움이 있다.
  • 의존 주입을 사용해 객체를 생성하는 경우 클라이언트에서 주입하는 대상만 바꿔주면 된다.

4. 객체 조립기

  • 객체 생성에 사용할 클래스를 변경하기 위해 객체를 주입하는 코드 한 곳만 변경하면 된다고 했었다.
  • 그렇다면 실제 객체를 생성하는 코드는 어디에 존재할까?
    • 쉽게 생각하면 메인 메서드에서 객체를 생성하면 될 것 같다.
  • 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것 (= 조립기 클래스)
    • 조립기는 객체를 생성하고 의존 객체를 주입하는 기능을 제공한다.

5. 스프링의 DI 설정

  • 스프링은 앞서 구현한 조립기와 유사하게 필요한 객체를 생성하고 생성한 객체에 의존을 주입한다.
  • 객체를 제공하는 기능(메서드)을 정의하고 있다.

5-1. 스프링을 이용한 객체 조립과 사용

  • 스프링이 어떤 객체를 생성하고, 의존을 어떻게 주입할지를 정의한 설정 정보를 작성해야 한다.
  • @Configuration 어노테이션을 단 클래스 내부에서 @Bean 어노테이션을 단 메서드를 통해 빈 객체를 생성한다.
  • AnnotationConfigApplicationContext를 사용해서 스프링 컨테이너를 생성한다.
  • getBean() 메서드를 통해 객체를 불러온다.

6. 생성자 DI vs setter 메서드 DI장점

  • 생성자 방식 : 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
  • setter 메서드 방식 : 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.

단점

  • 생성자 방식 : 인수가 많아질 경우 생성자 코드를 확인해야하는 번거로움
  • setter 메서드 방식 : setter 메서드를 사용해서 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에 객체를 사용하는 시점에 NullPointerException이 발생할 수 있다.

7. @Configuration 설정 클래스의 @Bean 설정과 싱글톤

  • @Bean이 설정된 메서드가 호출될 때마다 새로운 객체를 생성해서 리턴하지 않을까?
  • @Bean이 붙은 메서드에 대한 한 개의 객체만 생성한다. 메서드를 몇번을 호출하더라도 항상 같은 객체를 리턴 한다.
  • 스프링은 설정 클래스를 그대로 사용하지 않고 설정 클래스를 상속한 새로운 설정 클래스를 만들어 사용한다.
  • 매번 새로운 객체를 생성하지 않고 한 번 생성한 객체를 보관했다가 이후에는 동일한 객체를 리턴하도록 한다.

8. 두 개 이상의 설정 파일 사용하기

  • 스프링은 한 개 이상의 설정 파일을 이용해서 컨테이너를 생성할 수 있다.
  • @Autowired 애노테이션을 활용하여 설정 파일간 등록된 빈도 사용할 수 있다.
@Configuration
public class AppConf1 {
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }

    @Bean
    public MemberPrinter memberPrinter() {
        return new MemberPrinter();
    }
}

@Configuration
public class AppConf2 {
    @Autowired
    private MemberDao memberDao;
    @Autowired
    private MemberPrinter memberPrinter;

    ... // 생략
}

8-1. @AutoWired 애노테이션

  • @Autowired 애노테이션은 스프링 빈에 의존하는 다른 빈을 자동으로 주입하고 싶을 때 사용한다.
  • 아래의 예제와 같이 @Autowired를 사용하여 의존 객체를 주입한 경우. setter 메서드를 사용해서 의존 주입을 하지 않아도 @Autowired를 붙인 필드에 스프링 컨테이너가 자동으로 해당 타입의 빈 객체를 주입 해준다.
// @Autowired를 사용하지 않은 경우
public class MemberInfoPrinter {

    private MemberDao memberDao;
    private MemberPrinter printer;

    public void printMemberInfo(String email) {
        Member member = memberDao.selectByEmail(email);
        if(member == null) {
            System.out.println("데이터 없음");
            return;
        }
        printer.print(member);
        System.out.println();
    }

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void setPrinter(MemberPrinter printer) {
        this.printer = printer;
    }
}

// @Autowired를 사용한 경우
public class MemberInfoPrinter {

    @Autowired
    private MemberDao memberDao;
    @Autowired
    private MemberPrinter printer;

    public void printMemberInfo(String email) {
        Member member = memberDao.selectByEmail(email);
        if(member == null) {
            System.out.println("데이터 없음");
            return;
        }
        printer.print(member);
        System.out.println();
    }
}

 

8-2. @Import 애노테이션 사용

  • 두 개 이상의 설정 파일을 사용하는 또 다른 방법으로 @Import 애노테이션을 사용하는 것이 있다.
  • @Import 애노테이션은 함께 사용할 설정 클래스를 지정한다.
// 위의 예 AppConf1에 AppConf2를 포함하여 1개의 설정 파일처럼 사용하게 만든 예제
@Configuration
@Import(AppConf2.class)
public class AppConfImport {
    @Bean
    public MemberDao memberDao() {
        return new MemberDao();
    }

    @Bean
    public MemberPrinter memberPrinter() {
        return new MemberPrinter();
    }
}

// 두 개 이상의 설정 클래스도 지정 가능하다.
@Configuration
@Import({AppConf1.class, AppConf2.class})
public class AppConfImport {
    
}

 

9. getBean() 메서드 사용

  • getBean() 메서드의 첫 번째 인자는 빈의 이름, 두 번째 인자는 빈의 타입이다.
  • 빈 이름을 지정하지 않고 타입만으로도 빈을 구할 수 있다.
    • 해당 타입의 빈 객체가 한 개만 존재하면 해당 빈을 구해서 리턴한다.
    • 해당 타입으로 여러개의 빈이 등록된 경우 익셉션(NoUniqueBeanDefinitionException) 이 발생한다.
  • getBean() 메서드 호출 시 존재하지 않는 빈 이름을 사용하면 익셉션(NoSuchBeanDefinitionException)이 발생한다.
  • 반대로 지정된 타입이 다를 경우 익센셥(BeanNotOfRequiredTypeException)이 발생한다.
VersionPrinter versionPrinter = ctx.getBean("versionPrinter", VersionPrinter.class);

VersionPrinter versionPrinter = ctx.getBean(VersionPrinter.class);

// 존재하지 않는 빈 이름을 사용하여 메서드 호출한 경우
VersionPrinter versionPrinter = ctx.getBean("versionPrinter2", VersionPrinter.class);

// 빈의 실제 타입과 메서드에서 지정한 타입을 다르게 하여 메서드 호출한 경우
// 이름이 listPrinter인 빈의 타입이 MemberListPrinter 일 때
VersionPrinter versionPrinter = ctx.getBean("listPrinter", VersionPrinter.class);

 

10. 주입 대상 객체를 모두 빈 객체로 설정해야 하나?

  • 주입할 객체가 꼭 스프링 빈이어야 할 필요는 없다.
  • 아래 예제와 같이 MemberPrinter 객체를 생성해서 사용해도 정상적으로 동작한다.
  • 객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다.
  • 스프링 컨테이너가 제공하는 관리 기능이 필요없고 getBean() 메서드로 구할 필요가 없다면 빈 객체로 꼭 등록해야 하는 것은 아니다.
@Configuration
public class AppCtxNoMemberPrinterBean {
	private MemberPrinter printer = new MemberPrinter(); //빈이 아님
	
    ... // 생략
	@Bean
	public MemberListPrinter listPrinter() {
		return new MemberListPrinter(memberDao(), printer);
	}
	
	@Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		infoPrinter.setMemberDao(memberDao());
		infoPrinter.setPrinter(printer);
		return infoPrinter;
	}
	... // 생략
}

// MemberPrinter를 빈으로 등록하지 않았으므로
// 아래 코드는 익셉션을 발생한다.
MemberPrinter printer = ctx.getBean(MemberPrinter.class);

 

'Spring' 카테고리의 다른 글

Spring과 Spring Boot의 차이  (0) 2021.07.06
Chapter 2. 스프링 컨테이너, 싱글톤 객체  (0) 2021.06.16
Chapter 1. 들어가며  (0) 2021.06.16