티스토리 뷰

반응형

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

  • 비즈니스 요구사항 정리
  • 회원 도메인과 레포지토리(회원 도메인을 저장하고 불러올 수 있다.일종의 저장소 개념) 만들기
  • 회원 레포지토리 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트

비즈니스 요구사항 정리

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

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

  • 컨트롤러: 웹 MVC 의 콘트롤러 역할

  • 서비스: 핵심 비즈니스 로직 구현한다.

  • 레포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리한다.

  • 도메인: 비즈니스 도메인 객체. 예 ) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리된다.

클래스 의존 관계

  • 아직 데이터 저장소가 선정되지 않아서 우선 인터페이스로 구현 클래스를 변경할 수 있게 설계한다.
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정한다.
  • 개발을 진행하기 위해서 초기 개발단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다.

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

[tip] getter, setter 에는 alt+ Insert

회원 객체

package com.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;
  }
}

회원 레포지토리 인터페이스

package com.hello.hellospring.repository;

import com.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();
}

회원 레포지토리 메모리 구현체

package com.hello.hellospring.repository;

import com.hello.hellospring.domain.Member;

import java.util.*;

/*
  동시성 문제가 고려되있지 않다. 실무해서는 `ConcurrentHashMap` , `AtomicLong` 사용을 고려해야한다.
*/
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());
  }

  public void clearStore() {
    store.clear();
  }
}

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

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

package com.hello.hellospring.repository;

import com.hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemoryMemberRepositoryTest {

  MemberRepository repository = new MemoryMemberRepository();

  @AfterEach
  public void afterEach() {
    repository.clearStore();
  }

  @Test
  void save() {
    Member member = new Member();
    member.setName("spring");
    repository.save(member);
    Member result = repository.findById(member.getId()).get();
    assertThat(member).isEqualTo(result);
  }

  @Test
  void findById() {}

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

  @Test
  void clearStore() {}
}
  • @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다. 테스트는 각각 독립적으로 실행되어야 한다.
  • 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

회원 서비스 개발

package com.hello.hellospring.service;

import com.hello.hellospring.domain.Member;
import com.hello.hellospring.repository.MemberRepository;

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

public class MemberService {
  public final MemberRepository memberRepository;

  public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;//회원 서비스 코드를 DI 가능하게 변경
  }

  // 회원 가입
  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);
  }
}

회원 서비스 테스트

package com.hello.hellospring.service;

import com.hello.hellospring.domain.Member;
import com.hello.hellospring.repository.MemberRepository;
import com.hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {
  MemberService memberService;
  MemberRepository memberRepository;

  @BeforeEach // @BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,의존관계도 새로 맺어준다
  public void beforeEach() {
    memberRepository = new MemoryMemberRepository();
    memberService = new MemberService(memberRepository);
  }

  @AfterEach
  void tearDown() {
    memberRepository.clearStore();
  }

  @Test
  void 회원가입() {
    // given
    Member member = new Member();
    member.setName("재은");
    // when
    Long savedId = memberService.join(member);
    // then
    Member findMember = memberService.findOne(savedId).get();
    assertThat(findMember).isEqualTo(member);
    assertThat(member.getName()).isEqualTo(findMember.getName());
  }

  @Test
  void 중복_회원_예외() {
    // given
    Member duplicateMember1 = new Member();
    duplicateMember1.setName("재은");
    Member duplicateMember2 = new Member();
    duplicateMember2.setName("재은");
    // when
    memberService.join(duplicateMember1);

    IllegalStateException e =
        assertThrows(IllegalStateException.class, () -> memberService.join(duplicateMember2));
    assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다. ");
    /*   try {
      memberService.join(duplicateMember2);
      fail();
    } catch (IllegalStateException e) {
      e.printStackTrace();
      assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }*/
    // then

  }

  @Test
  void 전체회원찾기() {}

  @Test
  void 한놈찾기() {}
}

스프링과 빈의 의존관계

