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

김민범·2024년 10월 19일

Spring

목록 보기
5/29

@Controller을 해두면, 아무것도 없지만 어떤 일이 벌어지냐면 Spring 컨테이너가 Spring 처음에 뜰 때 Spring이란 통이 생긴다.

거기에 이 컨트롤러 애노테이션이 있으면 컨트롤러 객체를 생성하여 Spring에 넣어두고 이를 Spring이 관리한다.

→ 이를 스프링 컨테이너에서 스프링 빈이 관리된다고 표현.

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


들어가기전에

i. 의존관계란 뭘까? : https://7942yongdae.tistory.com/177

의존은 객체 간의 의존을 의미한다. 간단하게 아래의 예시를 보자.

public class LottoService{
    private LottoTicket winLottoTicket = new WinLottoTicket();

    public int checkNumber(LottoTicket userTicket){
        return winLottoTicket.checkSameNumber(userTicket);
    }
}

위의 코드에서 집중해서 보아야 할 점은, LottoService라는 클래스가 LottoTicket이라는 클래스의 메서드를 사용한다는 점이다.

즉, 간단하게 말해서 객체에서 '의존'한다는 의미는 한 클래스가 다른 클래스의 메서드를 실행하는 것을 의미한다.

A객체가 B 객체에게 의존한다 = A객체가 B객체를 사용한다 = A → B

이러한 상황에서 변경에 의한 영향을 생각해보게 되면, B의 변화는 결국 A에게 영향을 주게 된다. 즉, 의존한다라는 것이 생각보다 많은 책임이 따르고, 변경에 영향을 받게 된다는 것을 알게 된다.

그렇다면, 이러한 의존을 어떻게 처리하는 것이 효과적인 방법일까?

기존에 만들어뒀던 winLottoTicket이 아닌, CachedWinLottoTicket (캐시는 간단하게 말하자면 자주 사용하는 정보를 모아둔 임시 장소 같은 느낌이다.)으로 바꿨다고 생각해보자.

이렇게 수정하였을 시, 지금은 수정해야 할 클래스가 하나이지만, 가령 이와 같은 의존 주입을 해야 되는 클래스가 10개, 100개라면 모두를 찾아서 수정하는 것은 번거로운 일이 될 것이다. 또한 여러 객체가 B라는 객체를 의존하고 있을 때, B

그렇기 때문에, 스프링에서는 이렇게 의존하게 되는 객체 간의 관계, 즉 의존성을 맺어주기 위해 의존성 주입 (Dependency Injection)을 사용하게 된다.

ii. 의존성주입(DI)

그럼 의존성 주입은 뭘까? 딱딱하게 이야기하면 객체의 생성과 사용 관심사를 분리하는 방법이라고 말하겠지만 코드로 풀이해보면 "객체는 내가 만들게 넌 사용해" 이다.

예제 코드를 통해 의존성을 주입하는 방법을 알아보자.

public class Main {

  public static void main(String[] args) {
	  Controller controller = new Controller();
    controller.print();
  }
}

class Controller {

  private Service service;

  public Controller() {
    this.service = new Service();
  }

  public void print() {
    System.out.println(service.message());
  }
}

class Service {

  public String message() {
    return "Hello World!";
  }
} 

예제 코드로 작성한 내용은 Controller 객체가 Service 객체를 접근해서 메시지인 "Hello World!"를 출력하는 코드다. 의존성을 설명할 때와 마찬가지로 두 객체 사이에는 직접적인 관계가 있다. Controller 객체는 Service 객체를 알고 있고 직접 만들어서 메시지를 사용한다.

이번에는 직접적인 관계에 있는 Controller 객체와 Service 객체 사이에 의존성을 주입해 간접적인 관계가 될 수 있도록 만들어보자.

public class Main {

  public static void main(String[] args) {
    Controller controller = new Controller(**new MessageService()**);
    controller.print();
  }
}

interface IService {
  String message();
}

class Controller {

  private IService service;

  public Controller(IService service) {
    this.service = service;  // new가 사라짐!
  }

  public void print() {
    System.out.println(service.message());
  }
}

class MessageService implements IService {

  public String message() {
    return "Hello World!";
  }
}

예제 코드를 실행시키면 화면에는 "Hello World!"가 출력된다. 예제 코드의 결과는 동일하지만 의존성을 주입할 수 있도록 코드를 수정하였다. 이제는 Controller 객체가 MessageService 객체를 생성하지 않고 인터페이스인 IService 객체를 사용해 메시지를 출력한다. 이전 코드와 달리 Controller 객체는 MessageService 객체를 몰라도 IService를 이용해 메시지를 출력할 수 있다.

더 이상 Controller 객체와 Message 객체의 사이는 이전처럼 구체적이지 않다. 여전히 구체적으로 알긴 하지만 이전보다는 구체적이지 않다. 이제 Controller 객체는 new 키워드를 사용해 Message 객체를 생성하지 않는다. 코드 라인 수가 증가하고 객체가 더 늘어났지만 이젠 생성하고 사용하는 직접적인 관계가 아닌 사용만 하는 조금 직접적인 관계가 되었다. 정말 "객체(MessageService)는 내가(Main) 만들게 넌 사용(IService) 해"라는 상황이 된 것이다

