1. 비지니스 요구사항 정리
간단한 회원관리 예제를 만든다. 회원등록과 조회 기능을 만든다. 데이터 저장소는 선정되지 않았다는 가정하에 메모리에 데이터를 저장하고 조회하는 방식으로 만든다.
2. 회원 도메인과 리포지토리 만들기
인텔리제이로 하고 있는데 VSCODE랑 이클립스를 주로 써서 단축키를 항상 까먹게 된다. 주요 단축키는 메모!
- Getter & Setter : Alt + Insert
- import 정리 : Alt + Shift + O
- implements Methods : Ctrl + I
- static import : static import할 메소드에 커서 있는 상태에서 Alt + Enter
- 변수 추출하기 : Ctrl + Alt + V
- 메서드 추출하기 : Ctrl + Alt + M
- 테스트 만들기 단축키 : 테스트 만들 클래스에서 Ctrl + Shift + t
Repository를 만들 때 임시적으로 메모리 저장 방식을 사용한다.
MemberRepository를 구현한 MemoryMemberRepository를 만든다. static한 HashMap 인스턴스를 생성하여 임시저장소를 만든다.
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) {
//null이 반환될 수 있으면 optional로 감싸주면 클라이언트에서 처리할 수 있다
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());
}
3. 회원 리포지토리 테스트 케이스 작성
테스트 케이스를 작성한다. 테스트 케이스 작성시 테스트할 데이터를 만들고, 실제로 테스트할 메소드를 호출한 후 assertThat이나 Assertions.assertEquals()를 사용하여 검증한다.
테스트끼리는 순서에 의존적이면 안 된다. 다음의 경우는 순서에 의존적이게 되어 테스트가 실패한 케이스다.
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get(); //get으로 꺼내는 방법은 좋은 방법은 아님, 테스트용
assertThat(member).isEqualTo(member);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
위의 코드는 findByName()이 실패했다. 순서를 보면 findAll()부터 테스트가 작동한 것을 확인할 수 있다. 이 때 name이 'spring1'인 member1이 참조하는 Member인스턴스가 만들어졌다.
이후 findByName() 테스트 메소드에서도 'spring1' name이 저장된 member1 객체를 만들어 save하여 중복이 발생한다. 둘의 sequence ID는 다르다. hashMap은 value 중복 저장이 가능하기 때문에 name으로 검색하면 원치 않는 결과를 가져올 것이기 때문에 정확한 테스트가 되지 않는 것이다. 순서 의존도를 없애기 위해 clear를 해줘야 한다.
//테스트가 끝날때마다 repository가 깔끔하게 지워지는 코드
@AfterEach
public void afterEach() {
repository.clearStore();
}
실제 MemoryMemberRepository에 메소드 clearStore()를 만든 후, @AfterEach 어노테이션을 적용한 메소드를 만든다. @AfterEach를 붙이면 각 테스트 실행 후 콜백하는 메소드 함수를 만들 수 있다. 이후 테스트 하면 순서 의존도 없이 테스트가 성공하는 것을 확인할 수 있다.
4. 회원 서비스 개발
리포지토리 테스트가 끝난 후, 실제 서비스단을 만든다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원가입
*/
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 IllegalStateException("이미 존재하는 회원입니다");
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
memberRepository.findAll();
}
/**
* 아이디로 회원 조회
*/
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
5. 회원 서비스 테스트
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
// 테스트시에는 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());
}
@Test
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("이미 존재하는 회원입니다");
/* try {
memberService.join(member2);
fail();
} catch (IllegalAccessError e){
}
*/
//then
}
6. 컴포넌트 스캔과 자동 의존관계 설정
MemberController위에 @Controller를 붙여 스프링컨테이너 관리 빈으로 만든다. 그리고 다음과 같이 @Autowired 어노테이션을 붙여 초기화한다.
@Autowired : 스프링 빈으로 관리되고 있는 객체를 DI(의존성주입) 해줌
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
이렇게 @controller만 붙이면 에러가 발생한다.
MemberService를 빈타입으로 정의해라 라고 해석할 수 있는데 말그대로 스프링컨테이너가 관리하는 빈으로 설정하라는 의미이다. MemberService에도 @Controller와 같은 역할을 하는 어노테이션을 붙여주는 작업이 필요하다.
서비스에는 @Service 어노테이션을 붙여준다. Repository 구현체에는 @Repository 어노테이션을 붙여주면 스프링컨테이너가 관리하는 빈이 된다. 그러면 Controller에서 @Autowired를 통해 의존할 수 있다.
@Service, @Controller 등은 모두 내부에 @Component 어노테이션을 갖고 있는데, 이 어노테이션이 붙은 애들만 스프링 빈으로 관리된다. 이런식으로 컴포넌트들을 스캔하는 방식을 컴포넌트 스캔 방식이라고 한다.
추가적으로, hello.hellospring 밖에 패키지를 만들고 @Component어노테이션을 붙인 클래스 파일을 만들면 컴포넌트 스캔이 될까? 기본적으로 안 된다. 다음 화면과 같이, HelloSpringApplication이라는 구동클래스를 포함하는 패키지 안에서만 스캔한다.
7. 자바 코드로 직접 스프링 빈 등록하기
컴포넌트 스캔 방식 이외에도 자바설정파일을 만들어서 빈 등록이 가능하다. 기존 @Service, @Repository, @Autowired를 제거한 후, 다음과 같은 자바 설정파일을 만든다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
SpringConfig 파일의 디렉토리가 어디까지 스프링컨테이너 설정정보로 이용될 수 있는지 궁금해서 이곳저곳에 만들며 테스트해봤다. hello.hellospring.config 패키지 하위에 두어도 인식이 가능했다.
반면 hell.hellospring을 벗어난 패키지(java/test/SpringConfig.java 등)로 설정하면 인식하지 못한다. 항상 HelloSpringApplication이 속한 패키지 하위에 설정정보 파일을 넣어둬야 한다.
그리고 DI주입 방식에는 3가지가 있다. (setter방식, 필드방식, 생성자방식)
//1.setter방식
@Autowired
public setMemberRepository(MemoryRepository memoryRepository) {
this.memoryRepository = memoryRepository
}
//2.필드방식
@Autowired
public void MemberRepository memberRepository;
//3.생성자방식(추천)
@Autowired
public MemberService memberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository
}
일반적으로 컴포넌트 스캔은 정형화된 MVC패턴 파일들을 빈등록할 때 사용한다. 반면 정형화되지 않거나 상황에 따라 구현 클래스 변경 필요한 경우 자바설정을 통해 스프링 빈으로 등록한다.(MemoryMemberRepository를 DBMemberRepository로 바꿔야할 경우)
'👩🏫 Study > 스프링부트 강의' 카테고리의 다른 글
[2. 스프링 핵심 원리] 객체 지향 설계와 스프링 (0) | 2023.10.03 |
---|---|
[1. 스프링 입문] AOP (0) | 2023.10.03 |
[1. 스프링 입문] 웹 MVC 개발 (0) | 2023.10.02 |
[1. 스프링 입문] 스프링 웹 개발 기초 (0) | 2023.10.01 |
김영한의 스프링 완전 정복 수강 시작 (0) | 2023.10.01 |