본문 바로가기
  • GDG on campus Ewha Tech Blog
3-2기 스터디/Spring 입문

[2주차] 회원 관리 예제, 스프링 빈과 의존관계

by 선달 2022. 5. 4.

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

🌱 비즈니스 요구사항 정리

  • 데이터 : id, name
  • 기능 : 회원 등록, 조회

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

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

클래스 의존관계

🌱 회원 도메인과 리포지토리 만들기 + 테스트


  • 회원 객체 (Member)

      package com.example.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;
          }
      }
  • 회원 리포지토리 인터페이스 (MemberMemoryRepository)

      package com.example.hellospring.repository;
    
      import com.example.hellospring.domain.Member;
    
      import java.util.*;
    
      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();
          }
      }
  • 회원 리포지토리 테스트케이스 (MemberMemoryRepositoryTest)

      package com.example.hellospring.repository;
    
      import static org.assertj.core.api.Assertions.*;
      import com.example.hellospring.domain.Member;
      import org.junit.jupiter.api.AfterEach;
      import org.junit.jupiter.api.Test;
    
      import java.util.List;
    
      public 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);
          }
      }

Assertions.assertThat() 오류

→ 임포트를 잘하자

static import가 자동완성이 되지 않습니다. - 인프런 | 질문 & 답변

테스트 성공으로 뜨는데 빨간색 오류

→ 테스트 코드 여러개중 일부만 수행햇을 때 생기는 거라 옆에 초록색만 생기면 ㄱㅊ

질문하나만 드려도될까요.. - 인프런 | 질문 & 답변

단축키 모음

optional?

코딩교육 티씨피스쿨

@AfterEach

: 각 테스트 코드가 실행된 후에 자동으로 수행

@AfterEach
public void afterEach() {
    repository.clearStore();
    // 데이터 삭제
}

🌱 회원 서비스 개발 + 테스트


  • 회원 서비스 개발 (MemberService)

      package com.example.hellospring.service;
    
      import com.example.hellospring.domain.Member;
      import com.example.hellospring.repository.MemberRepository;
      import com.example.hellospring.repository.MemoryMemberRepository;
    
      import java.util.List;
      import java.util.Optional;
    
      public class MemberService {
    
      //    private final MemberRepository memberRepository = new MemoryMemberRepository();
          private final MemberRepository memberRepository;
          public MemberService(MemberRepository memberRepository) {
              this.memberRepository = memberRepository;
          }
    
          // 회원 가입
          public Long join(Member member) {
    
              // 이름 중복 처리
              validateDuplicateMember(member);
              memberRepository.save(member);
              return member.getId();
          }
    
          private void validateDuplicateMember(Member member) {
      //        Optional<Member> result = memberRepository.findByName(member.getName());
      //        result.ifPresent(m -> {
      //            throw new IllegalStateException("이미 존재하는 회원입니다");
      //        });
              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);
          }
      }
  • 회원 서비스 테스트 (MemberServiceTest)

      package com.example.hellospring.service;
    
      import com.example.hellospring.domain.Member;
      import com.example.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.*;
    
      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("hello");
    
              //when
              Long saveId = memberService.join(member);
    
              //then
              Member findMember = memberService.findOne(saveId).get();
              assertThat(member.getName()).isEqualTo(findMember.getName());
          }
    
          @Test
          public void 중복_회원_예외() {
              Member member1 = new Member();
              member1.setName("spring");
    
              Member member2 = new Member();
              member2.setName("spring");
    
              memberService.join(member1);
              try {
                  memberService.join(member2);
                  fail("예외가 발생해야 합니다.");
              } catch (IllegalStateException e) {
                  assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");
              }
          }
    
          @Test
          void findMembers() {
          }
    
          @Test
          void findOne() {
          }
      }

@BeforeEach

: 각 테스트 코드가 실행되기 전에 자동으로 수행

@BeforeEach
public void beforeEach() {
    repository.clearStore();
}


