회원 관리 예제 - 백엔드 개발

이민성·2022년 8월 9일
2

spring 입문

목록 보기
2/5

회원 관리 예제-백엔드 개발

  • 비즈니스 요구사항 정리
  • 회원 도메인과 리포지토리 만들기
  • 회원 리포지토리 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트

비즈니스 요구사항 정리

  • 데이터: 회원ID, 이름
  • 기능: 회원 등록, 조회
  • 아직 데이터 저장소가 선정되지 않음(가상 시나리오)

일반적인 웹 애플리케이션 계층 구조

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현 ex)회원가입은 중복가입이 x
  • 도메인: 비즈니스 도메인 객체 ex)회원, 주문, 쿠폰 등 데이터베이스에 저장하고 관리됨
  • 리포지토리: 데이터베이스에 접근 도메인, 객체를 DB에 저장하고 관리

  • 아직 데이터 저장소가 선정되지 않아, 인터페이스로 구현 클래스를 변경할 수 있도록 설계
  • 개발을 진행하기 위해 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

회원 도메인과 리포지토리 만들기

  1. domain package -> Member class 생성 : 회원
private Long id;		//id 식별자 
private String name;	//이름
  1. repository package -> Memberrepository interface: 회원 객체를 저장하는 저장소
public interface MemberRepository {
    Member save(Member member); 		//저장소에 저장됨
    Optional<Member> findByid(Long id);			//id로 회원을 찾음
    Optional<Member> findByid(String name);		
    List<Member> findAll();
}

*findByid로 가져올 때 값이 없으면 null로 반환되는데 optional로 감싸서 반환 (NPE를 발생시키지 않게 하기 위해)

  1. repository package -> MemoryMemberRepository class
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));
    }
    findByid: id 값을 불러오는게 아닌 객체를 불러옴
    저장소에서 get.(id)로 키값을 가져와 값을 가져옴
    ex) key: 1 -> value: 회원 1

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    member객체의 Namereturn반환. (Member에 저장된 name과 같은지)

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    	}
	}
    멤버 value들을 리스트로 만들어 저장

	public void clearstore() {
        store.clear();
    }
    저장소를 클리어 시켜줌 (메소드가 랜덤으로 실행될 때 발생할 수 있는 꼬임을 막아줌)

회원 리포지토리 테스트 케이스 작성

리포지토리 작성한 것이 정상적으로 작동하는지 테스트 하기 위한 코드

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();


    @AfterEach
    public void afterEach() {
        repository.clearstore();
    }
메소드들 동작이 끝나고 호출되는 메소드로 Store저장소를 다 비워서 
테스트를 돌릴 때 랜덤으로 돌아가는 순서와 상관이 없어짐
(테스트는 순서와 상관없이 메소드들이 따로 동작하게 해야함
-> 다른 메소드에서 먼저 동작하게 되어 다른 값이 저장되면 꼬임)

    @Test
    public void save(){
        Member member = new Member();
        member.setName("spring");
        repository.save(member);
        Member result = repository.findByid(member.getId()).get();
        //System.out.println("result = " + (result == member));
        //Assertions.assertEquals(member, result);
        assertThat(member).isEqualTo(result);

    }
1. save(member)로 저장을 시키면 member에 있는 id들을 get으로 불러와 result라는 변수에 세팅시켜줌
2. member와 result의 값이 같으면 true로 검증시킴(assertThat)

    @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);
    }
}

회원 서비스 개발

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemoryMemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원가입
     */
    public Long join(Member member) {
        // 같은 이름이 있는 중복 회원X
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId();
    }
리포지토리에 있는 save호출. id반환
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
            .ifPresent(m -> {
                throw new IllegalStateException("이미 존재하는 회원입니다.");
             });
    }
ifPresent: null이 아닌 어떤 값이 있으면 동작
findByName 결과가 optional Member니까 중복된 이름이 있으면 실행됨

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
findAll메소드 호출로 Member 값들을 불러옴
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findByid(memberId);
    }
}

  • 서비스 클래스는 비즈니스적 네이밍. 리포지토리는 기계적.단순 네이밍

회원 서비스 테스트

