이번엔 회원 관리 예제를 만들어보려 한다.
기본 환경설정에 관한 내용은 아래를 참고하자.
https://surrealcode.tistory.com/85
비즈니스 요구사항 정리
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않았음
일반적인 웹 애플리케이션 구조는 위와 같은 형태를 띄고 있다.
컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현
리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
도메인 : 비즈니스 도메인 객체, 예)회원, 주문, 쿠폰, 등 주로 DB에 저장하고 관리 됨
클래스 의존관계는 다음과 같이 설계된다.
- 아직 데이터 저장소가 선정되지 않았기에 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
- 데이터 저장소는 다양한 저장소를 고민중인 상황으로 가정한다.
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다.
패키지 구조는 다음과 같다.
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;
}
}
Member클래스는 id와 String의 값을 갖는다.
public interface MemberRepository {
Member save(Member member); //회원을 저장하면 저장된 회원이 반환
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
아직 데이터 저장소가 선정되지 않았기에 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
여기서 Optional은 만약 findById나 findByName의 값이 null일때 Optional을 통해 반환하도록 한다.
findAll은 멤버를 List형태로 확인할 수 있게 설계하였다.
위 인터페이스를 구현한 MemoryMemberRepository는 아래와 같다.
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());
}
}
Map을 통해 데이터값을 저장한다.
save 메서드를 확인해보면
member의 id는 임의의 sequence값을 받도록 하였고 그 값은 id가 할당될때마다 1씩 증가한다.
store.put을 통해 member의 id값과 멤버 객체를 HashMap인 store에 저장하게 된다.
findById(Long id)는 id값을 받으면 store.get()을 통해 id에 맞는 값을 리턴한다.
회원 리포지토리 테스트 케이스 작성
이렇게 작성한 회원 리포지토리의 테스트 케이스를 작성해보자.
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고, 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
패키지명은 보통 검증할 클래스가 있는 패키지의 이름과 동일하게 하고, 클래스명 또한 동일한 클래스명에 Test를 붙혀주는 것이 관례이다.
MemoryMemberRepository에 이 메서드를 하나 추가해주자.
public void clearStore(){
store.clear();
}
코드는 아래와 같다.
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 result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
@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);
}
}
코드 검증을 위해 MemoryMemberRepository를 객체로 생성하였고 이것을 활용한다.
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
repository.clearStore();
}
메서드는 테스트그 하나씩 끝날때마다 호출되어 respository를 클리어해준다.
테스트는 순차적으로 이루어 지는것이 아니라 자바가 판단한 순서대로 테스트가 진행된다.
미리 생성되있는 객체와 충돌이 되면 검증에 실패할수도 있기 때문에 afterEach()메서드를 통해 매번 repository를 클리어 해주어야 한다.
@Test
public void save(){
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
save()메서드가 잘 이루어지는지에 대한 검증이 이뤄지는 메서드이다
Member객체를 생성하고, 이름을 지정 후 repository에 저장한다.
그 값의 id를 result로 꺼내와서 현재 member의 id와 result의 id가 동일한지 검증하는 코드이다.
Assertions.assertThat(member).isEqualTo(result);라는 코드를 통해 두 id가 같은지 검증한다.
Assertions는 import를 통해 생략할 수 있다.
다른 Test 코드들 또한 검증을 위한 코드이다.
다음은 회원 서비스 코드이다.
회원 서비스는 회원 리포지토리랑 도메인을 활용해서 실제 비즈니스 로직을 작성한다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member){
//같은 이름이 있는 중복 회원X
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> findOnd(Long memberId){
return memberRepository.findById(memberId);
}
}
중복 회원은 허용하지 않기 때문에 validateDuplicateMember()메서드를 통해 중복회원이 있을 시 Exception이 터지도록 설계하였다.
이 서비스 코드 또한 test로 검증을 해야하는데 이 클래스에서 ctrl+shift+t 버튼을 누르게 되면
create new test가 뜬다.
를 실행하면
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
@Test
void join() {
}
@Test
void findMembers() {
}
@Test
void findOnd() {
}
}
껍데기를 만들어 준다... ^^ 개꿀팁
참고 : 테스트메서드는 한글로 적어도 된다. join ->회원가입 이런 형식도 가능하다.
테스트는 또한 형식을 나누어서 테스트 하길 권장되는데
그 문법은
@Test
void 회원가입() {
//given
//when
//then
}
given when then 문법 이다.
given : 이런 상황이 주어졌는데
when : 이걸 실행 했을때
then : 결과가 이게 나와야 해
라는 문법이다.
상황에 따라서 안맞을때도 있다. 기초는 이걸 통해 다지자.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public 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 (IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}*/
//then
}
@Test
void findMembers() {
}
@Test
void findOnd() {
}
}
회원가입을 검증하는것도 중요하지만. 중복회원의 예외가 터지는 것도 검증을 해야 좋은 테스트이다.
회원 가입만 검증을 하는 것은 반쪽짜리 테스트이다.
따라서 중복 회원 예외도 검증을 하였다.
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
try, catch문으로 검증을 해도 되지만.
Assertions에서는 검증을 도와주는 메서드인 assertThrows 메서드가 있다. 람다형식으로 검증을 하였다.
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
또한 검증시 클리어를 해줘야하는데 MemberService엔 clear를 해주는 기능이 없다.
그래서 아래와 같이MemoryMemberRepository에 있는 clear기능을 가져와야 한다.
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
여기 보면 좀 애매한게 있다.
MemberService()로 생성된 객체 안에는 이미 MemoryMemberRepository()객체를 생성하고 있기 때문이다.
이걸 두개를 쓸 이유가 없다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
스프링 빈과 의존관계 설정하기
회원 컨트롤러가 회원 서비스와 회원 리포지토리를 사용할 수 있게 의존관계를 준비하자.
package hello.hello_spring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
}
Controller라는 애노테이션이 있는 클래스를 생성하게 되면 스프링이 뜰 때, 스프링 컨테이너 안에 MemberController라는 객체를 생성해서 스프링이 들고 있게 된다.
-> 이것을 스프링 컨테이너에서 스프링 빈이 관리된다고 표현한다.
이제 MemberController 클래스 내부에서
private final MemberService memberService = new MemberService();
를 통해서 MemberService()의 객체를 생성하려고 했다.
하지만 이렇게 객체를 생성하면 한가지 문제가 있다.
MemberController 말고 다른 여러 Controller들이 멤버 서비스를 가져다 쓸 수 있기 때문에 여러개의 인스턴스를 생성할 필요가 없다.
그냥 하나만 생성해서 같이 공통으로 쓰면 되기 때문이다.
이 문제를 Autowired를 통해 해결한다.
스프링 컨테이너에 등록을 하게 되면 딱 하나만 등록이 되고, 그 외 여러 부가적인 효과를 누릴 수 있다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
@Autowired를 사용하면 스프링 컨테이너에 있는 멤버 서비스를 가져다가 연결을 시켜준다.
하지만 이 상태로 실행을 하게 되면 아래와 같은 에러가 발생한다.
Parameter 0 of constructor in hello.hello_spring.controller.MemberController required a bean of type 'hello.hello_spring.service.MemberService' that could not be found.
Consider defining a bean of type 'hello.hello_spring.service.MemberService' in your configuration.
참고 : helloController는 스프링이 제공하는 컨트롤러여서 스프링 빈으로 자동 등록된다.
등록되지 않은 이유는 MemberService 클래스를 가보면 알 수 있다.
MemberService 클래스는 순수한 자바 클래스이다. 스프링이 얘를 알 수 있는 방법이 없다.
그렇기에 MemberService 위에 어노테이션을 붙혀줘서 해결한다.(@Service)
@Service
public class MemberService {
}
@Service를 붙혀주면 스프링이 올라올 때 서비스인것을 판별하고 스프링 컨테이너에 멤버 서비스를 등록해준다.
Repository또한 마찬가지로 어노테이션을 붙혀준다(@Repository)
@Repository
public class MemoryMemberRepository implements MemberRepository{
}
컨트롤러를 통해서 외부 요청을 받고, 그 다음에 서비스에서 비즈니스 로직을 만들고, 리포지토리에서 데이터를 저장한다.
컨트롤러, 레포지토리, 서비스는 정형화 된 패턴이다.
'memberService'와 'memberRepository'가 스프링 컨테이너에 스프링 빈으로 등록되었다.
@Autowired를 통해 helloController가 생성될때 memberService를 넣어준다 이걸 Dependency Injection(DI)라고 한다.
@참고 : helloController는 스프링이 제공하는 컨트롤러여서 스프링 빈으로 자동 등록된다.
'@Controller'가 있으면 자동 등록 됨
*스프링 빈을 등록하는 2가지 방법
- 컴포넌트 스캔과 자동 의존관계 설정(@Controller, @Service, @Repository 등)
- 자바 코드로 직접 스프링 빈 등록하기
컴포넌트 스캔과 자동 의존관계 설정
- '@Component' 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
-'@Controller' 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.
-'@Component'를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다
- '@Controller'
- '@Service'
- '@Repository'
(Service 어노테이션 속에 Component 어노테이션이 있기 때문에 Service 어노테이션을 붙히면 굳이 Component를 안붙혀도 된다.)
참고 : 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정으로 싱글톤이 아니게 설정할 순 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
자바 코드로 직접 스프링 빈 등록하기
회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.
package hello.hello_spring;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import hello.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
SpringConfig라는 클래스를 새로 생성하고 여기서 @Bean으로 객체를 생성한다.
참고 : DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
@Controller
public class MemberController {
//필드 주입
//@Autowired private MemberService memberService;
//생성자 주입
/*private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}*/
//setter 주입
/*private MemberService memberService;
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}*/
}
필드 주입 : 필드 주입은 뭔가 바꿀 수 있는 방법이 없다. 스프링 뜰때만 넣어주고 중간에 바꿔치기가 불가능하다.
setter 주입 : setter 주입은 누군가가 멤버 컨트롤을 호출했을때 퍼블릭으로 열려있어야 한다. 바꿔치기를 할 이유가 없으나 얘가 퍼블릭 하게 노출이 된다.
참고 : 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
주의 : '@Autowired'를 통한 DI는 'helloController', 'MemberService'등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고, 내가 직접 생성한 객체에서는 동작하지 않는다.
'공부 > Spring' 카테고리의 다른 글
스프링의 핵심 원리 이해 1 - 예제 만들기 (3) | 2024.10.26 |
---|---|
객체 지향 설계와 스프링 (1) | 2024.10.22 |
스프링 회원 관리 예제 2 (1) | 2024.10.21 |
스프링 입문 - 프로젝트 환경 설정2 (0) | 2024.10.15 |
스프링 입문 - 프로젝트 환경 설정1 (1) | 2024.10.11 |