인프런 강의 김영한
회원관리예제 개발
9. 비즈니스 요구사항 정리
회원 도메인과 리포지토리 만들기
회원 리포지토리 테스트 케이스 작성
회원 서비스 개발
회원 서비스 테스트
비즈니스 요구사항 정리
가장 쉬운거로 할것이다. 단순한예제로 스프링 동작을 알아보는 것이기 때문이다.
데이터: 회원ID, 이름
기능: 회원 등록, 조회
아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
db선정이 안되어잇을때의 구조를 알아보는 것이다.
일반적인 웹 애플리케이션 계층 구조
1.컨트롤러: 웹 MVC의 컨트롤러 역할이다.
2.서비스: 핵심 비즈니스 로직을 구현한다.
3.리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리한다.
4.도메인: 비즈니스 도메인 객체이다.
예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리된다.
클래스 의존관계
아직 데이터 저장소가 선정되지 않아서
우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
10. 회원 도메인과 리포지토리 만들기
10.1 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;
}
}10.2 MemberRepository
회원저장하는 것
optional로 찾기 null이면 들어가게 해준다.
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}10.3 구현체
멤버는 맵으로 저장한다.
id는 시퀸스만들어서 알아서 커지는 것 만들것이다.
id를 세팅하고 store에 저장하기
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;
}
}findById로 찾는 메소드 store에 id를 주고 찾는다.
null이 올수도 잇는데 요즘에는 null이 나올수잇으면 Optional.ofNullable()로 감싼다.
클라이언트에서 무언가를 할 수 잇다고 한다.
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}findByName 이름을 찾는다. 스트림을 활용했는데 파라미터로 넘어오는 이름이 같으면 반환한다.
findAny();찾으면 반환하는데 없으면 null을 반환한다.
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}반환한 store의 밸류들 = 멤버들을 반환한다.
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}이게 동작하는지 안하는지는 테스트케이스로 하면된다.
실무에서는 테스트가 당연하다.
11. 회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나
웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다.
이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵다.
그리고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다.
자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다
평소적는 곳이아닌 src-main-test에 패키지를 만들어준다.
테스트할 클래스이름 + Test로 만드는 것이 관례이다.
MemoryMemberRepositoryTest로 만들어주자.
@Test어노테이션을 붙이면 테스트할 수 있다.
꺼낸 멤버랑 내가 주는 것이랑 같으면 성공한 것이다.
Assertions.assertEquals(member, result);
assertThat(result).isEqualTo(member);
비교할 수 있다.
순서가 마음대로 되는데 그래서 이전 객체가 나와버리게 된다.
테스트 끝나고나면 데이터를 클리어해줘야한다.
@AfterEach어노테이션을 붙이면 마지막으로 실행된다.
MemoryMemberRepository에 메소드를 추가해서 map을 지우는 것을 만들어서 클리어해준다.
public void clearStore() {
store.clear();
}
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spirng");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// Assertions.assertEquals(member, result);
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
// given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// when
Member result = repository.findByName("spring1").get();
// then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
// given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// when
List<Member> result = repository.findAll();
// then
assertThat(result.size()).isEqualTo(2);
}
}만들때마다 검증할 틀을 미리 만들어주고 되는지를 확인해야한다.
이런것을 테스트 주도개발이라고 TDD라고 한다.
테스트가 수백개면 테스트를 GRADLE을 활용해서 자동으로 돌아가게 해주면된다.
테스트 관해서 깊이 있게 공부하는게 필요하다.
-> 테스트가 자바 11로 낮추면 junit이 안된다.
자동으로 설치되는 junit5가 17이상에서만 되는 것같다.
일단 자바17로 진행해야할 듯하다.
12. 회원 서비스 개발
서비스는 실제 비즈니스 로직을 작성하는 곳이다.
Optional을 감싸면 null인 것들을 그냥 비교할 수 있다.
과거에는 if null이런식으로 햇엇다.
Optional을 바로 반환하는 것은 좋지 않다.
repository는 그냥 db에서 값을 가져오는 느낌이다.
service는 비즈니스 로직에 좀 더 복잡한 처리를 하게 된다.
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);
}
}13. 회원 서비스 테스트
test는 한글로 이름 적어도괜찮다.
빌드될때 들어가지 않기때문임.
테스트를 세가지로 구분해서 하는게 좋다.
1.given 주어진상황
2.when 실행햇을때
3.then 이게 되야한다.
이 주석을 넣는 것만으로도 도움이 된다.
13.1 회원가입 테스트
@Test
void join() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
Assertions.assertEquals(member.getName(), findMember.getName());
}사실 이 회원가입하는 것은 너무 단순하다.
중요한 것은 중복회원을 보는 것이다.
멤버 객체를 두개 만들어서 넣는데 같다면 예외가 발생하게 된다.
try catch로 잡을 수 있다.
public void 중복_회원_예외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
try {
memberService.join(member2);
fail("예외가 발생해야합니다.");
} catch (Exception e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
// then
}그런데 여기서 try catch쓰는 것은 불편하다.
Assertions.assertThrows 을 사용하면 첫번째 파라미터로 준 예외가 발생한다.
@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 = Assertions.assertThrows(IllegalStateException.class,
() -> memberService.join(member2));// 예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}그런데 이렇게 두면 계속 데이터가 쌓여서 오류가 발생하게 된다.
그래서 계속 초기화해줘야한다.
memberRepository를 같은 인스털스로 두고 싶다면 설정을 해줘야한다.
@BeforeEach 어노테이션을 통해 동작하기 전에 넣어 줄 수있다.
각 테스트전에 알아서 같은 메모리를 만들어서 넣을 수 있다.
외부에서 memberRepository를 넣는다. 이것을 di라고 한다.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
}14. 스프링 빈과 의존관계
화면에 붙이려면 컨트롤러와 view템플릿이 필요하다.
이 멤버 컨트롤러가 서비스에 의존해서 데이터를 조회할 수 있어야한다
이 작업을 스프링으로 할 수 있다.
@Controller를 붙여놓으면 스프링이 알아서 객체를 만들어서 스프링 컨테이너에 넣어둔다.
이것이 스프링 컨테이너에서 스프링 빈이 관리된다고 하는 것이다.
스프링을 사용하면 스프링이 만들어서 알아서 가져다 쓰게 해야한다.
그래서 new로 직접 생성하는 것이 필요가 없고 하나를 만들어 두면된다.
생성자로 @Autowired해주면 생성자호출하면서 알아서 연결되게 해준다.
그런데 그냥쓰면 오류가 발생한다.
스프링컨테이너에 가져다 쓸 MemberService가 없기때문이다.
순수한 자바클래스인 MemberService은 뭐가 뭔지 알 수가 없는 것이다.
그래서 서비스에도 @Service를 붙여줘야한다.
컨트롤러 처럼 이거를 보고 스프링올라올때 알아서 등록을 해준다.
리포지터리는 @Repository을 붙여준다.
스프링이 켜질때 알아서 등록하고 @Autowired로 의존관계를 주입하게 된다.
스프링 bean을 등록하는 방법은 두가지가 있다.
1.컴포넌트 스캔과 자동 주입
@Controller @Service등등이 이에 포함된다.
스프링이 올라올때 알아서 객체화해서 넣는다.
@Autowired로 연관관계를 자동으로 주입해서 이어준다.
2.자바코드로 직접 스프링 빈 등록하기
두가지 방법을 다 알아야한다.
그럼 아무데나 @Controller이런게 잇어도되나?
안된다. main에서 일단 시작하고 하위패키지를 뒤져봐서 찾게 된다.
기본적으로는 컴포넌트 스캔 대상이 되지 않는다.
물론되게 설정할 수는 있다.
참고로 스프링은 컨테이너에 스프링빈을 등록할때 기본적으로 싱글톤으로 등록한다.
딱 하나만 등록해서 공유한다. 따라서 스프링빈이면 모두 같은 인스턴스이다.
설정으로 싱글톤이 아니게 설정할 수 있지만 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
15. 자바 코드로 직접 스프링 빈 등록하기
@Service, @Repository, @Autowired 애노테이션으로 등록을 했다.
회원 서비스와 회원 리포지토리의를 직접 설정파일에 등록해보자.
Config가 필요하다.
@Configuration어노테이션이 붙은 클래스가 있으면
@Bean어노테이션을 달아놓으면 스프링이 시작되면서 객체를 생성해준다.
각 객체를 만들어서 각각사용된것을 넣어주게 된다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}어노테이션과 직접 생성은 각각 장단점이 있다.
자바코드로 하지 않앗고 과거에는 XML로 햇는데 거의 사용하지 않는다.
DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에
필드 주입은 뭔가 바꿀수있는 방법이 없다.
setter의 단점은 public이 되어 있어야해서 다른 사람이 바꾸면 문제가 생길 수 있다.
그래서 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다고 하신다.
실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.
정형화 되지 않거나 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
여기서는 향후 메모리 레포지터리를 다른 레포지터리로 변경할 예정이므로
컴포넌트 스캔 방식 대-신에 자바 코드로 스프링 빈을 설정하였다.
컴포넌트 스캔을 하면 그 설정파일로 옮겨야하는데 직접 설정을하면 주입되는 거만 바꿔주면되서 편하다.
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}@Bean
public MemberRepository memberRepository() {
return new DBMemberRepository();
}주의할점
@Autowired 를 통한 DI는 helloController, memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다.
스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
config에서 관리하는 것은 atuowired할 수 없다. new Member()같은거도 안된다.
2023.05.04
싱글톤을 자바 책에서 배울때는 왜 사용하는지 몰랐었다.
스프링 자체가 싱글톤을 기반으로 작동한다는 것을 생각하면 싱글톤의 작동원리를 이해하게 되는 것 같다.
Junit 단위 테스트가 17이하에서는 자동으로 만들어진게 조금씩 달라진 점이 있어서 제대로 작동을 하지않아 버전을 올렸다.
사용방법을 생각을 할필요가 있어보인다.
'기초단계 > SPRING' 카테고리의 다른 글
| 2023.05.10 Spring (0) | 2023.05.11 |
|---|---|
| 2023.05.09 Spring (0) | 2023.05.09 |
| 2023.05.02 Spring 김영한 (0) | 2023.05.02 |
| 2023.04.28 Spring (0) | 2023.04.28 |
| 2023.04.27 Spring (0) | 2023.04.28 |