5. DI_ 컴포넌트 스캔과 자동 의존관계 설정
복습
우선 지금까지 '회원 관리 예제(프로젝트)'를 만들었습니다.
- 비지니스 요구사항 정리
- 회원 도메인과 리포지토리 만들기
- 회원 리포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
※짧은 예습
DI의 조건은?
방법_컴포넌트 스캔과 자동 의존관계 설정
방법_자바코드로 직접 스프링 빈 등록하기
이번 시간에는 실제 '화면'을 붙여보고자 합니다.
(화면: 쉽게 말해서 html로 브라우저화면 만들기)
▶화면 만드는 과정에서 필요한 사항
- 컨트롤러 만들기
- 템플릿 만들기
1. 컨트롤러
그 과정을 위해서는 우선 멤버 컨트롤러를 만들어야 합니다.
▶멤버 컨트롤러가 해주는 일
- 멤버서비스를 통해 회원가입을 한다.
- 멤버서비스로 회원조회를 할 수 있어야 합니다.
Q1. MemberController 만들기
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
}
바로 생성한 상태에서 @Controller를 추가합니다.
Q. @Controller가 뭐예요?
쉽게 말하면, @Controller를 적어둔 객체는 스프링이 관리대상으로 인식해서 관리해줍니다.
위의 코드 기준으로 설명하자면,
지금 코드는 기능은 없지만, 스프링 컨테이너가 스프링 창에 뜰 때, 스프링 컨테이너랑 스프링 통이 생깁니다.
거기에 @Controller가 있으면 MemberController 객체를 생성해서 스프링에 넣어두면 스프링이 그 객체를 관리해줍니다.
Q. 예시_ 컨트롤러를 사용했던 HelloController
@ Controller를 붙여 작성해서 만든 화면
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model){
model.addAttribute("data", "Spring!!!");
return "hello";
}
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
}
앞의 설명처럼 @Controller가 있다면,
스프링 컨테이너가 스프링 창에 뜰 때 MemberController 객체를 생성해서 들고 있게 됩니다.
이를 "스프링 컨테이너에서 스프링 빈이 관리된다"라고 표현합니다.
스프링 부트 내부를 보면, '내장 톰캣서버', '스프링 컨테이너'가 있습니다.
그리고 스프링 컨테이너 안에 'helloController', 'viewResolver'가 있는데 이를 'spring bin(일명 초록콩같은)'이 있습니다.
▶@Controller 원리는
스프링 컨테이너가 스프링 창에 뜰 때 스프링이 MemberController 객체를 생성해서 들고 있게 됩니다.
그러니까, "@Controller" 표시가 있다면 스프링이 관리해주기 때문에 스프링과 관련된 기능(예:'helloController')이 동작하게 됩니다.
다시 'Q1. MemberController 만들기'로 돌아와서
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
private final MemberService memberService = new MemberService();
}
이제 MemberService를 가져다 써야 하는데, 이렇게 new로 생성한다면?
다시 집어보자면, @Controller 표시를 한다면
- 스프링의 관리대상으로 지정된다.
- 그러면 사용하고 싶은 자원은 모두 컨테이너에 등록하고 받아쓰는 식으로 해야 한다.
그렇게 하면, 다른 컨트롤러들이 MemberService를 가져다 쓸 수 있는 공용자원이 되어버릴 수 있다.
이때 MemberService에 들어가 보면, "공용으로 했을 때 별 문제가 없고 별 기능이 없다면?"
▶그렇다면 스프링에 등록하고 공용으로 쓰면된다.
Q2. 어떻게 공용으로 사용하냐?
- new생성자 지우기
- constructor(생성자) 만들기
- @Autowired추가
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
▶@Autowired
스프링창이 뜰 때 MemberController객체를 생성해서 스프링이 가지고 있는데
"이때 생성자(construtor)를 호출하고, 또 @Autowired로 되어 있다면?"
스프링이 스프링 컨테이너에 있는 멤버컨트롤러에서 멤버서비스를 꺼내 연결해준다.
Q2'. 그래서 이 상태에서 실행시킨다면???
Q. 그런데 왜 연결이 안되었을까?
"@Autowired라고 하면 스프링이 컨테이너에서 MemberService를 가져온다고 했는데....?"
"HelloController 경우에는 스프링이 딱 뜰 때, 스프링 컨테이너에 등록이 되어서 잘 동작했거든요..."
Q. 여기서 HelloController는 조금 다르다.
- HelloController는 @Controller가 있어서 자동 등록되었지만
- MemberService는 '그냥 순수한 자바클래스이다.'
MemberService클래스에 들어가 보면, "그냥 순수한 자바 클래스"이다.
스프링은 MemberService를 보고 "@(어노테이션)" 표시도 없고 알 수 있는 방법이 없어서 관리를 못한다.
그러면, 어떻게 '스프링 빈'을 등록하여 @Autowired해서 연결에 성공할 수 있을까?
2. 스프링 빈을 등록하는 2가지 방법
스프링 빈을 등록하는 방법은 다음과 같다.
- 컴포넌트 스캔과 자동 의존관계 설정
- 자바 코드로 직접 스프링 빈 등록하기
Q1. 방법> 먼저 컴포넌트 스캔과 자동 의존관계 설정
@Component(컨포넌트)란?
스프링이 올라올 때, @Component가 있다면 객체를 생성하여 스프링 컨테이너에 등록한다.
(※ @Autowired는 "연결관계"이다.)
특징
- @Component가 있다면, 스프링 빈으로 자동 등록된다.
- @Controller가 있으면 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다. (Controller는 Component속성이다.)
@Component를 포함하는 다음 애노테이션을 추가하면 스프링이 등록된다.
(@Controller, @Service,@Repository에 들어가 보면,
@Component가 있는데 그 이유는 Component에 포함되는 속성이기 때문이다.)
- @Controller
- @Service
- @Repository
이렇게 만드는 것을 "정형화된 패턴"이라고 한다.
- controller를 통해서 외부요청을 받고
- Service에서 service(비지니스로직)을 만들고,
- Repository에서 데이터 저장을 한다.
※주의사항
1. (싱글톤: 1개만 등록하고 그 하나를 공유한다.)
- 스프링 컨테이너에서 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다.
- 싱글톤으로 등록했기 때문에, 같은 스프링 빈이면 모두 같은 인스턴스이다.
- 설정으로 싱글톤이 아니게 바꿀 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
2. "아무대나 @Component가 있어도 되는지?"
아니요!
이번 실습기준으로는 "hello.hellospring'을 포함한 하위패키지여야 가능하다.
HellospringApplication에 @springBootApplication이 있는데 열어보면 안에 '@Component Scan'이 있다.
그러니까 Component시작점인 곳과 연결된 하위패키지에서 만족하는 조건이기 때문에
꼭 해당 하위패키지에서 사용해야 한다.
1. MemberController에 @Controller를 추가한다.
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;
}
}
2. MemberService에 '@Service' '@Autowired'를 추가한다.
package hello.hellospring.Service;
import hello.hellospring.domain.Member;
import hello.hellospring.repositoty.MemberRepository;
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;
}
/*
회원가입
*/
public Long join(Member member) {
//비지니스 조건: 같은 이름의 회원 x : (중복회원x)
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(member1 -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
전체회원조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
3. MemoryMemberRepository에 @Repository를 추가한다.
package hello.hellospring.repositoty;
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<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
1. helloController
helloController는 스프링이 제공하는 컨트롤러여서 스프링 빈으로 자동 등록된다.
2. helloController → memberService
@Autowired로 생성자를 작성하면 MemberController가 생성될 때
스프링 빈에 등록되어 있는 memberService를 객체로 가져다가 스프링 컨테이너에 넣는다.(이 부분이 DI스럽다.)
3. memberService → memberRepositor
memberService에도 '@service, @Autowird'를 추가
과정>
- @Autowired ('생성자 호출')을 하여 'memberRepository'를 필요하다고 인식한다.
- 스프링 컨테이너에 memberRepository를 넣어준다.
- 이때 구현체는 MemberMemoryRepository이므로 이 구현체를 service에 넣어준다.(이 부분이 DI스럽다.)
마무리
너무 길어져서 'DI조건_자바코드로 직접 스프링 빈 등록하기' 편은 다음 포스팅으로 나누겠습니다.