[스프링 입문] Section 04 - 스프링 빈과 의존관계

Update:     Updated:

카테고리:

태그:

들어가며…

화면을 붙이고 싶음 -> 컨트롤러와 뷰 템플릿 필요
‘멤버 컨트롤러’가 ‘멤버 서비스’를 통해서 회원가입 하고, 데이터를 조회할 수 있어야 함
‘멤버 컨트롤러’와 ‘멤버 서비스’ -> 서로 의존관계가 있다.(멤버 컨트롤러가 멤버 서비스를 의존한다.)

spring의 주요 개념 IOC & DI

앞으로 등장할 스프링 컨테이너와 스프링 빈을 이해하기 위해선 Spring의 가장 큰 특징인 IOCDI에 대해서 알아야 한다.

- 제어의 역전 (`IOC`, Inversion Of Control)

일반적인 자바 프로그램에서는 각 객체들이 ‘프로그램의 흐름을 결정’하고 ‘각 객체를 직접 생성하고 조작하는 작업(객체를 직접 생성하여 메소드 호출)’을 했다.

즉, 모든 작업을 사용자가 제어하는 구조!

예를 들어 A 객체에서 B 객체에 있는 메소드를 사용하고 싶으면, B 객체를 직접 A 객체 내에서 생성하고 메소드를 호출한다.

하지만 IOC가 적용된 경우, 객체의 생성을 ‘특별한 관리 위임 주체’에게 맡긴다.
이 경우 사용자는 객체를 직접 생성하지 않고, 객체의 생명주기를 컨트롤하는 주체는 다른 주체가 된다.

즉, 사용자의 제어권을 다른 주체에게 넘기는 것을 IOC(제어의 역전) 라고 한다.

요약하면 Spring의 Ioc
클래스 내부의 객체 생성 -> 의존성 객체의 메소드 호출이 아닌,
‘스프링에게 제어를 위임’하여 스프링이 만든 객체를 주입 -> 의존성 객체의 메소드 호출 구조이다.

스프링에서는 모든 의존성 객체를 스프링이 실행될때 만들어주고 필요한 곳에 주입해준다.

- 의존성 주입 (`DI`, Dependency Injection)

어떤 객체(B)를 사용하는 주체(A)가 객체(B)를 직접 생성하는게 아니라

객체를 외부(Spring)에서 생성해서 사용하려는 주체 객체(A)에 주입시켜주는 방식이다.

사용하는 주체(A)가 사용하려는 객체(B)를 직접 생성하는 경우 의존성(변경사항이 있는 경우 서로에게 영향을 많이 준다)이 높아진다.

하지만, 외부(Spring)에서 직접 생성하여 관리하는 경우에는 A와 B의 의존성이 줄어든다.

자세한 건 뒤 쪽 코드를 보며 살펴보자.

스프링 컨테이너와 스프링 빈

- 스프링 컨테이너

‘스프링 컨테이너’는 ‘스프링 빈’의 생명 주기를 관리하며, 생성된 스프링 빈들에게 추가적인 기능을 제공하는 역할을 한다. IoCDI의 원리가 스프링 컨테이너에 적용된다.

개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸하지만, 스프링 컨테이너를 사용하면 해당 역할을 대신해 준다. 즉, 제어 흐름을 외부에서 관리하게 된다. 또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 준다.

- 스프링 빈

Spring에서는 직접 new를 이용하여 생성한 객체가 아니라, Spring에 의하여 관리당하는 자바 객체를 사용한다. 이렇게 Spring에 의하여 생성되고 관리되는 자바 객체를 Bean이라고 한다.

스프링 빈을 등록하는 방법

Image

스프링은 스프링이 뜰 때, 스프링 컨테이너에 객체를 생성해서 넣어두고 관리한다.

이것을 스프링 컨테이너에서 스프링 빈이 관리된다.고 한다.

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

@Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.

1) @Controller
2) @Service
3) @Repository

위의 세 가지 애노테이션 모두 @Component을 포함하기 때문에 스프링 빈으로 자동 등록된다.
생성자에 @Autowired를 사용하면 객체 생성 시점에서 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다.

주로 정형화된 컨트롤러, 서비스, 리포리토리 같은 코드는 컴포넌트 스캔을 사용한다.

