1. 의존관계 설정하기
1.1 의존관계란?
이전 장에서 회원 관리 서비스의 마지막에 MemberService에서 사용되는 MemberRepository 가 테스트코드와 서로 다른 객체를 바라보고 있었고, 이를 통일시켜주기 위해서 MemberService 클래스에 있는 생성자에 MemberRepository를 주입하는 "의존성 주입 (Dependencies Injection)"에 대해 간략하게 설명드렸습니다.
이번 장에서는 이 부분으로 조금 더 깊게 알아보려하는데, 예시로 회원 컨트롤러가 회원 서비스와 회원 레포지토리를 사용할 수 있도록 의존관계를 추가한다고 가정해 보겠습니다. 이를 위해 controller 패키지에 "MemeberController"라는 회원 컨트롤러를 먼저 생성하도록 하겠습니다.
[Java Code - MemberController.java]
package com.example.hellospring.controller;
import com.example.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
}
맨 처음 HelloController 를 생성했을 때도 보았지만, 위의 예시에서도 컨트롤러를 통해서 요청에 대한 웹 페이지를 보여주고, MemberService를 통해서 회원가입이나 회원 조회에 관한 정보를 가져오게 됩니다. 이를 "MemberController는 MemberService와 의존관계에 있다."라고 표현합니다.
스프링에서 말하는 의존관계란, 예시의 MemberController 와 MemberService처럼 객체지향의 세계에서 하나의 객체가 행하는 행위(메소드)를 위해 다른 객체를 내부에 갖고 있는 관계를 의미합니다. 좀 더 쉬운 예로 설명해보겠습니다. 예를 들어, 물을 마시는 행위에 대해 실세계에서 "내" 가 "물"을 마시면, 물의 양이 줄어들기 때문에 물의 양을 줄이게 하는 주체는 "나"가 되지만, 객체지향의 세계에서는 "물" 이 갖고 있는 "마신다"라는 행위(메서드)를 통해 "물" 객체가 양을 줄이는 것으로 해석되어 주체는 "물" 이 됩니다.
결과적으로, 만약 "나" 라는 객체에 "물" 객체가 없다면, "마신다"는 행위를 할 수 없게 되므로, "나" 객체 안에는 "물" 객체가 필요한 것입니다. 이러한 이유로 MemberController에 아래와 같이 MemberService 객체를 추가해 줍니다.
[Java Code - MemberController.java]
...
@Controller
public class MemberController {
private final MemberService memberService = new MemberService();
}
하지만, 이럴 경우 MemberService 가 필요한 서비스마다 객체를 생성해줘야 하는 번거로움이 있고, 굳이 여러 개의 객체로 만들기 보다는 스프링 컨테이너에 객체로 한 번 등록하고, 이후 필요할 때마다 찾아서 사용하는 것이 훨씬 효율적입니다. 이를 위해 코드를 아래와 같이 MemberController의 생성자를 만들고, 그 안에 MemberService 객체를 할당받도록 추가해 줍니다.
[Java Code - MemberController.java]
...
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
위와 같이 생성자 안에 MemberService를 할당받도록 함으로써, 이후 MemberService를 가지고 사용하는 회원가입이나 회원조회와 같은 기능을 MemberController에서도 사용할 수 있게 되었다는 점을 기억하기 바랍니다. 그리고 해당 클래스를 스프링 컨테이너에 등록하는 과정이 필요한데, 이를 수행하는 것이 위의 코드에 있는 @Autowired 어노테이션입니다. 자동으로 MemberService 클래스와 같이 생성자를 생성하기 위해 필요한 클래스(정확히는 스프링 빈 객체입니다.)를 당겨와 연결시켜 준다로 이해하시면 됩니다.
2. 스프링 빈 생성하기
2.1 스프링 빈 자동으로 연결하기 : @Autowired
위와 같이 변경을 하고, 한 번 실행을 시켜보겠습니다. 우리가 원하는 클래스를 가져와 MemberController에 생성되었으니 정상 실행이 될 것으로 보이지만, 아래 그림과 같이 실패하게 될 것입니다.
에러 메시지를 보면 MemberService와 같은 스프링 빈 객체가 필요한데 찾을 수 없다는 내용입니다. 이유가 뭘까요? 정답은 에러에 있는 것처럼 "MemberService와 같은 스프링 빈 객체가 없다"는 것입니다.
무슨 말이냐면, 현재 우리가 실행한 스프링 컨테이너에 MemberService가 등록되지 않았다는 것입니다. 하지만, MemberController는 정상적으로 등록이 되었는데, 코드를 조금만 유심히 보게 되면, 둘의 명확한 차이가 있습니다. 바로 클래스 시작 부분에 추가한 어노테이션의 유무입니다. MemberController나 이전에 다룬 HelloController의 클래스 시작 부분을 보게 되면, @Controller라는 어노테이션이 추가되어 있지만, MemberService의 클래스 시작 부분에는 어노테이션이 추가되어있지 않습니다.
여기서 @Controller 어노테이션과 같은 종류의 어노테이션을 "컴포넌트"라고 부르는데, 이 컴포넌트 어노테이션이 있으면 스프링 컨테이너가 생성되면서 자동으로 관리를 위한 등록이 진행됩니다. 그리고 이러한 컴포넌트를 하나의 클래스에서, 한 번만 사용하는 것이 아니라 스프링 컨테이너가 종료되기 전까지 재사용 가능하도록 관리되는데, 이를 가리켜 스프링 빈(Bean)이라고 표현하는 것입니다. 방금 전에 언급한 @Controller를 포함해, @Component, @Service, @Repository 어노테이션이 해당합니다.
위의 실행결과에서 스프링 빈 객체를 찾을 수 없다는 것은 결국 해당 클래스명으로 스프링 컨테이너에 스프링 빈 객체로 관리되는 컴포넌트가 없다는 의미이며, 현재의 MemberService 클래스는 스프링이 관리하지 않는, 그저 순수한 자바 클래스일 뿐입니다. 이를 스프링 빈 객체로 만들어 주기 위해서는 MemberService에 컴포넌트 어노테이션을 추가하면 해결됩니다. 해당 클래스는 비즈니스 로직이 포함된 "서비스"에 관한 것이므로, @Service 어노테이션을 추가해 주겠습니다.
[Java Code - MemberService.java]
package com.example.hellospring.service;
import com.example.hellospring.domain.Member;
import com.example.hellospring.repository.MemberRepository;
import com.example.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) {
this.memberRepository = memberRepository;
}
....
}
그리고 마찬가지로 MemberService의 생성자에서도 MemberRepository에 대해 @Repository 어노테이션을 추가해야 하는데, MemberRepository 클래스는 인터페이스입니다. 때문에 해당 인터페이스를 상속받은 구현체인 MemoryMemberRepository에 @Repository 어노테이션을 추가해야 합니다.
[Java Code - MemoryMemberRepository.java]
package com.example.hellospring.repository;
import com.example.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;
....
}
위와 같이 변경하고 실행하면, 정상적으로 서비스가 실행되는 것까지 확인할 수 있습니다. 위의 구조를 그림으로 표현하면 다음과 같습니다.
위의 그림에서처럼 MemberController를 사용하기 위해서는 MemberService의 객체가 필요하고, MemberService를 사용하기 위해서는 MemberRepository의 객체가 필요한 것처럼, 해당 클래스의 외부에서 의존적으로 필요한 클래스의 객체를 주입하는 것이 의존성 주입 (Dependency Injection)입니다. 그리고 위의 그림처럼, 처음에 스프링 컨테이너가 생성되고, 관리할 대상을 찾을 때 @Component 어노테이션이 포함된 컴포넌트들을 찾는데, 이를 가리켜, "컴포넌트 스캔 방식"이라고 합니다.
컴포넌트 스캔 방식이 실행되는 범위는 우리가 서비스 실행을 위해 눌렀던, 웹 애플리케이션 메인함수가 위치한 패키지 이하의 모든 컴포넌트를 관리하게 되며, 패키지 외에 다른 패키지의 컴포넌트들은 자동으로 관리 대상에 포함되지 않습니다. 그렇다고, 다른 패키지의 컴포넌트를 사용할 방법이 없는 것은 아니지만, 이번 장에서는 기본적인 내용을 위주로 설명드릴 것이기에 넘어가도록 하겠습니다.
끝으로 위와 같이 스프링에서 스프링 컨테이너에 스프링 빈을 등록할 때는 기본적으로 싱글톤 패턴을 사용하여 등록합니다. 유일하게 하나만 등록해서 공유하기 때문에, 같은 스프링 빈이면 모두 같은 인스턴스입니다. 물론 설정으로 싱글톤 이외의 방식으로 바꿀 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤 패턴을 사용한다는 점도 참고하시면 됩니다.
2.2 코드로 직접 스프링 빈 등록하기
이번에는 @Autowired 어노테이션 없이, 자바 코드로 직접 스프링 빈으로 등록하는 방법을 알아보도록 하겠습니다. 이를 위해 앞선 예시에서 @Service, @Repository 어노테이션과 @Autowired 어노테이션을 제거한 후 진행해 보겠습니다. 제거를 한 후, 실행을 했을 때, 스프링 컨테이너에서 스프링 빈 객체를 찾을 수 없다는 에러문이 나오면 됩니다.
다음으로 SpringApplication 이 위치한 경로에 SpringConfig라는 클래스를 생성해 줍니다. 이후 코드에 대해서는 먼저 작성을 하고, 설명을 이어가겠습니다.
[Java Code - SpringConfig.java]
package com.example.hellospring;
import com.example.hellospring.repository.MemberRepository;
import com.example.hellospring.repository.MemoryMemberRepository;
import com.example.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();
}
}
위의 코드를 보게 되면, 가장 먼저 해당 파일이 스프링 컨테이너가 참고할 설정파일임을 명시하기 위해서 @Configuration 어노테이션을 사용했습니다. 해당 어노테이션을 사용함으로써, 이후, 스프링 컨테이너가 실행될 때, 참고하는 파일로 설정이 되며, 파일 내에 선언된 객체를 관리대상으로 추가하게 됩니다.
다음으로는 스프링 빈 객체를 생성해야 하는데, 앞선 예제와 유사하게 MemberController에서 사용되는 MemberService를 먼저 스프링 빈으로 등록해줘야 합니다. 이를 위해서 @Bean 어노테이션과 함께 MemberService에 대한 생성자를 만들어주고, 반환하는 값으로는 객체를 반환하도록 합니다.
그리고 생성자에는 우리가 MemberRepository 정보를 넣어야만 MemberService의 생성자를 사용할 수 있기 때문에 다음줄에 MemberRepository에 대한 생성자 및 스프링 빈으로 등록하는 과정을 넣어준 것입니다. 물론 MemberRepository는 인터페이스이기 때문에 반환할 때는 구현체인 MemoryMemberRepository를 사용해야겠죠?
이렇게 해서 MemberService와 MemberRepository를 모두 스프링 빈으로 등록했고, 실행해 보면 앞서 본 의존관계 그림을 만족하기 때문에 스프링 컨테이너에 등록되어 정상적으로 실행될 것입니다.
이렇게 해서 자바코드로 직접 스프링 빈을 만들어보는 것까지 해보았습니다. 과거에는 XML을 사용해서 스프링 빈 객체를 등록하는 방법을 많이 사용했지만, 최근에는 위의 예시처럼 직접 자바 코드를 사용해서 스프링 빈 객체를 등록/관리하는 방식을 채택합니다. 이후 다른 예시들에서도 많이 활용될 예정이니 참고하면 좋을 것 같습니다.
다음으로 앞선 예시들에서는 모두 생성자를 이용해서 의존성을 주입하는 방법을 채택하였습니다. 사실 의존성 주입을 하는 방법은 생성자를 사용하는 것 이외에 필드나 Setter 메서드를 사용해서 주입하는 방법들이 있는데, 일반적으로 의존관계가 애플리케이션이 실행되는 중에 동적으로 변하는 경우가 없기 때문에 생성자를 이용한 방법을 사용하는 것을 권장합니다. 자세한 내용은 추후에 의존성 주입에 대해 다룰 때 설명드리도록 하겠습니다.
'BackEnd > Spring 🍃' 카테고리의 다른 글
[Spring] 5. 좋은 객체지향 프로그래밍이란 Ⅱ : 실습편 - 회원 도메인 (0) | 2025.03.12 |
---|---|
[Spring] 4. 좋은 객체지향 프로그래밍이란 Ⅰ : 이론편 (0) | 2025.02.03 |
[Spring] 2. 웹 개발 기초 (1) | 2024.12.31 |
[Spring] 1. 스프링을 시작하며... (0) | 2024.12.29 |