scone-lemon

2022 01 08 토요일 공부 (김영한 스프링 입문 day2 : 스프링 웹 개발 기초, 회원 관리 예제 - 백엔드 개발) 본문

PROJECT/2학기 공통

2022 01 08 토요일 공부 (김영한 스프링 입문 day2 : 스프링 웹 개발 기초, 회원 관리 예제 - 백엔드 개발)

lemon-scone 2022. 1. 8. 13:40

1. 스프링 웹 개발 기초

스프링 웹 개발 기초
목표 : 디테일 하게 보다는 크게 어떻게 돌아가는지 이미지를 그리기

웹 개발의 크게 세가지 방식
- 정적 컨텐츠 방식 : welcome page 처럼 파일 웹 브라우저에 그대로 보여줌, 파일을 그대로 웹브라우저에 전달
- MVC 와 템플릿 엔진 방식 : jsp php -> template engine 동적으로 브라우저에 보여줌, 서버에서 변형을 해서 html을 바꿔서 웹 브라우저에 전달
- API 방식 : json이라는 데이터 포맷으로 클라이언트에게 데이터를 전달, 서버끼리 통신할 때 사용

정적 컨텐츠
- 스프링부트는 정적 컨텐츠 기능을 자동으로 제공함
- resources/static/ 에 위치
- 웹브라우저 --> 내장 톰켓 서버 --> 스프링부트 (컨트롤러에서 먼저 찾기, 없으면 static에서 찾기) --> 반환
- ===> resources/static/ 에 위치한 html 정적파일을 그대로 웹 브라우저에 내려준다

김영한 선생님 스프링 입문 강의자료 중 13페이지
resources/static/hello-static.html
http://localhost:8080/hello-static.html


MVC와 템플릿 엔진
- MVC : model view controller
- 예전에는 view와 controller 가 jsp 형태로 합쳐져 있었으나,
이후에는 기능을 나누어서 view은 저스트 화면을 그리는데 모든 역량을 집중하고,
controller는 비즈니스 로직에 관련이 있거나 서버 뒷단 내부적인 처리에 모든 역량을 집중
(model에 화면에서 필요한 것들을 담아서 화면으로 넘겨줌)
- org.thymeleaf.exceptions.TemplateInputException 에러 발생해서 실습을 따라하지 못함
- ===> 템플릿 엔진을 m v c 방식으로 쪼개서 뷰를 템플릿 엔진으로 랜더링 한 html을 고객에게 전달
(정적컨텐츠를 제외하면 mvc방식과 api방식만 생각하면 된다 :
이걸 웹 브라우저에서 html로 보여줄건가 혹은 api 방식으로 데이터를 바로 보여줄건가)

김영한 선생님 스프링 입문 강의자료 중 14페이지
HelloController.java helloMvc
resources/templates/hello-templete.html
http://localhost:8080/hello-mvc?name=spring


API
- localhost:8080/hello --> 정적 컨텐츠 (웹 브라우저에 html 문서 내용 그대로 뿌림)
- localhost:8080/hello-mvc --> 500 error (Server error)
- localhost:8080/hello-string --> (String 은 StringConverter)
- localhost:8080/hello-api --> json 으로 반환 (객체는 JsonConverter)
- ===> 객체를 반환하는 역할을 주로 함 !!! json으로 반환, view를 거치지 않음

김영한 선생님 스프링 입문 강의자료 중 16페이지
HelloController.java helloString, helloApi
http://localhost:8080/hello-string?name=spring
http://localhost:8080/hello-api?name=spring


[API 방식 세부내용] @RespnseBody 사용 원리 1
- http : header부, body부 --> 응답 body 부에 직접 데이터를 넣어주겠다는 뜻
- @ResponseBody를 쓸 경우, json으로 데이터를 반환하는게 기본
- 웹브라우저 --> url 호출 --> 내장 톰캣 서버 --> helloController
--> @ResponseBody (http 응답에 데이터를 그대로 옮겨야 되겠구나)
--> 객체면 (객체네...? default : json 방식으로 데이터를 만들어서 http 응답에 반환하겠다)
--> HttpMessageConverter 동작 (mvc 경우 ViewResolver 호출? 동작?)
--> (객체의 경우) JsonConverter 동작 (객체가 아니면 : StringConverter)

[API 방식 세부내용] @RespnseBody 사용 원리 2
- HTTP의 BODY에 문자 내용을 직접 반환
- viewResolver 대신에 HttpMessageConverter가 동작
- 기본 문자처리 : StringHttpMessageConverter
- 기본 객체처리 : MappingJackson2HttpMessageConverter (Jackson2 : json을 처리해주는 라이브러리)
- byte처리 등등 여러 HttpMessageConverter가 기본으로 등록되어 있음
- 클라이언트의 HTTP Accept 헤더(accept 하고싶은 데이터 형태를 알려줌)와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter가 선택된다


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

비즈니스 요구사항 정리
- 데이터 : 회원아이디, 이름
- 기능 : 회원 등록, 회원 조회
- 아직 데이터 저장소가 선정되지 않음 (가상의 시나리오)