스프링은 빈으로 이뤄져있는데, 콤포넌트 스캐닝을 통해 @Component 로 등록된 것을 모두 스프링 빈으로 자동 등록하고 스프링 컨테이너에 띄운다. (초록색을 빈으로 생각) 후에 @Autowired 등을 통해 의존관계(연결, 선)를 설정하는 것이다.

  • 생성자에 @Autowired 라고 붙어있으면, 스프링이 연결된 객체를 스프링 컨테이너에서 찾아서 넣어준다.
    • 이렇게 객체의 의존관계를 외부에서 넣어주는 것을 DI(Dependency Injection) , 의존성 주입 이라 한다.
  • 이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired 에 의해 스프링이 직접 주입한다.

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

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

컴포넌트 스캔 원리

  • @Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다.

  • @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.

  • @Component 를 포함하는 다음 어노테이션도 스프링 빈으로 자동 등록된다.
    • @Controller
    • @Service
    • @Repository
@Service
public class MemberService {
 private final MemberRepository memberRepository;
 @Autowired
 public MemberService(MemberRepository memberRepository) {
 this.memberRepository = memberRepository;
 }
}

생성자에 @Autowired 를 사용하면, 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아 주입한다. 생성자가 1개 있으면 @Autowired 는 생략할 수 있다.

참고

스프링은 스프링 컨테이너에 스프링 빈을 등록할때, 기본으로 싱글톤으로 등록한다.(= 유일하게 하나만 등록해 공유한다는 뜻). 따라서 같은 스프링 빈이면, 모두 같은 인스턴스이다. 설정으로 싱글톤이 아니게 할 수 있다.

  • 컴포넌트 스캐닝의 경우 mainApplication 에서 수행한다. 메인 어플리케이션의 패키지 com.hello.hellospring 내의 어디서든지 @Component 로 등록하면, 스프링 빈이 자동으로 등록된다. 다른 패키지에서도 빈이 등록되게 하려면 내부를 커스텀해야한다.

  • package com.hello.hellospring;
    @SpringBootApplication
    public class HelloSpringApplication {
       public static void main(String[] args) {
           SpringApplication.run(HelloSpringApplication.class, args);
       }
    
    }

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

  • 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.

  • 콘트롤러는 어쩔 수 없다. @Controller@Autowired 를 사용해야한다.

package com.hello.hellospring;

import com.hello.hellospring.repository.MemberRepository;
import com.hello.hellospring.repository.MemoryMemberRepository;
import com.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());
  }

  @Bean
  public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
  }
}

DI 방법

  • 필드 주입 : 필드주입은 갈아낄 수 없다는 단점이 있다.

    • @Autowired private MemberService memberService; // 스프링 컨테이너야 알아서 등록해
  • setter 주입: 바뀔 일이 없는데 public 하게 노출이 된다. (다른 개발자가 호출할수도.. 조립시점 이후에는 결단 내버리자)

      @Autowired private MemberService memberService; 
      public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
      }
  • 생성자 주입

        private MemberService memberService; 
        @Autowired
        public MemberController(MemberService memberService) {
            this.memberService = memberService;
        }

    의존관계가 실행중에 동적으로 변하는 경우는 거의 없기 때문에 생성자 주입을 권하고 있다.

  • 실무에서는 주로 정형화된 컨트롤러, 서비스, 레포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야하는 경우 설정을 통해 스프링 빈으로 등록한다.

  • 주의: @Autowired 를 통한 DI 는 helloController, memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고, 내가 직접 생성한 객체에는 동작하지 않는다.


반응형

'스프링, 자바' 카테고리의 다른 글

김영한 강사님의 스프링 입문 나머지  (0) 2021.01.31
회원 관리 예제 - 웹 MVC 개발  (0) 2021.01.30
필터와 리스너 간단 정리  (0) 2021.01.26
MVC 패턴 구현 요약  (0) 2021.01.26
자바 서블릿 요약  (0) 2021.01.26
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함