4. 스프링 빈과 의존관계


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


컨트롤러가 서비스와 리포지토리를 사용할 수 있게 의존관계를 준비하는 과정

회원 컨트롤러에 의존관계 추가

package com.example.hellospring.controller;

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

@Controller
public class MemberController {
    private final MemberService meberService;

    @Autowired
    public MemberController(MemberService meberService) {
        this.meberService = meberService;
    }
}
  • @Component : 스프링 빈으로 자동 등록

  • @Autowired : 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어줌

    → 의존성 주입 (DI : Dependency Injection)

  • 회원 서비스 스프링 빈 등록

      @Service
      public class MemberService {
    
          private final MemberRepository memberRepository;
    
          @Autowired
          public MemberService(MemberRepository memberRepository) {
              this.memberRepository = memberRepository;
          }
  • 회원 리포지토리 스프링 빈 등록

      @Repository
          public class MemoryMemberRepository implements MemberRepository {}

기본으로 싱글톤으로 등록 (하나만 등록해서 공유)
→ 같은 스프링 빈 = 같은 인스턴스

참고: helloController 는 스프링이 제공하는 컨트롤러 → 스프링 빈 자동 등록

스프링 빈 등록방법 2가지

  • 컴포넌트 스캔과 자동 의존관계 설정 (위)
  • 자바코드로 직접 스프링 빈 등록 (아래)

컴포넌트 스캔원리

다음 annotation 이 있으면 스프링 빈으로 자동 등록

  • @Component
  • @Service
  • @Repository

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


package com.example.hellospring;

import com.example.hellospring.repository.MemoryMemberRepository;
import com.example.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 MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}


5. 회원 관리 예제 - 웹 MVC 개발


홈 화면 추가


홈 컨트롤러

package com.example.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}
  • 회원 관리용 홈 html

      <!DOCTYPE HTML>
      <html xmlns:th="http://www.thymeleaf.org">
      <body>
      <div class="container">
          <div>
              <h1>Hello Spring</h1> <p>회원 기능</p>
              <p>
                  <a href="/members/new">회원 가입</a> <a href="/members">회원 목록</a>
              </p> </div>
      </div> <!-- /container -->
      </body>
      </html>

등록


회원 등록 폼 컨트롤러

@Controller
public class MemberController {
    private final MemberService meberService;

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

    @GetMapping("/members/new")
    public String createForm() {
        return "members/createMemberForm";
    }

    @PostMapping("/members/new")
    public String create(MemberForm form) {
        Member member = new Member();
        member.setName(form.getName());

        System.out.println("System : " + member.getName());
        memberService.join(member);

        return "redirect:/";
    }
}
  • 회원 등록 폼 화면 html

      <!DOCTYPE HTML>
      <html xmlns:th="http://www.thymeleaf.org">
      <body>
      <div class="container">
          <form action="/members/new" method="post">
              <div class="form-group">
                  <label for="name">이름</label>
                  <input type="text" id="name" name="name" placeholder="이름을 입력하세요"> </div>
              <button type="submit">등록</button> </form>
      </div> <!-- /container -->
      </body>
      </html>

회원 등록 컨트롤러

웹 등록 화면에서 데이터를 전달 받을 폼 객체

package com.example.hellospring.controller;

public class MemberForm {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

조회


회원 컨트롤러에서 조회 기능

@GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
  • 회원 리스트 html

      <!DOCTYPE HTML>
      <html xmlns:th="http://www.thymeleaf.org">
      <body>
      <div class="container">
        <div>
          <table>
            <thead>
            <tr>
              <th>#</th>
              <th>이름</th> </tr>
            </thead>
            <tbody>
            <tr th:each="member : ${members}">
              <td th:text="${member.id}"></td>
              <td th:text="${member.name}"></td>
            </tr>
            </tbody>
          </table>
        </div>
      </div> <!-- /container -->
      </body>
      </html>

댓글