본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 컴포넌트 스캔과 자동 의존관계 설정

by 방구석 대학생 2021. 11. 7.

"인프런의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확인해주세

www.inflearn.com

 

도메인, 서비스, 레포지토리를 만들었으니, 컨트롤러도 만들어주자.

MemberController 를 통해서 사용자의 입력에 따라 뷰를 조작하거나 레포지토리, 서비스 측을 통해 얻어온 모델 정보를 뷰 측에 뿌려주는 역할을 수행하게끔 해야한다.

즉, 컨트롤러가 서비스, 레포지토리 등에서 제공해주는 기능들을 이용할 수 있어야 한다는 것이다.

이런것들을 두고 의존관계가 있다고 표현한다.

- 이전에 회원 서비스 테스트 강의에서 MemberService 측이 MemoryMemberRepository 를 통해 memberRepository 객체 변수 값을 생성했던 것을 의미, 즉 DI(Dependency Injection)

 

회원 서비스 테스트 강의에서 수행해봤던 DI 를 좀 더 스프링 스럽게 작업해보자.

 

MemberController 를 생성한다.

- MemberController.java

@Controller
public class MemberController {
}

위와 같이 아무 내용없이 단순히 컨트롤러만 생성해 놓는것으로 어떤일이 벌어질까?

 

스프링이 동작할 때 스프링 컨테이너 라는 공간이 생기는데, 만약 @Controller 어노테이션이 붙어있는 파일이 있다면 해당 파일의 객체를 생성해서 스프링 컨테이너 내부에 적재해둔다

즉, 위와 같이 파일을 생성해서 @Controller 어노테이션을 달아두는 것 만으로도 MemberController 객체가 생성되어서 스프링 컨테이너에 적재된다는 뜻이다.

스프링 컨테이너에 객체가 적재된 이후부터는 스피링이 해당 객체를 관리하기 시작하는데, 이를 두고 스프링 컨테이너 에서 스프링 빈을 관리한다고 표현한다.

 

이와 같이 스프링 컨테이너에 컨트롤러, 서비스와 같은 것들이 스프링 빈으로서 적재되면 스프링이 각 객체들에 대해 관리를 하기 시작하면서, 개발자가 작성해놓은 회원 가입, 회원 조회와 같은 기능들이 정상적으로 동작하게 되는 것이다.

 

스프링이 객체를 관리하게 되면 컨트롤러, 서비스, 레포지토리와 같은 것들은 모두 스프링 컨테이너에 적재 시킨 후, 그 컨테이너에서 필요한 것들을 받아와서 사용하는 방식으로 바꾸어야 한다.

즉, 각 객체를 필요에 의해서 생성할 때 new 와 같은 키워드를 사용하여 따로 메모리에 적재를 해두려고 하면 안된다는 것이다.

- 이전 강의에서 얘기했던 바와 같이 같은 데이터베이스에 접근하지 못하게 되는 문제가 발생하게 된다.

 

- MemberController.java

@Controller
public class MemberController {


