[Spring Boot] 5. 스프링 빈과 의존 관계

Noh_level0·2024년 1월 24일
0

Spring Boot

목록 보기
5/5

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

일단 코드를 보며 이해해 보자.

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    
    //private final MemberService memberService = new MemberService();

    private final MemberService memberService;
    //@Autowired private MemberService memberService; //필드 주입

//    private MemberService memberService;
//    @Autowired
//    public void setMemberService(MemberService memberService){
//        this.memberService = memberService;
//    }                                                 setter 주입

    @Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }
    /**
     * MemberService.java에 @Service를 달아주지 않으면
     * 'hello.hellospring.service.MemberService' that could not be found. 오류가 뜬다.
     * **/
}

코드의 의미를 하나씩 뜯어보도록 한다.

1.1 @Controller

이 어노테이션이 있으면 스프링이 시작될 때 스프링 컨테이너에 MemberController 객체를 생성해서 스프링에 넣어두고 스프링이 관리한다.
이를 스프링 컨테이너에서 스프링 빈이 관리된다고 표현한다.

1.2 private final MemberService memberService = new MemberService();

컨트롤러에서는 MemberService 객체를 사용해야 할 필요성이 있다.
이때 new를 통해 MemberService 객체를 생성하여 사용할 수도 있다.
그러나 현재 이 Controller가 아닌 다른 여러 Controller에서도 MemberService 객체를 사용할 수 있는데, MemberService는 큰 기능을 가지지 않으며 여러개를 생성할 필요성도 없으므로 하나만 생성하여 공용으로 사용하는 것이 좋다. 따라서 스프링 컨테이너에 등록하여 사용하는 것이 좋다.(스프링 컨테이너에 등록하게 되면 딱 하나만 등록된다.)

1.3 @Autowired

@Autowired 어노테이션을 통해 memberService을 스프링이 스프링 컨테이너에 있는 MemberService 객체와 연결해준다.
즉, @Autowired를 쓰면 Cotroller와 Service가 연결되며, MemberController가 생성될 때 스프링 빈에 등록되어 있는 MemberService 객체를 가져와 넣어준다.
이것이 바로 Dependency Injection이다.(의존관계 주입)

💡 MemberService.java에 @Service를 달아주지 않으면 'hello.hellospring.service.MemberService' that could not be found. 오류가 뜬다.
즉, @Service 어노테이션을 사용해 스프링이 관리해야하는 객체라는 것을 알 수 있도록 해준다. 이를 통해 스프링이 시작될 때 스프링이 스프링 컨테이너에 MemberService를 등록해준다.

1.4 스프링 빈을 등록하는 2가지 방법

  1. 컴포넌트 스캔과 자동 의존관계 설정
    1.1 @Controller, @Service, @Repository는 컴포넌트 스캔 방식
    1.2 @Autowired는 자동 의존관계 설정
  2. 자바 코드로 직접 스프링 빈 등록하기

스프링은 스프링 컨테이너에 스프링 빈을 등록할 때 기본적으로 싱글톤으로 등록한다.(유일한 하나만 등록하여 공유한다.)
즉, 같은 스프링 빈이면 모두 같은 인스턴스이다. 메모리 절약의 이점도 있음!
설정을 해준다면 싱글톤이 아니게 할 수 있지만, 특별한 경우가 아니라면 대부분 싱글톤을 사용한다.

💡주의!!
HelloSpringApplication의 main으로부터 스프링을 동작시키고 있다.
따라서 hello.hellospring 패키지부터 시작하여 이 하위의 파일은 스프링이 스캔하여 스프링 빈으로 등록을 하게되는데, hello.hellospring과 동일하거나 하위 패키지가 아니라면 스프링 빈으로 컴포넌트 스캔을 하지 않는다.


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

일단 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 어노테이션을 제거 후 진행한다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

//@Service
public class MemberService {

    private final MemberRepository memberRepository;

    //@Autowired
    public MemberService(MemberRepository memberRepository){ //생성자 사용, 이러한 것을 Dependency Injection이라고 한다.
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     * **/
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원X
        validateDuplicateMember(member); // 중복 회원 검증

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     * **/
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}
package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.*;

//@Repository
public class MemoryMemberRepository implements MemberRepository{
    
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; // 0, 1, 2와 같은 key값을 생성해 주는 것.
    
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values()); // store에 있는 Member들이 반환됨.
    }

    public void clearStore(){
        store.clear();
    }
}

이러한 상황에서 hello.hellospring 내에 SpringConfig.java 파일을 하나 생성 후 아래와 같이 작성한다.

package hello.hellospring;


