[김영한의 스프링 입문] 4. 스프링 빈과 의존관계

스프링 빈과 의존관계

1. 컴포넌트 스캔과 자동 의존관계 설정

화면을 붙이고 싶은데 컨트롤러와 뷰템플릿이 필요하다. 그러려면 멤버 컨트롤러를 만들어야한다. 멤버컨트롤러가 멤버서비스를 통해서 회원가입하고 데이터를 조회할 수 있어야한다. 이 것을 의존관계라고 하며 컨트롤러가 서비스에 의존한다고 표현한다. 일단 멤버 컨트롤러를 만들어야한다.

일단 MemberController를 만들고 @Controller를 붙이면, 기능은 아무것도 없지만, 스프링 컨테이너가 스프링이 처음 뜰때 스프링 통이 생긴다. 애노테이션이 있으면 멤버컨트롤러 객체를 생성해서 스프링에 넣어두고 관리를 한다. 이 컨트롤러라는 애노테이션을 보고 스프링이 뜰때 객체를 생성해서 스프링을 딱 들고있다. 스프링 컨테이너에서 스프링 빈이 관리된다고 표현을 한다.

스프링 부트안에 스프링 컨테이너와 내장 톰캣 서버가 있다. 스프링 컨테이너안에 컨트롤러 객체가 있다. 이 컨트롤러라는 애노테이션이 있으면 객체를 생성해서 관리하는 것이고 이게 동작이 되는 것이다. 멤버 서비스를 가져다가 써야하는데, new 생성자로 직접 생성하여 쓸수는 있으나, 스프링 컨테이너로부터 객체를 받아서 쓰도록 바꿔야한다. 왜? 하나의 객체를 갖고 여러 컨트롤러가 사용하는 것이 효율적이기 때문이다. 또한 여러가지 효과도 있으나, 그것은 추후에 설명할 것이다.

생성자를 만든 후 생성자 위에 @Autowired 애노테이션을 붙이면 스프링에서 컨트롤러 객체를 생성할 떄 이 생성자를 호출한다.

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

스프링 컨테이너에서 멤버 서비스 객체를 이 컨트롤러에게 주입해야 하는데, 멤버 서비스가 컨테이너에 없다. 따라서 멤버 서비스 클래스에 @Service를 붙여줘야한다. 그러면 스프링이 올라올때 스프링 컨테이너에 멤버 서비스를 등록한다. 리포지토리 구현체도 마찬가지로 @Repository 애노테이션을 붙인다. 이 컨트롤러, 서비스, 리포지토리 이 세단계가 정형화된 패턴이다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
@Repository
public class MemoryMemberRepository implements MemberRepository {

스프링이 쫙 등록된 객체들을 가지고 컨트롤러에서 서비스를 연결시켜줄떄 @Autowired를 붙여주면 멤버 컨트롤러 객체를 생성할떄 스프링 컨테이너 안에있는 멤버 서비스 객체를 넣어준다.

멤버 서비스 객체를 생성할떄에도 마찬가지로 생성자 위에 @Autowired를 붙이면 스프링 컨테이너 안에 등록된 멤버 리포지토리 객체를 생성자의 파라미터로 넣어준다.

따라서 이렇게 해두면 리포지토리와 서비스, 컨트롤러가 연쇄적으로 의존관계를 맺게 된다.

사실은 @Service, @Controller, @Repository와 같은 애노테이션에는 @Component 애노테이션의 특수 케이스들로 등록되어있다. 따라서 @Component와 관련된 것이 있으면 쫙 스캔하여 하나씩 객체를 생성하여 컨테이너에 등록하며, Autowired를 통해 의존관계를 자동으로 설정시켜준다는 방법을 말한다.