    private final MemberService memberService;

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

 

 

위와 같이 코드를 작성하면 스프링은 어떻게 객체를 생성해서 컨테이너에 적재해 두는 걸까?

 

우선 스프링 컨테이너에 등록할 객체(MemberController)의 생성자를 호출한다.

이때 @Autowired 어노테이션이 생성자에 붙어있으면 스프링이 스프링 컨테이너에 있는 memberService 객체를 찾아서 자동으로 컨트롤러 객체에 연결시켜준다.

(프로젝트를 할 때는 이와같은 의존성 주입을 해줄 때 @Autowired 어노테이션을 사용하기는 했지만 어떤 방식으로 동작하는지 모르고 사용하느라 객체에 대한 생성자를 만들어서 쓰지도 않았고, private final 과 같이 상수화를 해놓지도 않았다.)

(서비스의 경우 서비스를 인터페이스로 만들어놓고 그 서비스를 구현하는 Impl 클래스를 만든 다음, 그 클래스를 컨트롤러에 의존관계로 연결해주었다.)

(객체 생성을 위한 생성자를 작성해놓지 않았어도 객체 자체는 스프링에서 알아서 잘 생성해 줬을지는 모를일이지만, private final 키워드를 모르고 쓰지 않았던걸 생각해보면 내가 했던 프로젝트에서 의존성 주입은 제대로 되었을지언정 어플리케이션이 동작하는 중간에 주입된 객체의 데이터 값이 변경되는 치명적인 상황을 마주하게 됐을지도 모르겠다.)

 

그럼 여기까지 코드를 작성해놓고 어플리케이션을 동작시켜 보자.

그러면 다음과 같은 오류를 발견하게 될 것이다.

MemberController required a bean of type 'hello.hellospring.service.MemberService' that could not be found.

 

생성자에서 사용한 @Autowired 어노테이션이 있으면 스프링 컨테이너에서 MemberService 객체 값을 가져온다고 위에서 설명했었다.

그런데 MemberService 를 찾지 못했다는 오류 로그가 발생했다는 것은 쉽게 말해, 지금 스프링 컨테이너에 스프링 빈으로서 MemberService 객체가 적재 되어있지 않다는 뜻이다.

아래의 링크를 타고 가면 글 초반에 MemberService 클래스의 코드가 작성되어 있는것을 확인할 수 있다.

https://evan-development.tistory.com/109

 

스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 회원 서비스 개발

"인프런의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 듣고 작성한 글 입니다." MemberService - 회원 Repository 와 도메인을 활용하여 실제 비즈니스 로직을 작성하는 파트

evan-development.tistory.com

여기서 MemberService 를 살펴보면 현재 작성해놓은 MemberService 는 순수한 자바 클래스로서 작성되었음을 알 수 있다.

즉, 스프링이 MemberService 의 존재를 알 수 있는 방법이 없다는 것이다.

 

컨트롤러의 경우 @Controller 어노테이션을 통해서 스프링이 컨트롤러의 존재를 인식하고 스프링 컨테이너에 스프링 빈으로서 객체 값을 등록해 줄 수 있는데 반해, MemberService 의 경우 그렇지 않다

그럼 어떻게 해야 하느냐, MemberService 클래스에서 클래스 이름 위에 @Service 어노테이션을 붙여주자.

 - MemberService.java

@Service // @Service 어노테이션 추가
public class MemberService {

    private final MemberRepository memberRepository;
    
    @Autowired // @Autowired 어노테이션 추가
    public MemberService(MemberRepository memberRepository) {
        	this.memberRepository = memberRepository;
    }
	
