내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
섹션 3. 회원 관리 예제
이번시간에는 MVC패턴을 활용하여 간단한 예제코드를 구현해 보면서 감을 익히는 시간이였다.
1. 비지니스 요구사항 정리
간단하게 회원ID, 이름을 저장한다. 기능은 회원 등록, 조회 뿐이다.
또한 데이터 저장소가 아직 선택되지 않았기 때문에 interface를 중심으로 설계 해야한다.
인터페이스로 구현해야 나중에 DB를 변경할때 쉬워진다.
2. 회원 도메인과 리포지토리 만들기
이번시간에는 도메인과 관련된 클래스와 인터페이스를 구현하였다.
우선 회원 정보에 해당하는 클래스부터 확인해 보자.
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;
}
}
간단하게 id값과 name을 저장하는 클래스 이다. id값은 서버에서 회원을 인식할수 있도록 부여하는 ID 이다. 가입할때 사용하는 ID가 아니다! 이후 간단한 setter, getter를 설정해 주었다.
인터페이스는 다음과 같이 구현하였다.
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();
}
저장, ID로 찾기, Name으로 찾기, 모두 찾기 로 4개의 기능을 제공할것을 약속하는 인터페이스 이다.
이 인터페이스를 구현한 구현체는 다음과 같다.
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 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());
}
}
구현채를 만들때 사용하는 문법들이 조금 어색했다. 문법 자체는 공부한적이 있어 이해는 가는데... 사용을 거의 안해봤었던 지라... stream이랑 람다식이 조금은 어색했다.
또한 Optional같은 경우 생각나지 않았다... 자바의 정석에서 봤었는데... 영한님의 설명을 듣고나니 Null을 반환할때의 문제를 막기위한 wrapper class같은 역할을 하는것이 생각났다.
하나의 캡슐화를 하는 느낌이다.
위의 메소드들 중 다음 메소드를 살펴보자.
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
반환값으로 Optional을 반환하하고 있다.
메소드 몸체에서는 store 라는 Map에서 values()로 리스트를 반환받은 후, 이를 stream으로 바꾼다.
바뀐 stream에서 필터링을 통하여 람다식 ( member -> member.getName().equals(name) ) 애 해당되는 아이템을 찾는다.
.findAny() 이기 때문에 만족하는 어떤것 이라도 발견하면 바로 반환한다.
3. 테스트 케이스 작성
나같은 경우 처음으로 테스트 케이스를 작성해보는 시간이였다. 하나의 검증과정을 배운다는 저에서 즐거움이 엄청났다!
우선 소스코드를 살펴봅시다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member res = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(res);
}
@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 res = repository.findByName("spring1").get();
assertThat(res).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> res = repository.findAll();
assertThat(res.size()).isEqualTo(2);
}
}
기존에 코드를 작성하던 main폴더가 아닌! test라는 폴더에 페키지를 만들어 주었다.
전통적으로 클래스의 이름은 테스트 해볼 클래스 이름 뒤에 Test를 붙인다고 하셨다. 따라서 MemoryMemberRepositoryTest 가 되었다.
기본적 사용법은 간단했다. 테스트해볼 메소드 이름위에 @Test를 선언해주면 됬다.
하나의 메인메소드를 작성한다 생각하면서 사용하니 편하게 다가왔다.
위의 코드를 보면 초반부에 AfterEach 를 볼수있다. 이는 모든 테스트 케이스 마다 적용되도록 하는 에노테이션 이다.
이름이 AfterEach인 만큼 테스트 함수가 종료되고 나서 사용한 자원들을 clean-up을 하기위해 사용하였다.
@AfterEach
public void afterEach(){
repository.clearStore();
}
이런 클린업 과정이 왜 필요했을까?
위의 테스트 코드에서는 클레스의 멤버로 repository라는 인스턴스를 만들었기 때문에 모든테스트 케이스에서 자원을 공유하는 문제가 생긴다. 각 테스트 끼리 영향을 미치지 않게 하기위해 테스트끝에 자원을 청소하도록 하는것 이다.
4. 회원 서비스 개발
이번에는 Service 코드를 구현하는 시간이였다. 위에서 Repository를 구현할때는 메소드 이름들이 단순하게 동작을 나타내는 "동사" 였다면,
Service에서는 좀더 서비스적친화적인, 예를들면 findMembers 와 같은 제공할 서비스를 중심으로 메소드 이름을 만들라 하셨다.
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 = 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(){
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
위의 코드에서 인상깊었던 부분은 다음 코드이다.
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
memberRepository의 인터페이스에서 선언된 findByName()을 호출하면서 인자로 member의 이름을 넘겨준다.
여기까지 반환된 것이 Optional<Member>이다. 즉 다음과 같다.
Optional<Member> res = memberRepository.findByName(member.getName());
res에는 Optional이 담기게 된다. 하지만 우리의 코드는 이부분을 건너뛰고 바로 결과값인 Optional에 ifPresent()를 호출한다.
또한 ifPresent()안에 인자로 람다식을 넘겨준 점이 아주 인상깊고 재미있는 부분이였다.
다음은 이와 관련된 백기선님이 작성하신 글인데 읽어보면 도움될것이다.
5. 회원 서비스 테스트
테스트 하고싶은 클래스를 바로 만드는 단축키는 ⇧ + ⌘ + t
◆ 테스트 코드 작성 tip
테스트를 진행할때 초급자 단계에서는 given. when, then 절을 주석으로 달고 진행해보라는 tip을 주셨다.
given : 주어진 데이터를 기반으로
when : 검증할 부분
then : 그 결과
(추가 내용은 BDD를 찾아보길!)
생각해보면 테스트를 (input을 주고, 언제, 조건에 부합했을때, 기대하는 결과) 와 같이 테스트를 진행하는것이 타당하다.
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
아! 테스트 코드에서는 테스트 메소드 이름을 한글로 작성하는것도 추천하셨다.
어짜피 배포되는 부분도 아니고, 외국인과 하는 일이 아니라면 한글로이름을 작성하여 효율적으로 의미를 전달하는 것 또한 도움이된다 하셨다.
◆ 테스트 검증 부분
중복된 id의 가입을 막는것이 목표이다.
해당 id가 가입이 정상적으로 되는지 검증하는것도 중요하지만, 이는 반쪽짜리 테스트이다.
중복된 id가 들어왔을때 예외를 던져주는지 또한 확인해야 검증을 완료했다고 할 수 있다.
@Test
public void 중복_회원_예외() throws Exception {
//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("이미 존재하는 회원입니다.");
}
위의 코드를 통해 배울점이 있엇다. 예외가 터졌는지 확인하는 방식에 대한 부분이다.
assertThrows(터져야 하는 예외, 람다식) 처럼 사용한다. 람다식 부분의 logic이 실행되는데, 첫번째 인자로 넘겨준 예외가 발생해야만 한다!
◆ 의존성 주입(Dependency Injection)
테스트 코드를 작성하던 도중 다음과 같은 부분이 있었다.
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
// memberService 내부
public class memberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
...
}
MemberService 클래스 내부에서 MemoryMemberRepository 인스턴스를 만들어 사용하는데 외부에서 따로 MemoryMemberRepository 를 만들어 사용하고 있다.
하나의 MemoryMemberRepository 를 이용해서 검증해야 하는데 2개의 MemoryMemberRepository 를 이용하고있는 것이 문제였다.
이를 해결하기위해 의존성 주입을 이용하였다. 변경된 memberService 코드를 살펴보자!
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
이후 각각의 테스트는 독립적으로 이루어져야 하기 때문에, 각 테스트가 실행되기전 실행될 코드를 작성해 준다.
이는 @BeforeEach 를 이용한다.
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); // 의존성 주입
}
매 테스트 케이스 마다 테스트 전에 MemoryMemberRepository를 만들고, MeberService에 인자로 넘겨서 MeberService생성자 실행시 외부에서 받아오도록 구현하였다.
그럼 최초에 문제점으로 생각된 2개의 MemoryMemberRepository를 사용한다는 점을 해결할 수 있게되었다!
'BackEnd > Spring' 카테고리의 다른 글
[Spring] AOP : Aspect Oriented Programming (0) | 2022.01.14 |
---|---|
[Spring] 스프링 DB 접근 기술 (0) | 2022.01.14 |
[Spring] 회원 관리 예제 - 웹 MVC 개발 (0) | 2022.01.14 |
[Spring] 스프링 빈과 의존관계 (0) | 2022.01.12 |
[Spring] 스프링 웹 개발 기초 (0) | 2022.01.12 |
댓글