멤버서비스와 메모리멤버리포지토리 인스턴스를 하나로 통일한 이유

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();     
        memberRepository 생성 후
        memberService = new MemberService(memberRepository);   
    }
  	//memberRepository 생성 후
    //MemberService에 넣어줌 그럼 같은 memberRepository사용
    //이렇게 외부에서 넣어주는 방식을 DI = Dependency Injection


    @AfterEach
    public void afterEach() {
        memberRepository.clearstore();
    }
    // clear시켜줌으로써 member의 이름이 변경되어도 메소드들이 제대로 작동함

    @Test
    void 회원가입() {       //test코드는 한글로 적어도됨 (빌드될 때 포함X)
        //given         //given: 무언가가 주어짐 when: 이걸 실행했을 때 then: 결과가 이것이 나와야함
        Member member = new Member();
        member.setName("hello");
    // hello이름을 가진 멤버를 생성
        //when
        Long saveId = memberService.join(member);
    //멤버서비스 join(member)를 검증하면 save된id가 반환
        //then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }
    //저장을 한게 리포지토리에 있는 것과 일치한지 검증
    // Member의 이름이 findMember의 이름과 같은지

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
    //예외가 잘 되는지 보려고 멤버1,2 모두 같은
        // 이름을 가진 spring으로 설정

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,() -> memberService.join(member2));
    //member2를 넣으면 예외가 터져야함. IllegalStateException.class 가 터져야함
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

//        try {
//            memberService.join(member2);
//            fail();
//        } catch (IllegalStateException e) {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
    //try: 예외가 터져서 실행되는 문장 catch: 정상적으로 실행

        //then


    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}


컴포넌트 스캔과 자동 의존관계 설정

스프링 빈을 등록하는 2가지 방법

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기

멤버 컨트롤러는 멤버 서비스를 통해서 회원가입, 데이터 조회

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller //컨트롤러가 있으면 스프링 컨테이너가 관리를 함
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    //Autowired가 있으면 스프링이 스프링 컨테이너에서 멤버서비스를 가져와 연결시켜줌
    //멤버 컨트롤러가 생성이 될 때 스프링 빈에 등록되어 있는 
    //멤버 서비스 객체를 가져와서 넣어줌
    //  => DI(의존 관계 주입)
}

컴포넌트 스캔과 자동 의존관계 설정

  • @Component: 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
  • @Controller: 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.
  • @Component를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.
    - @Controller
    - @Service
    - @Repository
  • @Component라는 annotation이 있으면 스프링이 객체를 생성 하여 스프링컨테이너에 등록을 함
  • memberService는 memberRepository를 필요로 할 때
    @Autowired로 연결
    MemoryMemberRepository가 구현체니까 memberService에 주입해줌
    => 쉽게 말하면 그림의 화살표처럼 서로 연관 관계를 맺게 해줘서
    memberController가 memberService를 쓸 수 있게 해주고,
    memberService가 memberRepository를 쓸 수 있게 해줌.
    + 참고
    스프링은 스프링 컨테이너에 스프빙 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다). 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다.

자바 코드로 직접 스프링 빈 등록하기

직접 설정 파일에 등록하는 방법

  1. 서비스와 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 먼저 없앤 후 진행
  2. SpringConfig 파일 생성 후 memberService와 memberRepository를 @Bean으로 스프링 빈에 등록시켜줌
  3. 서비스는 리포지토리를 사용.
  4. 컨트롤러는 스프링이 관리하는 컴포넌트 스캔이기 때문에 Autowired로 위에 등록해논 멤버 서비스를 연결
package hello.hellospring;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.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());
    }
    //스프링이 뜰 때 Configuration을 읽고 @Bean으로 스프링 빈에 등록하라고 인식
    //MemberService는 memberRepository를 사용함

    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    //스프링이 뜰 때 @Bean으로 memberService, memberRepository를 스프링 빈에 등록을 함
    //스프링 빈에 등록되어 있는 memberRepository를 멤버 서비스에 넣어줌

}

+참고

  • DI에는 필드 주입, setter 주입, 생성자 주입 3가지 방법이 있다.
    요즘은 생성자 주입을 많이 쓴다.
  • 실무에서는 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하는 설정을 통해 스프링 빈으로 등록한다.
    ex) 데이터 저장소가 선정되지 않은 가상 시나리오면 메모리를 나중에 변경해야 할 상황이 옴
  • 주의: @Autowired를 통한 DI는 helloConroller, MemberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 설정한 객체에서는 동작하지 않는다.
    ex)MemberService같이 스프링에 등록이 되고 스프링이 관리를 해야만 Autowired도 동작함

0개의 댓글