그렇다면 어떻게 관심사가 분리된 걸까? 바로 간호사가 주사로 약을 놔주듯이 main() 함수에서 MessageService를 주입했기 때문이다. Controller 객체와 Service 객체가 직접적인 관계에 있을 때는 지금처럼 main()이 간호사처럼 객체를 생성해서 주입해주지 않았다.

✅ **의존성을 주입하는 이유**

지금까지 의존성 주입이 어떤 것인지 알아보았다. 그렇다면 우리는 왜 의존성을 주입하려고 하는 걸까?
그 이유는 바로 생성과 사용에 대한 관심을 분리하게 되면 생성에 대한 책임을 다른 누군가에 위임할 수 있는 동시에 필요에 따라 객체 생성 방식을 선택할 수 있기 때문이다. 최종적으로는 객체들이 가지는 강한 결합을 느슨하게 만들 수 있고 이는 설계의 유연성을 부여한다.

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

의존관계에 대해서 알아봤으니, 다시 돌아와서 회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있게 의존관계를 준비하자. ( 컨트롤러가 서비스를 의존하게끔!! = 컨트롤러가 서비스를 사용하게끔!!!)

회원 컨트롤러에 의존관계 추가

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;
    
		@Autowired
		public MemberController(MemberService memberService) {
				this.memberService = memberService;
    }
}
  • new를 사용하여 MemberService를 가져다 쓸 수도 있다.
    • 매번 여려개의 객체를 new를 통해 생성하여 쓰는 것 보단, 스프링 컨테이너에 딱 하나만 등록을 한 이후 사용하면 좋다.
  • 생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다.
    • 위 코드에서는 스프링이 스프링 컨테이너에 있는 MemberService를 가져다가 자동으로 MemberController와 연결을 시켜준다!!!!
    • 이렇게 객체 의존관계를 외부(=스프링)에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.
  • 이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired에 의해 스프링이 주입해준다.

오류 발생

Consider defining a bean of type 'hello.hellospring.service.MemberService' in
your configuration.

memberService가 스프링 빈으로 등록되어 있지 않기때문!!!.( 스프링 컨테이너에 존재하지 않는다. )

→ why? 현재 memberService는 순수한 자바 코드일 뿐…. 어떠한 애노테이션도 존재하지 않아 스프링에 연결되어 있지 않다….

참고: helloController는 스프링이 제공하는 컨트롤러여서 스프링 빈으로 자동 등록된다. @Controller 가 있으면 자동 등록됨

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

  1. 컴포넌트 스캔과 자동 의존관계 설정
  2. 자바 코드로 직접 스프링 빈 등록하기

c. 컴포넌트 스캔 원리

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

  • @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.

  • @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.

    • @Controller
    • @Service
    • @Repository

d. 회원 서비스 스프링 빈 등록

@Service
public class MemberService {

		private final MemberRepository memberRepository;
    
		@Autowired
		public MemberService(MemberRepository memberRepository) {
				this.memberRepository = memberRepository;
    }
}
✅ **생성자에 `@Autowired` 를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입**한다. 생성자가 1개만 있으면 @Autowired 는 생략할 수 있다.

e. 회원 리포지토리 스프링 빈 등록

@Repository
public class MemoryMemberRepository implements MemberRepository {}

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


  • 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.
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());
				// 이렇게 하면 스프링이 실행될 때 이 Configuration을 인식하여 로직을 실행시켜 MemberService를 스프링빈에 등록해준다.
    }

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

여기서는 향후 메모리 리포지토리를 다른 리포지토리로 변경할 예정이므로, 컴포넌트 스캔 방식 대신에 자바 코드로 스프링 빈을 설정하겠다.

→ why?

멤버리포지터리를 설계할때 아직 DB가 선정되지 않은 문제가 있어서 인터페이스를 설계하고 구현체로 메모리 멤버 리포지터리를 쓰는 중이다.

그런데 나중에 이 메모리 멤버 리포지토리를 다른 레포지토리로 변경할 것 이다. 이때 기존에 운영중인 코드를 하나도 손대지 않고 바꿀 수 있는 방법이 있다.

→ 이를 위해서 스프링 빈으로 설정한것이다.

Controller는 어쩔 수 없이 컴포넌트 스캔을 사용한다. 컨트롤러는 어차피 스프링이 관리하는 거기 때문….

참고: XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않으므로 생략한다.

참고: DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.

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

주의: @Autowired 를 통한 DI는 helloController , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

스프링 컨테이너, DI 관련된 자세한 내용은 스프링 핵심 원리 강의에서 설명한다.

1개의 댓글

comment-user-thumbnail
2024년 10월 20일

정리가 엄청 깔끔하시네요 잘보고갑니다

답글 달기