* 일반적인 웹 어플리케이션 계층 구조
- 컨트롤러 : 웹 mvc의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

* [내맘대로 컨텐츠] sts structure 리뷰
- controller --> 컨트롤러
- model --> dto 객체 만들어줌
- model.mapper --> interface...? 뭐하는건지 모르겠다 (resources/mapper 아래에 있는 쿼리문들과 관련있는 것 같다)
- model.service --> interface, implement...? 뭐하는건지 모르겠다

* MemberRepository가 interface인 이유
- 아직 데이터 저장소가 선정되지 않아서 (요구사항 정리부분 가상의 시나리오) 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, JPA 등등 다양한 저장소를 고민중인 상황으로 가정
--> 향후 구체적인 데이터 저장 기술이 선정되면 바꿔끼울건데 바꿔끼울거면 인터페이스가 필요하다
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

회원 도메인과 리포지토리 만들기
- hellospring.controller
- hellospring.domain --> Member 객체 생성
- hellospring.repository --> 구현할 기능 interface에 선언, implements class에 기능 세부적으로 구현 (구현체)

package hello.hellospring.domain;

public class Member {
    // 데이터를 구분하기 위해서 시스템이 마음대로 저장하는 id
    private Long id;
    // 사용자 이름
    private String name;

    // Getter, Setter
    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 hello.hellospring.repository;

import 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(); // 전체 회원정보 리스트로 반환

    // Optional : 
    // 가져오는 데이터가 null 이여서 null 을 반환할 때
    // 요즘에는 null 을 그대로 반환하기 보다 Optional 로 감싸서 반환
}
package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {

    // 메모리를 저장
    private static Map<Long, Member> store = new HashMap<>();
    // key 값을 생성
    private static long sequence = 0L;

    @Override
    // store 에 데이터를 save 해주는 기능 구현
    public Member save(Member member) {
        // sequence 값을 올려줌
        member.setId(++sequence);
        // 데이터를 store 에 넣어줌
        store.put(member.getId(), member);
        return member;
    }

    @Override
    // store 에서 꺼내는 기능 구현
    public Optional<Member> findById(Long id) {
        // null 이 반환 될 가능성이 있으면 Optional.ofNullable 로 감싸준다
        return Optional.ofNullable(store.get(id));
    }

    @Override
    // store 에서 꺼내는 기능 구현
    public Optional<Member> findByName(String name) {
        // stream = 루프로 돌린다
        return store.values().stream()
                // member.getName() 이 파라미터로 넘어온 name 과 같은지 확인
                // 같은 경우에만 필터링
                .filter(member -> member.getName().equals(name))
                // 하나라도 찾아서 반환
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

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


회원 리포지토리 테스트 케이스 작성
- MemoryMemberRepositoryTest.java
- @AfterEach MemoryMemberRepositoryTest.afterEach
--> 테스트가 끝날 때마다 리포지토리를 깨끗하게 비워주는 기능

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
//import org.junit.jupiter.api.Assertions;
import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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

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();

        // method 1
        System.out.println("(result == member) = " + (result == member));
        // method 2 (Assertions 기능 사용)
        // import org.junit.jupiter.api.Assertions;
        // Assertions.assertEquals(member,result);
        // method 3 (org.assertj.core.api.Assertions 기능 사용)
        // member 가 result 랑 똑같아! 라는 의미전달이 잘 된다...
        // import org.assertj.core.api.*; 해주면 용이하게 사용 가능
        //org.assertj.core.api.Assertions.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);

        // get 으로 받으면 Option 을 한번 까서 받을 수있다(...?몬말이얌)
        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);
    }
}


회원 서비스 개발
- MemberService.java

package hello.hellospring.service;

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

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

// service : 비즈니스에 의존적으로 네이밍
// repository : 개발스럽게 네이밍
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){
        // 같은 이름을 가진 중복 회원 X

        // method 1
        // result 를 Optional 으로 반환
        //Optional<Member> result = memberRepository.findByName(member.getName());
        // isPresent = result 가 값이 있으면 (null 이 아니면)
        //result.ifPresent(m -> {
        //    throw new IllegalStateException("이미 존재하는 회원입니다.");
        //});

        // method 2
        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);
    }
}


회원 서비스 테스트
- ctrl + shift + T
- MemberService --> Singleton
- MemberServiceTest --> Dependency Injection(DI)

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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 static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    //MemberService memberService = new MemberService();
    //MemoryMemberRepository memberRepository = new MemoryMemberRepository();

    // Dependency Injection (DI)
    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();
        Assertions.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);
        // assertThrows() 를 사용한다는데 아몰랑
        try{
            memberService.join(member2);
            fail("예외가 발생해야 합니다");
        } catch (IllegalStateException e){
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }

        // then (결과가 이게 나와야 해)


    }

    @Test
    void findMembers() {
        // given (주어진 상황)


        // when (이걸 실행 했을 때)


        // then (결과가 이게 나와야 해)


    }

    @Test
    void findOne() {
        // given (주어진 상황)


        // when (이걸 실행 했을 때)


        // then (결과가 이게 나와야 해)


    }
}