forDevLife
스프링 입문 - 웹 개발 기초 <2> 본문
1. 비즈니스 요구사항 정리
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
2. 회원 도메인과 리포지토리 만들기
package hello.hellospring.repositiry;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
//sequence 는 key를 생성해주는 것임(0, 1, 2 ...)
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 store.get(id);
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
//stream 공부 필요...
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
// 자바에서 실무할 때 List로 많이 쓰므로, 이걸로 반환.
// hashmap의 member를 values로 쭉 반환해서 ArrayList로 만듬
return new ArrayList<>(store.values());
}
}
- 결과 없을 때 Null로 감싸주기 위해 optional 반환형 사용한다.
- optional로 반환된 경우, get()을 통해 Optional<Member>의 Member로 반환해서 받을 수 있다.
3. TestCase 작성 (JUnit)
- 방금 만든 회원 repository class가 원하는대로 동작하는지 검증하는 방법임.
- 코드로 코드를 검증하기!
- 원래는, 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능 실행
- 이런 방법은 실행이 너무 오래 걸림. 여러 테스트 한번에 실행하기 어려움
- 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결
* test 주도 개발(TDD) : 테스트를 먼저 만들고, 구현하는 방식
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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();
//test 1가지 끝날 때 마다 repository를 비워주는 메서드 추가
@AfterEach
public void afterEach() {
repository.clearStore();
}
//test를 추가해서 실행 가능 함. main method 쓰는 거랑 비슷!
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
//optional(findById의 반환형)에서 get으로 꺼낼 수 있음
Member result = repository.findById(member.getId()).get();
//글자로 보는 것보다는, 아래의 Assertions 사용
// System.out.println("result = " + (result == member));
//Assertions - by junit
// Assertions.assertEquals(member, null);
//Assertions static import 하면, 앞에 Assertion 안써도 - by assertj
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
//Shift f6을 통해, 겹치는 이름 변경할 수 있음.
Member member2 = new Member();
member2.setName("spring1");
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("spring1");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
- test/java/hello.hellospring에 repository 추가하여 Repository test를 위한 클래스 생성
- 다른 곳에서 쓰는게 아니므로, public으로 안해도 된다.
- 테스트는 각각 독립적으로 실행되어야 한다. 순서에 의존관계가 있는 것은 나쁜 테스트이다.
- @AfterEach : test는 순서대로 돌아가는게 아님. 또한 기존 테스트 내용을 삭제하기 위해 clear 메서드 추가
-> 해당 어노테이션은 테스트가 끝날 때마다 실행됨!
- @Test : 해당 어노테이션이 붙은 메서드가 테스트 시 실행됨. 하나씩도 가능하고, 한꺼번에도 실행 가능
- 위에서 설명했던 걸 다시한번 작성하자면, Optional<Member> 반환형으로 지정되어 있을 경우 get()으로 원래 형태로 받을 수 있다.
- 직접 결과를 println으로 출력하기보다는, Assertion을 통해서 code의 pass/fail을 확인할 수 있다.
- Assertion은 Junit / AssertJ 방식 등이 있으며, AssertJ의 경우, 좀 더 직관적인? 형식으로 작성 가능하다.(아래서 후자가 AssertJ)
-. Assertions.assertEquals(member, result) == Assertions.assertThat(member).isEqualTo(result)
+ Shift F6을 통해, 복사 후 겹치는 이름 변경 가능하다.
+ Option + cmd + V를 통해, 해당 메서드의 반환값을 자동으로 만들어 줄 수 있다.
4. 회원 서비스 개발
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//회원 가입
public long join(Member member) {
// 가입 할 때, 같은 이름은 안된다는 로직을 추가해보자.
Optional<Member> result = memberRepository.findByName(member.getName());
//Optional로 감싸면, 이 안에 멤버가 갇혀 있고, 감싼 덕에 ifPresent 메서드를 사용가능하다.
//result를 바로 빼서 사용도 가능하다.
//멤버 값이 있다면, 아래 출력.
result.ifPresent(m -> {
throw new IllegalStateException("이미 있는 회원입니다.");
});
//아래 처럼 result 없이 바로도 가능하다.
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 있는 회원입니다.2");
});
memberRepository.save(member);
return member.getId();
}
}
- Optional을 이용하면, 이 안에 멤버를 두면서 Optional만의 메서드를 사용할 수 있다.
- get()으로 원래의 멤버(클래스)를 추출 가능하다.
- 여기에서는 ifPresent 메서드를 사용하였다.
- ctrl + t -> extract method를 통해 지정한 부분을 메서드로 뽑아낼 수 있다.
- 위에서 findByName 부분을 validateDuplicateMember라는 메서드로 변경해서 두었다.
* 최종 코드
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repositiry.MemberRepository;
import hello.hellospring.repositiry.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
//비즈니스 서비스 로직!
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//회원 가입
public long join(Member member) {
// 가입 할 때, 같은 이름은 안된다는 로직을 추가해보자.
Optional<Member> result = memberRepository.findByName(member.getName());
//Optional로 감싸면, 이 안에 멤버가 갇혀 있고, 감싼 덕에 ifPresent 메서드를 사용가능하다.
//result를 바로 빼서 사용도 가능하다.
//멤버 값이 있다면, 아래 출력.
result.ifPresent(m -> {
throw new IllegalStateException("이미 있는 회원입니다.");
});
validateDuplicateMember(member);//중복회원 검출
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 있는 회원입니다.2");
});
}
// 전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
- 서비스(비즈니스) 부분은 기존에 작성된 Repository를 활용하는 단계임.
- 따라서 메서드 이름을 비즈니스 스럽게? 직관적으로 작성해야 한다.
5. 회원 서비스 테스트
- 테스트 바로 만드는 법 : cmd + shift + t -> create new test -> test library : Junit
-> 아래 처럼 자동으로 만들어 줌
- 실제 동작하는 코드는 한글로 적기 애매하지만, 테스트는 한글로 적어도 무방!
- build 시 테스트 코드는 포함되지 않으므로 한글로 작성 가능.
* ctrl + option + o -> 안쓰는 import 제거해줌(optimize imports)
command + p -> parameter info
- 한글 name으로 테스트 작성
- given / when / then으로 나눠서 작성
- 이 코드에서는 단순하게 hello라는 이름으로 저장 후, 다시 불러와서 비교하는 테스트. 매우 단순하므로 테스트 추가.
- 항상 예외 적인 상황을 테스트 할 수 있어야 함.
- 같은 이름으로 들어갈 경우 발생하는 예외 체크하는 테스트
- 회원 서비스에서 중복 예외 발생 시 "이미 있는 회원입니다"라는 예외 메세지를 출력했었음
- try ~ catch를 통해 예외 메세지가 서비스의 예외 메세지와 동일한지 체크
- try ~ catch 말고 다른 방법 제공됨. -> assertThrows 사용
- assertThrows 사용
- 오른쪽 람다식이 진행되었을 때, IllegalStateException.class가 발생하게 되면 test pass
- assertThrows는 예외를 반환해주므로, assertThat.isEqual을 통해 메세지 확인 가능하다.
MemberService(비즈니스 서비스 로직)에서 사용되는 repository와, Test에서 사용되는 repository 일치를 위해 아래와 같이 작성.
- MemberService에서 repository를 외부에서 생성하여 받을 수 있도록 생성자를 만든다.
- MemberServiceTest에서 테스트 시, 'BeforeEach'에서 repository를 만들어 서비스 로직에 전달하는 방식으로 작성한다.
- 이를 Dependancy Injection(의존성 주입)이라고 한다.
6. 컴포넌트 스캔과 자동 의존관계 설정
- 멤버 컨트롤러가, 멤버 서비스를 통해 회원가입 / 조회를 해야 한다. -> 의존관계 필요
- 녹색 동그라미가 Controller(bean)이다.
- @controller annotation이 있는 Controller를 컨테이너에서 관리하게 됨.
- 스프링 빈을 등록하는 2가지 방법
1) 컴포넌트 스캔과 자동 의존관계 설정(컴포넌트 + autowire 이용)
2) 자바 코드로 직접 스프링 빈 등록하기
1) 컴포넌트 스캔 > 이 어노테이션이 있으면, 스프링이 객체를 생성하여 컨테이너에 등록을 함.
@Component : 스프링 빈으로 자동 등록
@Controller : 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문임
@Component를 포함하는 다음 애너테이션도 스프링 빈으로 자동 등록된다.
- @Controller
- @Service
- @Repository
* command + B -> 해당 메서드 / 어노테이션 정의로 이동
- 컴포넌트 스캔의 범위는, hellospring 동일 / 하위 패키지까지임.
- 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다.(유일하게 하나만 등록해서 공유)
따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정으로 싱글톤 아니게 설정할 수는 있다.
+ 생성자에 @Autowired 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 해당 어노테이션을 생략할 수 있다.
package hello.hellospring.controller;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
//spring container에서, @Controller 객체를 생성해서 관리하게 됨.
//Spring bean으로 관리된다고 함.
@Controller
public class MemberController {
// 아래처럼 new를 이용하면, 다른 컨트롤러에서도 memberService를 사용할 수 있게 됨.
// Spring Container에 등록하기 위해 아래처럼 진행한다.
// private final MemberService memberService = new MemberService();
private final MemberService memberService;
//생성자에 Autowired 써있으면, memberService를 컨테이너에서 가져와서 연결해준다.
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
2) 자바 코드로 직접 스프링 빈 등록하기
: 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowire 제거 후 진행
* navigate : cmd + o -> 검색하여 메서드로 이동
* 들어가는 parameter 찾기 -> cmd + p
- HelloSpringApplication(main)위치에 SpringConfig 생성
- 이 안에서 @Bean을 통해 memberService(), memberRepository()를 만들어줘야 함.
- memberRepository는 interface이므로 실제 구현체인 MemoryMemberRepository 생성
- Controller는 Component Scan & Autowire로 기존과 같이 작성함.(변경사항 없음)
- memberService를 bean에서 연결 -> memberService에서 repository연결
- annotation이랑, 직접 쓰는거랑 장단점이 있다.
+ DI 주입 방법
1. 생성자 주입 (권장) - 아예 변경 못하게 막을 수 있다.
2. 필드 주입(비권장)
3. Setter 주입 : 중간에 바꿀 일 없음에도, public이라 노출됨. 아무 개발자나 setMemberRepository 호출 가능
- 의존 관계가 실행 중에 동적으로 변하는 경우는 아예 없으므로, 생성자 주입으로 합시다.
- 실무에서 주로 정형화된 컨트롤러, 서비스, 리포지토리(우리가 작성하는)는 컴포넌트 스캔 사용
- 정형화 되지 않고나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록
-> 우리는 repository를 우선 메모리로 만들고, 이를 다른 repository로 바꿔야 한다.
-> 실제 database에 연결되는 repository로 바꿔야 함
-> 기존 코드 변경 없이 바꾸기 위해서 스프링 빈으로 등록이 필요하다.
+ 만약 config에서 스프링 빈으로 등록되지 않았을 경우, Autowired를 통한 DI는 동작하지 않는다.
아래 처럼 MemberService의 Autowired가 의미가 없어진다는 것이다.
'Spring' 카테고리의 다른 글
JPA 사용 시 Entity에 기본 생성자가 필요한 이유 (0) | 2022.05.13 |
---|---|
스프링 입문 - 웹 개발 기초 <4> (0) | 2021.05.24 |
스프링 입문 - 웹 개발 기초 <3> (0) | 2021.05.20 |
스프링 입문 - 웹 개발 기초 <1> (0) | 2021.05.17 |
[spring입문] 인프런 강의 1(프로젝트 생성) (0) | 2021.02.02 |