6. DI_ 자바코드로 직접 스프링 빈 등록하기
자바코드로 등록하기 위해서는 먼저 컴포넌트를 지우고, config파일을 만들어야 한다.
- MemberService에서 '@service', '@Autowired' 지우기
- MemoryMemberRepository에서 @Repository 지우기
- MemberController는 그대로 유지
- config파일 생성하기
Q. SpringConfig(config파일) 만들기
package hello.hellospring;
import hello.hellospring.Service.MemberService;
import hello.hellospring.repositoty.MemberRepository;
import hello.hellospring.repositoty.MemoryMemberRepository;
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();
}
}
▶@Bean
'스프링 빈 등록하기'라는 의미
▶코드 사고순서
- 스프링 창이 뜰 때, 스프링이 @Configuration을 읽고 MemberService() 비지니스로직을 등록하라는 지시를 받는다.
- memberService()로직을 호출을 해서 스프링 빈에 등록한다.
- 이때 "return new MemberServic();"에 넣어줄 값이 필요하다. //"MemberRepository"를 넣어 줘야한다.
- 그래서 MemberRepository를 등록해준다.
- "return new MemberService(memberRepository());"처럼 값을 넣어준다.
▶코드 동작순서
- 스프링 창이 뜰 때 '@Bean'표시를 보고, 스프링 빈에 'memberService()', 'memberRepository()'를 등록해준다.
- 스프링에 등록되어 있는 "return new MemberService(memberRepository())"의 memberRepository를 MemberService클래스에 있는 memberRepository에 넣어준다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
※MemberController 경우에는
(어쩔 수 없이) 스프링이 관리해주기 때문에 Component Scan으로 처리되기 때문에 @Autowired로 해주면 된다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
@Autowired로 해두면, memberService를 SpringConfig에 있는 memberService에 넣어주게 됩니다.
Q. 여기까지하면 '자바코드로 직접 스프링 빈에 등록하기'가 완성되는데....
역시 '컴포넌트 스캔과 자동 의존관계 설정' vs '자바코드로 직접 스프링 빈에 등록하기'를 비교해보자면?
당연히 '컴포넌트 스캔'으로 한 줄 추가해서 작성하는게 편합니다.
Q. 과거에는.....어떻게?
과거에는 xml로 문서로 작성했었는데 이거는 몰라도 됩니다.
XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않으므로 생략한다.
1. DI_ 주입방식 3가지
DI에는 3가지 방식이 있다.
- 생성자 주입
- 필드 주입
- setter 주입
Q. 생성자 주입
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
Generate to Construtor(생성자)를 만들어 의존관계를 설정합니다.
Q. 필드 주입
@Controller
public class MemberController {
@Autowired
private MemberService memberService;
}
- MemberController에서 constructor(생성자)를 지우고 필드에 '@Autowired'를 추가합니다.
- 이때 intelliJ경우에는 '@Autowired'에 물결표시로 'create constructor: MemberController(MemberService)' 메시지가 나옵니다.
- 확실히 "필드 주입 방식은 별로다" , 필드주입은 한번 선언하면 바꿀 수 없는 단점이 있다.
Q. setter 주입
@Controller
public class MemberController {
private MemberService memberService;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
- 'Q.setter주입' 방법을 기준으로 'Generate to setter'로 setter를 만들어 '@Autowired'를 넣어줍니다.
- 이렇게 하면 생성은 따로 생성되고, setter는 나중에 호출되어 들어옵니다.
- 단점으로, "누군가 memberController를 호출하면 public으로 열려있어야 한다." 이때 잘못 바꿔지면 문제가 생길 수도 있다.
Q. (예시: )setter는 public으로 열려있어서 "누군가가 바꿀 수 있다"는 위험성이 있다.
package hello.hellospring.Service;
import hello.hellospring.domain.Member;
import hello.hellospring.repositoty.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Optional;
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(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);
}
}
@Controller
public class MemberController {
private MemberService memberService;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
memberService.setMemberRepository(); //이렇게 문제가 발생!
}
}
- 아무 개발자가 "마음대로 호출할 수 있게 열려있다는 문제"
- 개발은 최대한 호출되지 말아야 하는 메소드는 호출되면 안된다.
- 조립시점에 생성자를 딱 한번 호출해서 조립해놓고 더 이상 변경하도록 막아야 한다.
Q. 그렇다면 권장하는 스타일은?
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 MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
생성자를 통해서 주입을 하면 처음에 '애플리케이션이 조립된다'로 표현하는데
스프링 컨테이너에 올라가고 세팅되는 시점에 memberService가 들어오고 끝납니다.
2. 참고사항
- 실행 중에 동적으로 변하는 경우(:runtime을 서버가 떠있는데 중간게 바꿔치기 된다.)에는 그럴 경우는 거의 없지만config파일을 수정해 서버에 올려야 한다.
- 실무에서 주로 정형화된 컨트롤러, 서비스, 리포지토리같은 코드는 컴포넌트 스캔을 사용한다.
- 정현화되지 않거나 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈에 등록한다.
위의 그림처럼 MemberRepository를 만들 때, "비지니스 요구사항 중, 아직 데이터 저장소가 선정되지 않은 시나리오"라고 정했었다. 나중에 메모리를 만들어 교체하기 위함이였다.
그림 설명을 다시 하자면,
- interface를 설계해 구현체로 MemoryMemberRepository를 쓰는 그림을 만든다.
- "나중에 MemoryMemberRepository를 다른 Repository로 바꿔치기 할 수 있게 의도한 것"
- 이때, (interface로 설계했기 때문에) 기존 운영코드를 수정없이 다른 Repository로 바꿀 수 있다.
다시 집어보자면,
▶컴포넌트 스캔을 사용
실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.
▶자바코드로 직접 등록하기
그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
Q. 정리
컴포넌트 스캔를 사용하면 여러 코드를 바꾸는 작업을 해야 하지만
자바코드로 직접 운영하게 되면, '구현체(설정파일)'만 바꾸면 된다.
@Bean
public MemberRepository memberRepository() {
return new DbMemberRepository();
}
위의 코드처럼 구현체(MemoryMemberRepository() → DbMemberRepository())만 바꿔서
기존 운영코드를 수정없이 다른 Repository로 바꿀 수 있다.
Q.주의 "스프링 빈에 등록되어야 @Autowired가 된다."
@Autowired 를 통한 DI는
helloConroller , memberService 등과 같이 스프링이 관리하는 객체 에서만 동작한다.
스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
마무리
스프링 컨테이너, DI 관련된 자세한 내용은 스프링 핵심 원리 강의(인프런)에서 설명한다고 합니다.
사실 DI관련 내용은 너무나 깊은 내용이며 해당 포스팅은 맛보기수준입니다.