  • @Component : 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
  • @Controller : 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 떄문이다.
  • @Component을 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.
    • @Controller
    • @Service
    • @Repository

아무때나 @Component를 붙이면 다 객체가 생성되나?

아무 패키지에나 아무 클래스를 만들어서는 객체 생성은 안된다. @SpringBootApplication 애노테이션 붙은 애플리케이션 클래스와 같은 경로 혹은 하위 경로에 있는 애들만 스프링이 뒤지기 때문이다. 실제로 이 애노테이션에 들어가보면 @ComponentScan이라는 애노테이션이 등록되어있기 때문에 이 클래스가 기준이 된다.

싱글톤 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 싱글톤(유일하게 하나만) 등록하며 이 객체를 공유한다. 이렇게 하면 메모리를 절약하므로 효율적이다. 싱글톤이 아니게 할수있는 방법도 있으나, 실무에서 왠만하면 싱글톤만 쓴다. 특수한 케이스만 아니라면

2. 자바 코드로 직접 스프링 빈 등록하기

직접 설정 파일을 등록할 수도 있다. 컨트롤러를 제외하고 일단 @Service, @Repository 애노테이션을 삭제한다. 그렇다면 컨트롤러 클래스의 생성자에서 해당 서비스를 컨테이너에서 찾을 수 없다는 오류가 뜰것이다. 그렇다면 SpringConfig라는 클래스를 만들고, @Configuration애노테이션을 붙인뒤, 이안에 스프링 빈을 등록하듯이 메서드를 작성한다.

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}

이렇게 하면 스프링이 뜰때 설정을 읽고 스프링 빈을 등록한다는 뜻으로 인식하여 이 로직을 실행하여 등록한다. 멤버 서비스가 스프링 빈에 등록이 된다. 근데 이 생성자에 멤버 리포지토리 객체를 넣어줘야한다.

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

컨트롤러는 관리해야하는 것이므로 어쩔수가 없다. 컴포넌트 스캔과 Autowired로 해줄 수 밖에 없다. 대신 이제 config를 통해 멤버 서비스 객체가 등록되었기때문에 이것을 멤버컨트롤러 생성자 호출시 이 객체를 넣어줄 것이다.

옛날에는 이 설정을 xml 문서의 형태로 설정헀었으나 지금은 거의 자바코드로 설정을 많이 하는편이다.DI 에는

  • 필드주입 : 필드에다가 @Autowired를 바로 붙이는 것. 별로 좋지 않다. 스프링 뜰때만 넣어주고 중간에 바꿔치기할수있는 방법이 없기때문

  • setter 주입 : 생성은 생성대로 되고 세터를 통해서 주입이 된다. 이것의 단점은 이 메서드는 Public으로 열려있게 되는데, 보통은 생성시 말고 그 후에 새로 주입할 일이 없음에도 불구하고, 누군가가 이 필드를 다른것으로 다시 주입할 가능성을 막을 수 없다는 점이다. 로딩 시점에서 조립할때만 바꾸는 것지 세팅이 한번 되고 나면 왠만하면 바꿀일이 없기때문이다.

    개발은 최대한 호출되지 않아야할 메서드는 접근제한자로 막아놔야한다. 따라서 객체 생성 시 말고 런타임시 필드가 동적으로 변경될 일이 없을때에는 세터 주입을 사용하지않는 것이다.

    private MemberService memberService;
      
    @Autowired
    public void setMemberService(MebmerService memberService) {
      this.memberService = memberService;
    }
    
  • 생성자 주입(우리가 만든 것) : 요새 권장되는 스타일, 모든것이 세팅되는 시점에 생성자를 통해 주입이 되고 끝이난다.

실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용하고, 정형화되지 않거나 상황에 따라 구현클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록해야한다. 예시로 들자면 메모리가 아니라 다른 데이터 베이스를 사용하도록 결정이 되었을 경우, 기존에 사용중이던 MemoryMemberRepository에서 다른 리포지토리 구현체로 사용 객체를 변경하고자 할때, 기존에 작성한 코드를 하나도 손대지 않고 이 사용 객체를 변경하려면 애노테이션을 삭제, 추가하지않고 설정 파일만 바꿈으로써 편리하게 변경이 가능한 것이다.

    @Bean
    public MemberRepository memberRepository() {
        return new DBMemberRepository();
    }

@Autowired 동작 조건 @Autowired를 통한 DI는 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록되지 않고 직접 생성한 객체에서는 동작하지 않는다. 또한 @Autowired를 붙인 생성자를 개발자가 직접 호출해도 @Autowired는 동작하지 않는다. 따라서 이 경우에도 직접 객체를 생성해서 넣어줘야 한다.

Comments