import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    /**
     * 자바 코드로 직접 스프링 빈 등록하기!!
     * **/

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

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

이와 같이 작성하면 Spring이 시작될 때 @Configuration을 본 후 @Bean을 통해 스프링 빈에 등록해야 한다라는 인식을 하게된다.
따라서 MemberService, MemberRepository를 스프링 빈에 등록해준다.

이후 MemberService.java에서 스프링 빈에 등록되어 있는 MemberRepository를 생성자를 통해 넣어준다. 지금과 같은 경우에는 구현체로 있는 MemoryMemberRepository를 주입해준다. 즉, Service와 Repository가 연결된다.

💡 Controller에서는 @Controller 어노테이션이 없이 직접 빈으로 등록하는 것이 불가능하다고 한다.(어차피 스프링이 관리해야 하기때문)
따라서 @Autowired를 사용하여 직접 빈으로 등록한 MemberService 객체를 넣어줄 수 있다.


3. DI(Dependency Injection)의 3가지 방법

DI를 할 수 있는 방법으로는 다음과 같은 3가지 방법이 있다.

  • 필드 주입
  • setter 주입
  • 생성자 주입

실행 도중 의존 관계가 동적으로 변경되는 경우는 거의 없다. 따라서 생성자 주입을 권장한다고 한다.
코드를 통해 해당 3가지 방법을 살펴본다.

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    
    //private final MemberService memberService = new MemberService();

    private final MemberService memberService;
    //@Autowired private MemberService memberService; //필드 주입

//    private MemberService memberService;
//    @Autowired
//    public void setMemberService(MemberService memberService){
//        this.memberService = memberService;
//    }                                                 setter 주입

    @Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }
    /**
     * MemberService.java에 @Service를 달아주지 않으면
     * 'hello.hellospring.service.MemberService' that could not be found. 오류가 뜬다.
     * **/
}

3.1 필드 주입

@Autowired private MemberService memberService;

이와 같은 방법을 필드 주입이라고 하며, final 키워드를 사용할 수 없다. final 사용 시 선언과 동시에 초기화를 해야한다. 그러나 객체의 생성 즉, 생성자 이후 호출되므로 final 키워드 사용이 불가능하다.

3.2 setter 주입

private MemberService memberService;

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

이와 같은 방법을 setter 주입이라고 하며, final 키워드를 사용할 수 없다. 마찬가지로 final 사용 시 선언과 동시에 초기화를 해야한다. 하지만 객체의 생성 즉, 생성자 이후 호출되므로 final 키워드 사용이 불가능하다.

setter주입의 단점
누군가가 MemberController를 호출했을 때 해당 setter 메서드가 public으로 열려있어야 한다. 그러나 setMemberService를 중간에 바꾸는 일은 거의 없다. 그러므로 public으로 노출이 되는 문제가 있다.(중간에 잘못 바꾸면 문제가 발생할 수 있음.)

3.3 생성자 주입

private final MemberService memberService;

@Autowired
    public MemberController(MemberService memberService) { //생성자로 스프링 컨테이너에 등록(생성자 주입)
        this.memberService = memberService;
    }

이와 같은 방법을 생성자 주입이라고 하며, final 키워드를 사용할 수 있다는 장점이 있다!!


4. 참고 및 주의사항

실무에서는 주로 정형화되어있는 컨트롤러, 서비스, 리포지토리와 같은 코드에서는 컴포넌트 스캔을 사용한다고 한다.
만일 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다고 한다.

ex) DB를 현재 지정하지 않은 상황인데, 차후 DB를 선정했다고 가정하면

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

에서

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

로만 변경하면 나머지를 손댈 필요가 없어진다.(구현체를 바꿔준다.)

❗주의❗
@Autowired를 통한 DI는 helloController, MemberService처럼 스프링이 관리하는 객체에서만 동작한다.
만일 스프링 빈으로 등록하지 않고, 직접 생성한 객체에서라면 동작하지 않는다.(스프링 컨테이너에 올라가야 @Autowired 기능이 동작한다.)

ex) MemberService를 스프링 빈으로 등록하지 않은 상태에서 해당 클래스 내에 @Autowired를 사용하더라도 동작하지 않는다.(스프링이 MemberService를 관리하지 않기 때문)

또한

public static void main(String[] args){
	MemberService memberService = new MemberService();
}

이와 같이 new를 통해 새로운 객체를 생성하는 경우에도 @Autowired를 사용하더라도 동작하지 않는다.




본 포스팅은 inflearn 김영한 강사님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의에 기반하여 작성되었습니다.
강의 링크

0개의 댓글