Spring 입문
회원 리포지토리, 서비스
controller
HelloController
GetMapping을 이용한 페이지 이동
http://localhost:8080/hello
RequsetParam 사용
http://localhost:8080/hello-mvc?name=sumi
return “hello-template”;
- resources폴더의 hello-template.html파일을 읽어오게된다
@Controller
public class HelloController {
@GetMapping("hello")
pulic String hello(Model model) {
model.addAttribute("data","hello");
return "hello";
}
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam(value="name",required = false) String name, Model model) {
model.addAttribute("name",name);
return "hello-template";
}
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
domain
Member
package hello.hellospring.domain;
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;
}
}
repository
MemberRepository
interface 생성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
MemoryMemberRepository
Optional 클래스
Optional<T>클래스는 ‘T’타입의 객체를 포장해주는 래퍼 클래스(Wrap class)이다.
따라서 Optional 인스턴스는 모든 타입의 참조변수를 저장할 수 있으며
Optional 객체를 사용해 예상치 못한 NullPointerException예외를 제공되는 메소드로 간단하게 회피할수 있다.
null값 으로 인해 발생하는 예외를 피하기 위해 사용
.get()을 이용해 Optional 객체에 저장된 값을 반환하여 사용
findAny()
findAny 는조건에 일치하는 요소 1개를 리턴합니다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
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 List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny()
}
public void clearStore() {
store.clear();
}
}
service
MemberService
회원 서비스가 메모리 회원 리포지토리를 직접 생하게 했던것을
-> DI가 가능하게 변경
private final MemberRepository memberRepository = new MemoryMemberRepository()->
private final MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
자바 8에 추가된 Optional이 제공하는 ifPresent를 사용해서 null을 확인하는 if 문을 줄일 수 있다.
memberRepository.findByName(member.getName()) 값이 존재하는 경우에 그 안에 있는 내용을 실행하는 메소드
사용 예제
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IlegalStateException("이미 존재하는 회원입니다. ");
})
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member) {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IlegalStateException("이미 존재하는 회원입니다. ");
})
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
Given - When - Then Pattern
Given - When - Then은 테스트 코드를 작성하는 표현방식이다
한글로 번역해보면 준비 - 실행 - 검증 이다.
예시
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
Given
테스트를 위해 준비를 하는 과정으로, 테스트에 사용하는 변수, 입력값 등을 정의한다.
//given
Member member = new Member();
member.setName("hello");
When
실제로 액션을 하는 테스트를 실행하는 과정이다.
하나의 메서드만 수행하는 것이 바람직하므로
일반적으로 “When”과정이 테스트 코드에서 가장 심플한 구간이 된다.
//when
Long saveId = memberService.join(member);
Then
마지막으로 테스트를 검증하는 과정이다.
예상한 값과 실제 실행을 통해서 나온 값을 검증하여 테스트가 잘 되었는지 확인한다.
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
회원 서비스 테스트
test/java/hello.hellospring/service/MemberServiceTest
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
//@BeforeEach : 각 테스트 전에 필요한 설정
@BeforeEach
public void beforeEach() {
//MemoryMemberRepository이나 MemberService를 다른 객체로 쓸 이유가 없다. 따라서 하나의 객체만 만들어서 사용하는 것이 좋다.
//각 테스트를 실행하기 전에 memberRepository를 만들고 MemberService에서 memberRepository을 이용해 memberService객체를 만들어 사용해, 같은 memory repository를 사용한다.
//이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); //test마다 clear가 필요하다.
}
//AtterEach : 각 테스트가 끝날 때 마다 리소스를 반환(종료)하는 용도로 사용 - 여기에선 clear
@AfterEach
public void afterEach() {
memberRepository.clearStore(); //clear
}
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
void 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("hello");
Member member2 = new Member();
member2.setName("hello");
//when
memberService.join(member1);
//1. 밑과 같은 방법 try/catch를 이용해 하는 방법도 있지만 애매하다.
// try {
// memberService.join(member2);
// fail();
// }catch ( IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.")
// }
//2. 따라서 다른 문법을 사용하자
// assertThrows(기대하는 error, 로직)
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2) );
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
스프링 빈과 의존 관계
위에 같이 만든 것들을 화면에 보이게 하고 싶은데 그럴려면 controller과 view templete이 필요하다.
그러기 위해서 member controller이 필요하다.
이 member controller은 member service를 통해서 join하고, 데이터를 조회할 수 있어야 한다. 이를 의존관계가 있다고 말한다.

위의 그림과 같이 controller 어노테이션이 있으면 스프링이 뜰때 스프링이 뜰때 생성을 해서 관리한다.
src/main/java/hello.hellospring/controller/MemberController
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 {
// new 를 사용해서 쓸수도 있지만, 스프링이 관리를 하게 되면 스프링 컨테이너로 다 등록하고 스프링 컨테이너로 받아서 쓰게 바꾸어야한다.
// private final MemberService memberService = new MemberService();
// 따라서 new를 사용하지 않고 스프링 컨테이너에 등록을 하고 사용한다! 스프링 컨테이너에 등록을 하면 딱 하나만 등록이 되기 때문
private final MemberService memberService;
// 생성자 생성, window 기준 alt + insert하면 Constructor을 자동으로 생성할 수 있다.
// @Autowired : 생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI라고 한다. (자동)
// 이전과 달리 스프링이 자동으로 연결해준다, 전에는 개발자가 직접 주입했음
// error : Consider defining a bean of type 'hello.hellospring.service.MemberService' in your configuration => MemberService가 순수한 자바 클래스여서 스프링이 인식하지 못한다.
// 따라서 @Service를 MemberServiced앞에 붙여주고, @Repository를 MemberRepository 앞에 붙여준다. => spring이 인식한다.
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
스프링 빈을 등록하는 2가지 방법
-
컴포넌트 스캔과 자동 의존관계 설정
-
자바 코드로 직접 스프링 빈 등록하기
컴포넌트 스캔 원리
@Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
@Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.
@Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.
- @Controller
- @Service
- @Repository