코드를 살펴보자!

1) MemberController

// MemberController.java
package hello.hellospring.controller;

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

@Controller // Controller라는 어노테이션이 있으면, 스프링이 스프링 컨테이너(스프링 뜰 때 생김)에 객체를 생성해서 넣어두고 관리 == 스프링 컨테이너에서 스프링 빈이 관리된다
public class MemberController {
    /*
    컨트롤러가 MemberService를 가져다 써야하는데..

    private final MemberService memberService = new MemberService();
    스프링이 관리를 하게 되면, 다 스프링 컨테이너에 등록을 하고, 스프링 컨테이너로부터 받아서 쓰도록 바꾸어야 함
    이런 식으로 new 해서 인스턴스를 직접 생성해서 쓴다면, 다른 여러 컨트롤러가 MemberService를 가져다 쓰는 등의 상황에서 각각 다른 인스턴스를 사용하는 것이 되기 때문에
    하나만 생성해놓고 공유해서 쓰는 것이 좋다.
     */

    private final MemberService memberService;

    @Autowired
    /*
    1. 스프링이 뜸
    2. 스프링 컨테이너 생김
    3. 생성자 호출
    4. 스프링이 스프링 컨테이너에 있는 memberService를 컨트롤러에 자동으로 연결해줌(Autowired)
     */
    public MemberController(MemberService memberService) {  // 오류 뜨는 이유: 서비스에 어노테이션이 등록되어 있지 않다.
        this.memberService = memberService;
    }
}


2) MemberService

// MemberService.java
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 = new MemoryMemberRepository(); // 기존 방식: MemberService가 MemoryMemberRepository를 직접 생성하게 함
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {   // MemberService 코드를 DI 가능하게 변경한다.
        this.memberRepository = memberRepository;
    }

    /**
     * 회원가입
     */
    public Long join(Member member) {
        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);
    }
}


3) MemoryMemberRepository

// MemoryMemberRepository.java
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<>();   // ConcurrentHashMap을 사용함
    private static long sequence = 0L;  // AtomicLong을 사용함

    @Override
    public Member save(Member member) {
        member.setId(++sequence);   // sequence로 id 설정
        store.put(member.getId(), member);  // store라는 map 객체에 <id, member>인 객체 추가
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));  // 해당 id를 가진 객체가 없는데 조회를 할 경우, null 처리를 위해 ofNullable로 감싸줌
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()  // 3. 반환
                .filter(member -> member.getName().equals(name))    // 1. member.getName() 과 name이 같은지 확인하여
                .findAny(); // 2. 같은 걸 하나라도 찾으면
    }

    @Override
    public List<Member> findAll() { // store는 map인데, 반환은 리스트로 되어 있음 -> java에서 실무할 땐 루프 돌리기도 쉽고 해서 리스트를 많이 쓴다고 함
        return new ArrayList<>(store.values());
    }

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

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

SpringConfig 파일을 만들어 자바 코드로 직접 스프링 빈을 등록할 수 있다.

상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

예를 들자면, MemoryMemberRepository 를 DB와 연결하는 repository로 바꾸는 등의 상황이 있다.

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();
    }
}

DI 3가지 방법

1) 필드 주입
추천하지 않음. 뭔가 중간에 바꿀 수 있는 방법이 아예 없음.

@Controller
public class MemberController {
    @Autowired private MemberService memberService;
}

2) setter 주입
누군가가 MemberController를 호출했을 때 public으로 열려있어야 함.
setMemberService를 한 번 세팅을 하면 중간에 바꿀 이유가 없는데도 불구하고 public하게 노출되어야 한다는 단점이 있음.

@Controller
public class MemberController {
    private MemberService memberService;

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

3) 생성자 주입
생성자를 통해 memberService가 memberController에 주입되고 있다.
이처럼 생성자를 통해서 방법을 생성자 주입이라고 한다.
의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.

@Controller
public class MemberController {
    private final MemberService memberService;

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

Reference

1) https://steady-coding.tistory.com/594 [Spring Bean 총 정리]
2) https://melonicedlatte.com/2021/07/11/232800.html [스프링 빈(Spring Bean)이란? 개념 정리]

Spring-Tutorial 카테고리 내 다른 글 보러가기

댓글 남기기