spring 입문 - MVC, 스프링빈 등록까지
정적 컨텐츠 - 파일을 고대로 클라이언트에게 전해주는것
MVC와 템플릿 엔진 - 서버에서 변형을 줘서 내려주는 방식
API - JSON이라는 데이터 포맷으로 클라이언트에게 보내주는 방식
ㅇ 정적 컨텐츠
스프링부트는 정적 컨텐츠 기능을 자동으로 제공함.
static 파일에 html파일을 넣으면 바로 전달이 됨.
ㅇ MVC와 템플릿 엔진
Model - 어플리케이션의 데이터를 나타냄. 비지니스 로직을 담담하고, 데이터의 상태를 저장하고 조작함. Controller로 부터 전달받은 요청 처리, 결과를 view로 전달. 데이터 조회,업데이트 같은 조작을 담당함. 객체로 표현됨.
Controller - 클라이언트로부터의 요청을 처리하고 그에 대한 응답을 반환함. Model과 View 사이의 중개자 역할을 함. 클라이언트의 요청에 따라 Model을 업데이트하거나, 적절한 View를 호출함.
View - 화면에 보여지는 부분을 나타냄. 사용자의 입력을 받아 Controller에 전달하는 역할도 수행함. HTML, JSP, Thymeleaf등으로 구현됨. Model의 데이터를 화면에 렌더링해 사용자에게 제공함.
ㅇ API
JSON 스타일의 객체를 반환한다.
ㅇ 회원 관리 예제 - 비지니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음
ㅁ member 도메인
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ㅁ MemberRepository
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
ㅁ MemoryMemberRepository
@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();
}
}
ㅇ 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행함. 이런 방법은 준비하고 실행하는데 오래걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있음. 자바는 JUnit 이라는 프레임워크로 테스트를 실행해 이런 문제를 해결함. 서비스는 서비스, 레포지토리는 레포지토리 별로 테스트를 작성할 수 있다.
mac os 에서 커맨드+시프트+t 누르면 저렇게 쉽게 빈 테스트케이스가 생긴다.
ㅁ MemberService
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member){
//같은 이름이 있는 중복 회원X
validateDuplicateMember(member); // 중복회원검증 //메서드 뽑는 단축기 커맨드 + t, extract Method
memberRepository.save(member);
return member.getId();
}
/**
* 중복회원검증
* @param member
*/
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers(){
return memberRepository.findAll();
}
/**
* 한명 회원 조회
*/
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
비지니스 로직인 서비스코드. 회원가입 로직인 join 메서드를 보면 중복회원이 없도록 예외처리를 해놨는데, 예외처리를 저렇게 validateDuplicateMember() 메서드로 따로 빼서 사용하는데 저렇게 하려면 cmd + t 를 누르면 메서드로 따로 빠진다. 정말 유용한 단축키다.
ㅁ MemberServiceTest
public class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void Join() {
//given : 상황이 주어질때
Member member = new Member();
member.setName("spring");
//when : 이걸 실행(검증)했을때
Long saveId = memberService.join(member);
//then : 결과는 이렇게 나와야함
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외(){
//given : 상황이 주어질때
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when : 이걸 실행(검증)했을때
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// memberService.join(member1);
// try{
// memberService.join(member2);
// fail("예외가 발생해야 합니다.");
// } catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
//then : 결과는 이렇게 나와야함
}
}
afterEach는 각 테스트가 한번씩 완료될때마다 부르는 메서드다. memberRepository에 clearStore() 메서드를 호출해서 테스트때 만든 객체들을 삭제해준다. 중복회원 예외에 when 부분에 두가지 방법으로 사용할 수 있다. try catch로 잡아도 되고 예외 객체를 만들어서 할수도 있다.
ㅇ spring bean 등록 (컴포넌트 스캔, 직접 스프링 빈 등록)
ㅁ 컴포넌트 스캔
생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어줌. 이렇게 객체 의존 관계를 외부에서 넣어주는것을 DI라고함. 멤버 컨트롤러가 멤버 서비스를 의존하게 된다.
@Controller 가 달린 빈 컨트롤러가 있으면 그 컨트롤러의 객체를 생성해서 스프링 빈에 넣어준다. 그리고 스프링이 관리를함.
MemberController에 private final MemberService memberService = new MemberService();
해서 쓸수도 있지만, 스프링이 관리하게되면 스프링 컨테이너에 등록하고 받아쓰도록 바꿔야함.
다양한 컨트롤러에서 memberService를 받아쓸것이다. 다시말해, 하나만 생성해서 공용으로 쓰면된다.
그렇게 하기 위해선 생성자 주입을 쓴다.
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
생성자를 만들고 @Autowired를 붙혀준다. 참고로 서비스에 @Service를 붙혀줘야함. 컨트롤러에는 @Controller, 리포지토리에는 @Repository를 붙혀준다.
이렇게하면 스프링이 스프링빈에 등록된 멤버서비스 객체를 가져다 넣어줌. 이게 의존성주입(DI). 이렇게 되면 아래와같이 된다.
위의 이 방식이 컴포넌트 스캔 방식이다. 참고로 스프링은 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록함. 다음으로 직접 자바코드로 스프링 빈 등록하는 방법을 알아보자.
ㅁ 자바 코드로 직접 스프링 빈 등록
컨트롤러에 @Autowired를 냅두고, 멤버서비스와 멤버리포지토리의 @Autowired를 지워보자.
그리고 SpringConfig를 하나 만들어보자.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
@Configuration을 붙혀주고, memberService와 memberRepository에 @Bean 을 붙혀 직접 스프링 빈으로 등록하는 방법이다.
당연히 컴포넌트 스캔하는 방법이 훨씬 편하다.