-
스프링입문 - 백엔드 개발, 빈과 의존관계, MVC패턴스프링/스프링부트+JPA - 블로그 2021. 8. 26. 11:08
인프런 '김영한'님의 강의 참조!
- 회원 관리 예제(백엔드 개발)
1) 비지니스 요구사항 정리
- 쓰는 데이터 딱 2가지 : 회원ID, 이름
- 기능: 회원 등록, 조회
- 데이터 저장소가 정해지지 않은 상태에서 개발한다는 가상의 시나리오
스프링 MVC패턴의 핵심중의 핵심 계층 구조이다! - 컨트롤러 - MVC패턴의 중심에서 컨트롤 역할
- 서비스 - 핵심 비지니스 로직 구현(회원중복가입안됨 등)
- 리포지토리 - DB에 접근하여 도메인 객체 그 자체를 저장하거나 불러오는 역할
- 도메인 - 비지니스 로직에 쓰이는 객체(회원,주문,쿠폰 등 DB에 저장하고 관리되는 부분)
2) 회원 도메인과 리포지토리 만들기
- DB 플래폼이 정해지지않았다는 가정하에 리포지토리를 INTERFACE화 하여 나중에 선정되면 바꿀 수 있도록 하기 위함
- 멤버리포지토리를 인터페이스화하여 현재 기능을 자유자재로 쓰도록 구성 함
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; } }
public interface MemberRepository { Member save(Member member); //회원 저장 기능 Optional<Member> findById(Long id); //Optional은 null 반환 기능 Optional<Member> findByName(String name); List<Member> findAll(); // 모든 회원 찾기 }
- 인터페이스를 받아서 기능 세팅
public class MemoryMemberRepository implements MemberRepository { private static Map<Long, Member> store = new HashMap<Long, Member>(); 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<Member>(store.values()); } }
- 현재 완벽한 MVC패턴을 구현한게 아니기때문에 기능만 보기 위한 간단히 세팅을 한 것
- Map이 가지고 있는 특성을 이용해서 아이디와 이름을 찾는 세팅이다
- save에서는 interface에서 구현한것을 기반으로 아이디가 세팅되면 시퀀스가 올라가도록하고 map을 통해 그 아이디를 저장한다
- findByIdsms optional을 통해 null을 처리하고 map에 저장된 아이디를 get으로 찾아온다
- findByName은 map과 람다식을 이용하였고 필터를 통해 getName시에 name과 같은것을 하나라도 있으면 찾아온다
- findAll은 모두 찾는것이기때문에 list형식으로 store의 키값에 해당하는 모든것을 찾음
stream()관련 글은 밑에 블로그 참조!!!
https://jeong-pro.tistory.com/165
자바 스트림(Stream) API 정리, 스트림을 이용한 가독성 좋은 코드 만들기(feat. 자바 람다, 함수형 프
Java Stream 자바 공부를 하면서 Stream이 무엇인지, 어떻게 사용되고 있는지 인지는 하고 있었으나 실제 코드로 타이핑해보지 않았다. 그러던 중 이번에 가볍게 API 훑어보는 식으로 공부를 하면서
jeong-pro.tistory.com
3) 테스트 케이스 작성
- 지금까지 만든것이 정상적으로 작동하는지 보는 것
- 보통 Junit을 통해서 한다
public class MemoryMemberRepositoryTest { MemberRepository repository = new MemoryMemberRepository(); //세팅된 이름값과 findById값이 같은지 테스트 @Test public void save() { Member member = new Member(); member.setName("Vic"); repository.save(member); Member result = repository.findById(member.getId()).get(); // Assertions.assertEquals(member, result); assertThat(member).isEqualTo(result); }
정상! public class MemoryMemberRepositoryTest { MemberRepository repository = new MemoryMemberRepository(); .... @Test public void findByName() { Member member1 = new Member(); member1.setName("vic"); repository.save(member1); Member member2 = new Member(); member2.setName("cha"); repository.save(member2); Member result = repository.findByName("cha").get(); assertThat(result).isEqualTo(member1); }
정상 작동이 아니면 빨간불! public class MemoryMemberRepositoryTest { ... @Test public void findAll() { Member member1 = new Member(); member1.setName("vic"); repository.save(member1); Member member2 = new Member(); member2.setName("cha"); repository.save(member2); List<Member> result = repository.findAll(); assertThat(result.size()).isEqualTo(2); }
정상! - Junit 테스트시 주의사항은 한번에 한클래스의 여러 메소드를 작동하면 메소드 작동 순서가 보장이 안된다. 그래서 오류가 발생 할 수도 있다. 그래서 반드시 매 매소드마다 클리어를 시켜줘야한다!
public class MemoryMemberRepository implements MemberRepository { ... public void clearStore() { store.clear(); } }
public class MemoryMemberRepositoryTest { MemoryMemberRepository repository = new MemoryMemberRepository(); @AfterEach public void afterEach() { repository.clearStore(); } ...
- 원래 MemberRepository객체를 테스트가 필요한 메모리로 바꾸고 거기에 clear() 시켜주는 메소드를 만든 후 @AfterEach의 기능을 이용해서 각 메소드 작동 후 비우도록 설정 한 것. 정말 중요하다!
4) 회원 서비스 개발
- 서비스는 항상 비지니스와 연관이 되도록 이름들을 세팅하는게 좋다!
public class MemberService { private final MemberRepository memberRepository = new MemoryMemberRepository(); //회원가입 public Long join(Member member) { //중복체크 validateDuplicateMember(member); memberRepository.save(member); return member.getId(); } private void validateDuplicateMember(Member member) { memberRepository.findByName(member.getName()) // Optional 멤버이기때문에 Optional이 들고 있는 기능 사용 가능 .ifPresent(m -> { throw new IllegalStateException("The ID already exist"); }); } //회원조회 public List<Member> findMembers() { return memberRepository.findAll(); } public Optional<Member> findOne(Long memberId){ return memberRepository.findById(memberId); } }
4-1) 회원서비스 테스트
class MemberServiceTest { MemberService memberService = new MemberService(); @Test void Join() { //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 IdException() { //given Member member1 = new Member(); member1.setName("spruing"); Member member2 = new Member(); member2.setName("spruing"); //when memberService.join(member1); IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2)); assertThat(e.getMessage()).isEqualTo("The ID already exist"); // try { // memberService.join(member2); // fail(); // } catch (IllegalStateException e) { // assertThat(e.getMessage()).isEqualTo("The ID already exist"); // } //then }
정상 - 똑같이 전체 한번에 테스트를 위한 clear() 세팅
class MemberServiceTest { .. MemoryMemberRepository MemberRepository = new MemoryMemberRepository(); @AfterEach public void afterEach() { memberRepository.clearStore(); } .. } }
*지금까지 테스트시에 new로 인스턴스한 객체와 실제로 테스트가아닌 자바패키지 안에서 만들어진 객체가 다른 문제가 있다. (예:MemberService에서 new한 MemoryMemeberRepository와 테스트에서 선언한 MemoryMemeberRepository가서로 다른 객체이다) 반드시 테스트와 자바패키지안의 내용물은 같은걸로 써야한다. 이를 해결해보자!
public class MemberService { private final MemberRepository memberRepository = new MemoryMemberRepository(); --> 위에의 것을 생성자로 바꿔서 객체화를 통일시킨다! public class MemberService { private final MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
class MemberServiceTest { MemberService memberService; MemoryMemberRepository memberRepository; @BeforeEach public void beforeEach() { memberRepository = new MemoryMemberRepository(); memberService = new MemberService(memberRepository); } ...
- 테스트를 실행하게되면 제일먼저 @BeforeEach가 실행되고 memberRepository 객체가 만들어지고 그 객체가 memberService생성자로 들어가게된다. 그렇게되면 같은 데이터를 쓰는 것, 즉 DI의 관계에 엮이는 것!
현재까지 한 것 간단 정리
- 도메인(Member.java) - 비지니스 로직에 쓰이는 전체 객체(아이디/이름)
- 레포지토리(MemberRepository.java(Interface)) - 인터페이스로 정의 후 기능 강제 및 어디서든 다양하게 쓰도록 함
MemoryMemberRepository.java - 인터페이스를 받아서 DB에 실제로 접근하여 데이터 송수신, 데이터를 저장하거나 꺼내오거나 등등
(아이디저장,아이디조회,전체조회 등) - 서비스(MemberService) - 레포지토리를 통해 세팅된 DB접근용 코드로 실제 비지니스 기능 구현
(회원가입,회원조회등)
5) 스프링 빈과 의존관계
- 이제 만들어놓은 기능들을 화면에 나타내고 싶다. 컨트롤러와 템플릿엔진이 필요하다
- 템플릿은 화면을 그린다면 컨트롤러는 서비스를 통해서 기능을 구현하고 데이터를 주고받는데 이를 의존관계가 있다고 한다
- @Controller 어노테이션을 통해 빈이 생성되고 컨테이너로 관리가 된다. 즉 new로해서 만들지 않는다. 스프링이 관리하도록 한다
@Controller public class MemberController { private final MemberService memberService; @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; } }
- 서비스,리포지토리 또한 어노테이션으로 빈처리를 하자
@Service public class MemberService { @Repository public class MemoryMemberRepository implements MemberRepository {
- 사진에서 설명하듯이 컨트롤러는 서비스가 필요하고 서비스는 레포지토리가 필요하게되는 서로 의존관계에 있다. 그 의존관계를 이어주는 @Autowired! 그렇게되면 서로 작동할때 반드시 엮이게 되는 장점이있다
- 생성자, 멤버변수, 필드에 선언이 가능하다
@Autowired public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; }
- 더 많은 지식은 아래 링크를 참조하자!
https://devlog-wjdrbs96.tistory.com/166
[Spring] @Autowired란 무엇인가?
저번 글에서 IoC 컨테이너와 빈(Bean)등록에 대해서 정리해보았다. 다시 정리하자면 의존성 주입과 빈 등록은 다른 것인데 일단 IoC 컨테이너에 빈으로 등록이 되어야 의존성 주입을 할 수 있다. 저
devlog-wjdrbs96.tistory.com
6) 자바 코드로 직접 스프링 빈 등록하기
@Configuration public class SpringConfig { @Bean public MemberService memberService() { return new MemberService(memberRepository()); } @Bean public MemberRepository memberRepository() { return new MemoryMemberRepository(); } }
- 직접 등록을 하게되면 등록시에 자기 입맛대로 설정을 바꿀 수 있다는 장점이 있다.
7) MVC 간단 패턴으로 완료하기
- 홈 화면 추가
@Controller public class HomeController { @GetMapping("/") public String home() { return "home"; } }
*home.html <!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <div class="container"> <div> <h1>Hello Spring</h1> <p>회원 기능</p> <p> <a href="/members/new">회원 가입</a> <a href="/members">회원 목록</a> </p> </div> </div> </body> </html>
항상 스캔되는 첫 부분은 스프링 컨테이너이다 static 이하가 아니다 - 회원 등록
@Controller public class MemberController { ... @GetMapping("/members/new") public String createForm() { return "members/createMemberForm"; } }
*createMemberFrom.html <!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <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> </body> </html>
- 데이터를 받을 폼 생성
public class MemberForm { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
--> html에서의 name 값과 매칭 시키는 것
- 직접 데이터를 들고 컨트롤이 수행되는 @PostMapping
@Controller public class MemberController { ... @PostMapping("/members/new") public String create(MemberForm form) { Member member = new Member(); member.setName(form.getName()); System.out.println("member.setName = " + member.getName()); memberService.join(member); return "redirect:/"; } }
- GetMapping은 단순 이동, PostMapping은 무언가를 들고 이동
- 데이터를 받았으니 그것을 기반으로 create()가 실행된다. Member 클래스가 객체화 되고 이 객체에 쓰일 name을 MemberForm을 통해 받는다. 그 후 서비스에 저장(join)한다.
- 받아오는 데이터가 잘 나온다
- 회원 조회
@GetMapping("/members") public String list(Model model) { List<Member> members = memberService.findMembers(); model.addAttribute("members", members); return "members/memberList"; }
*memberList.html <!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <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> </body> </html>
- 컨트롤러에서 단순 이동을 하는데 그 이동시에 리스트 형태로 모든것을 담는다
- 리스트 형태로 담긴 members를 .addAttribute로 키와 밸류로 저장한다
- 그리고 html에서 타임리프 태그를 통해 member의 밸류값을 불러오는것
'스프링 > 스프링부트+JPA - 블로그' 카테고리의 다른 글
스프링부트+JPA 블로그 프로젝트 04 부트와 JPA의 필수 개념 (JSON/영속성/어노테이션 그리고 CRUD 테스트) (0) 2021.08.28 스프링부트+JPA 블로그 프로젝트 03 DB세팅, 모델링, 연관관계 (0) 2021.08.27 스프링부트+JPA 블로그 프로젝트 02 환경 설정 (0) 2021.08.27 스프링부트+JPA 블로그 프로젝트 01 스프링 기본 개념 (0) 2021.08.27 스프링입문 - 환경설정 및 개발기초 (0) 2021.08.20