    // 이후 코드 동일 
}

* 참고 : 생성자에 @Autowired 어노테이션을 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈(memberRepository)을 찾아서 주입한다.

그런데 만약 생성자가 1개만 있으면 @Autowired 어노테이션을 생략할 수 있다.

 

여기서 MemberService 파일을 보면 MemberRepository 클래스의 의존성을 주입 받고 있는것을 알 수 있다.

그렇기 때문에 MemberRepository 인터페이스를 구현하고 있는 구현체인 MemoryMemberRepository 클래스로 가서 다음과 같이 작성해주자.

- MemoryMemberRepository.java

@Repository // @Repository 어노테이션 추가
public class MemoryMemberRepository implements MemberRepository{
	// 이하 코드 동일
}

 

정형화된 Spring 웹 MVC 디자인 패턴 

컨트롤러를 통해 외부의 요청을 받고, 서비스를 통해 비즈니스 로직을 만들고, 레포지토리를 통해 데이터를 저장하고, 이와 같은 패턴이 Spring 웹 MVC 의 디자인 패턴으로서 정형화되어 있다.

컨트롤러 객체가 생성될 때, 그리고 서비스 객체가 생성될 때 생성자에 @Autowired 와 같은 어노테이션을 통해 클래스간에 의존성 주입을 통해 연결을 시켜주는 것을 바로 DI(Dependency Injection - 의존성 주입) 라고 한다.

 

스프링 빈(Spring Bean) 을 등록하는 2가지 방법

스프링 빈을 등록하는데는 다음과 같은 2가지 방법이 있다.

1. 컴포넌트 스캔과 자동 의존관계 설정(현재 학습중인 방법)

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

 

원래 사실은 @Controller, @Service, @Repository 가 아니라 @Component 어노테이션을 붙여주면 된다.

그런데 어떻게 @Component 어노테이션이 아니라 위와 같이 각자 역할에 맞는 어노테이션을 붙여주는 것으로 각 클래스 객체 값이 스프링 컨테이너에 스프링 빈으로서 잘 적재가 되는 것일까?

각 어노테이션을 Ctrl + 좌 클릭을 통해 세부 내용을 확인해보면 어노테이션의 내부 구성으로 @Component 어노테이션이 붙어 있는 것을 확인할 수 있다.

- @Controller 어노테이션의 경우 세부내용

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // @Component 어노테이션이 있는것을 확인할 수 있다.
public @interface Controller {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}

 

스프링이 동작할 때 @Controller 와 같이 @Component 어노테이션과 관련있는 어노테이션이 부착된 클래스가 확인되면, 스프링은 그 클래스들의 객체를 만들어서 스프링 컨테이너에 적재한다.

그렇게 등록이 되고 나면 @Autowired 어노테이션을 통해 각 클래스들 끼리 어떻게 의존관계가 설정되어 있는지 확인하고 각 클래스들간의 의존관계를 연결해줌으로서 의존관계를 주입 받은 클래스에 구현되어 있는 각종 기능들을 사용할 수 있게 되는 것이다.

- 스프링 빈 등록과 클래스간 의존관계 연결을 통해 컨트롤러에서 정상적으로 서비스, 레포지토리에 작성되어있는 비즈니스 로직 관련 기능, 데이터 삽입, 조회 관련 기능들을 사용할 수 있게 된다.

- 이와 같은 방식이 바로 스프링 빈을 등록하는 2가지 방법 중 컴포넌트 스캔과 자동 의존관계 설정에 해당한다.

 

그렇다면 아무 클래스에나 @Component 이나 @Service 같은 어노테이션을 달아줘도 스프링 빈에 정상적으로 잘 등록이 될까?

결론을 말하자면 그렇지 않다.

 

기본적으로 스프링이 동작할 때 HelloSpringApplication 클래스와 같이 main 메소드가 실행되는 클래스(@SpringBootApplication 어노테이션이 부착되어 있는 클래스) 가 있는 패키지를 기준으로 하위 패키지들 까지 컴포넌트 스캔을 하게 된다.

즉, main 메소드와 전혀 다른 패키지에 존재하는 클래스에 @Component 나 @Service 와 같은 어노테이션이 붙어있다고 해도 그 클래스들의 객체가 스프링 빈으로 등록되는 것은 아니라는 뜻이다.

* main 메소드와 전혀 다른 패키지에 존재하는 클래스들에 대해서는 컴포넌트 스캔이 적용되지 않는다.

 

스프링은 컨테이너에 스프링 빈을 등록할 때 기본적으로 싱글톤으로 등록한다.

싱글톤이란?

스프링 컨테이너에 스프링 빈을 등록할 각각의 객체들을 딱 유일하게 하나씩만 등록하는 방식이다.

- MemberController 는 MemberController 하나만, MemberService 는 MemberService 하나만 등록되는 방식

- 유일하게 하나씩만 등록해서 서로 공유한다.

 

따라서 같은 스프링 빈을 가져왔다면 모두 같은 인스턴스이다.(설정으로 싱글톤이 아니게끔 할 수는 있지만 왠만하면 싱글톤으로 사용한다.)

* 스프링을 쓰면 어쨌든 왠만한 것들은 다 스프링 빈으로 등록해서 사용해야 이점이 많다.

* 뭐가 어떻게 좋은지는 앞으로의 강의를 통해 학습하도록 하자.