스프링은 지정된 패키지 내에서 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙은 클래스를 자동으로 스캔하여, 해당 클래스를 스프링 컨텍스트에 빈(Bean)으로 등록한다.
직접만든 회원 컨트롤러에 의존관계를 추가해보도록 한다.
import java.util.List;
@Controller
public class MemberController {
private final MemberService memberService;
// spring container에 등록
// MemberController가 생성될 때 Spring Bean에 있는 MemberService 객체를 넣어준다.
// 이것이 DI다.(스프링이 넣어주는거다.)
@Autowired // 연결
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@Autowired는 스프링 프레임워크에서 의존성 주입(Dependency Injection)을 위해 사용하는 어노테이션으로, 필드, 생성자, 세터 메서드에 적용할 수 있다. 이 중 생성자 주입이 권장되며, 이는 객체 생성 시 모든 의존성이 주입되어 객체를 불변 상태로 유지하고, 필수 의존성을 보장하며, 순환 참조를 방지할 수 있기 때문이다. 또한, 생성자 주입은 의존성을 명시적으로 전달하므로 테스트 시 Mock 객체를 주입하기 용이하다. 이러한 이유로 생성자 주입은 코드의 안정성과 예측 가능성을 높이는 좋은 선택으로 간주된다.
@Controller가 붙은 MemberController 클래스는 스프링 컨테이너에 Bean으로 등록되며, 스프링은 이 클래스의 인스턴스를 하나만 생성하여 관리한다. 이를 싱글톤(Singleton) 스코프라고 하며, 빈에 등록되는 대부분의 객체는 기본적으로 싱글톤으로 사용된다.
MemberController의 생성자에 @Autowired를 적용하면, 생성자에서 필요한 MemberService 인스턴스를 스프링이 자동으로 주입한다. @Autowired는 스프링 컨테이너에서 MemberService 빈을 찾아 생성자에 전달하며, 이를 통해 의존성 주입이 이루어진다.
@Service
public class MemberService {
private final MemoryMemberRepository memberRepository;
@Autowired
public MemberService(MemoryMemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired가 MemberService를 주입하려면, MemberService도 스프링 컨테이너에 Bean으로 등록되어야 한다. 이를 위해, MemberService 클래스에 컴포넌트 스캔이 가능하도록 적절한 어노테이션(예: @Service)을 추가한다.
MemberService를 컨트롤러에 자동으로 의존 관계를 주입하려면, MemberService 자체도 생성자를 통해 리포지토리를 주입받아야 한다. 이 과정에서 @Autowired를 사용하여 스프링 컨테이너에서 리포지토리를 가져온다.
또한, 리포지토리가 스프링 Bean으로 등록되어야 하기 때문에, @Repository를 사용해 컴포넌트 스캔 대상으로 지정한다. 이를 통해, MemberController → MemberService → 리포지토리로 이어지는 의존 관계가 스프링에 의해 자동으로 설정된다.
@Repository
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;

Main실행 자바 파일과 같은 경로에 SpringConfig 클래스 파일을 생성한다.
package hello.hello_spring;
import hello.hello_spring.repository.MemoryMemberRepository;
import hello.hello_spring.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 MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
위와 같이 컴포넌트 스캔 방식을 사용하지 않고, 직접 Config 파일을 구성하여 Bean을 등록할 수도 있다. 이를 통해 개발자가 명시적으로 의존성을 설정할 수 있다.
하지만, @Controller 및 @Autowired는 컨트롤러 클래스에서 유지해야 하며, 컨트롤러는 컴포넌트 스캔 방식으로 등록된다. 이는 컨트롤러가 웹 요청을 처리하는 스프링 MVC의 특성과 역할을 유지하기 위함이다. 따라서, Config 파일을 사용한 의존성 주입은 서비스와 리포지토리와 같은 핵심 로직 구성 요소를 설정하는 데 주로 사용된다.
홈 컨트롤러를 만들어 baseurl에서의 기본 화면이 index.html이 아니도록 구성한다.
package hello.hello_spring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
이렇게 구성하고 template에 home.html을 적당히 잘 만들면,
이와 같이 설정한 환경에서 우리의 목적은 다음과 같다:
회원 가입 버튼 클릭
회원 가입 페이지 동작
데이터 처리 및 저장
이 과정을 통해 회원 데이터를 등록하는 기능이 구현된다.
//MemberForm
package hello.hello_spring.controller;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//////////////////////////////////
// MemberController
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
회원가입이 성공한다면 다시 홈 화면으로 돌아오고 회원 목록에 들어가면 우리는 리포지토리에 넣은 member들을 확인할 수 있다.
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
모델(Model)은 데이터를 HTML에서 사용하기 위해 전달하는 역할을 한다.
회원 데이터 조회
memberService.findMembers()를 호출하여 모든 회원을 조회하고, 결과를 List로 저장한다.모델에 데이터 추가
model.addAttribute("members", memberList)HTML 렌더링
memberList.html로 전달되며,타임리프를 활용해 HTML에서 전달된 리스트 데이터를 반복 출력하거나, 조건에 따라 동적으로 내용을 구성할 수 있다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th> </tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
${}로 하여금 모델에서 데이터를 뽑을 수 있다.
물론 현재 사용 중인 리포지토리는 메모리 기반 리포지토리이기 때문에, 스프링 애플리케이션 실행을 종료하면 모든 데이터가 삭제된다. 이러한 한계를 보완하기 위해 파일 저장소나 데이터베이스를 도입할 수 있다.
파일 저장소를 사용하면 데이터를 로컬 파일에 영구적으로 저장할 수 있으며, 데이터베이스를 사용하면 대량의 데이터를 효율적으로 관리하고 복잡한 쿼리를 통해 조회와 조작이 가능해진다. 이러한 변경은 기존 리포지토리 인터페이스를 유지하면서, 구현체만 교체하는 방식으로 유연하게 적용할 수 있다.