Spring FrameWork(인프런 강의 중심) - 서지훈

JI HUN SEO·2022년 7월 4일
0

Bit

목록 보기
2/2

1. 스프링 프레임워크란?

스프링 프레임워크(영어: Spring Framework)는 자바 플랫폼을 위한 오픈 소스애플리케이션 프레임워크로서 간단히 스프링
(Spring)이라고도 한다. 동적인 웹 사이트를 개발하기 위한 여러 가지 서비스를 제공하고 있다. 대한민국 공공기관
의 웹 서비스 개발 시 사용을 권장하고 있는 전자정부 표준프레임워크의 기반 기술로서 쓰이고 있다.

2. 수강 강의명 및 프로젝트 설정

2.1. 수강 강의명

  • 스프링 입문 - 코드로 배우는 스프링 부트, 웹MVC, DB접근 기술(김영한)
  • 수강 기한: 무제한
  • 시간: 321분

2.2. 기본 스펙

  • Java : 11버전
  • IDE : Intellij
  • 스프링 부트 스타터 사이트에서 프로젝트 생성(https://start.spring.io/)

2.3. 스프링 부트 스타터

  • Gradle Project를 사용한 이유 → Maven에서 Gradle로 넘어가는 추세라서 Gradle 사용

  • repositories는 mavenCentral()이라는 사이트에서 라이브러리를 다운 받아라는 뜻인데 다른 사이트로 변경가능하다.

java 기본값이 17인데 실수로 11로 변경하는 것을 놓쳐서 나중에 기본 예제를 실행하는데 계속 에러가 나서 고생하다 나중에 버전 문제인 것을 알고 다시 프로젝트를 GENERATE했다.


src/main/java/hello/hellospring/HelloSpringApplication을 실행하면


이렇게 뜨면 성공한 것이다.

2.4. 라이브러리 살펴보기

Gradle, Maven은 의존성 관리를 해주어서 의존관계가 있는 라이브러리를 함께 다운로드한다.

즉, 특정 라이브러리를 사용하기 위해 필요한 다른 라이브러리를 같이 다운 받는다는 뜻이다.

2.5. Welcome Page 만들기

  • 서버를 껐다가 다시 키면 성공적으로 페이지가 나오는 모습을 확인할 수 있다.
  • 스프링 부트가 제공하는 Welcome Page 기능
  • static/index.html 을 올려두면 Welcome page 기능을 제공한다.

2.6. Thymeleaf 템플릿엔진 동작 확인

  • Controller(helloController)에서 리턴 값으로 문자를 반환하면 viewResolver가 화면을 찾아서 처리한다.
    스프링 부트 템플릿엔진 기본 viewName(hello) 매핑
    resources: templates/ +{ViewName}+ .html

2.7 Maven vs Gradle 간단비교

Maven

  • 아파치 메이븐은 자바용 프로젝트 관리 도구이다.
  • 아파치 Ant의 대안으로 만들어졌다.
  • 아파치 라이센스로 배포되는 오픈 소스 소프트웨어이다.
프로젝트를 진행하면서 사용하는 수많은 라이브러리들을 관리해주는 도구입니다.
여기서 메이븐의 특징적인 점은 그 라이브러리들과 연관된 라이브러리들까지 거미줄처럼 모두 연동이 되서 관리가 된다는 점입니다.
즉, 메이븐은 네트워크를 통해 연관된 라이브러리까지 같이 업데이트를 해주기 때문에  사용이 편리합니다.

Gradle

  • 빌드, 프로젝트 구성/관리, 테스트, 배포 도구
  • 안드로이드 앱의 공식 빌드 시스템빌드 속도가 Maven에 비해 10~100배 가량 빠름
  • JAVA, C/C++M Python 등을 지원
  • 빌트툴인 Ant Builder와 Groovy 스크립트 기반으로 만들어져 기존 Ant의 역할과 배포 스크립트의 기능을 모두 사용 가능
1. 스크립트 길이와 가독성 면에서 gradle이 우세하다.
2. 빌드와 테스트 실행 결과 gradle이 더 빠르다. (gradle은 캐시를 사용하기 때문에 테스트 반복 시 차이가 더 커진다.)
3. 의존성이 늘어날 수록 성능과 스크립트 품질의 차이가 심해질 것이다.
 
maven은 프로젝트가 커질수록 빌드 스크립트의 내용이 길어지고 가독성이 떨어집니다.
반면에 gradle은 훨씬 적은 양의 스크립트로 짧고 간결하게 작성할 수 있습니다.
 
maven이 정적인 형태의 XML 기반으로 작성되어 동적인 빌드를 적용할 경우 어려움이 많다면,
gradle은 Groovy를 사용하기 때문에 동적인 빌드는 Groovy 스크립트로 플러그인을 호출하거나 직접 코드를 짜면 됩니다.
출처: https://dev-coco.tistory.com/65 [슬기로운 개발생활:티스토리]

출처: https://dev-coco.tistory.com/65 [슬기로운 개발생활:티스토리]

2.8 템플릿 엔진이란?

템플릿 양식(html)과 데이터 모델(DB)에 따른 입력 자료를 결합해서 문서를 출력하는 소프트웨어를 템플릿 엔진이라고 한다.

  • 즉, View를 담당하는 html코드와 DB Logic Code를 따로 분리해서 합쳐주는 기능을 하는 것이다.

그리고 두가지의 종류로 나뉘는데

  • 하나는 서버사이드 템플릿 엔진,
  • 하나는 클라이언트사이드 템플릿 엔진이다.

서버사이드 템플릿 엔진이란?

서버에서 가져온 데이터를 미리 만들어진 템플릿에 넣어서 html을 완성시키고 클라이언트에게 전달한다.

클라이언트 사이드 템플릿 엔진이란?

브라우저 위에서 html 형태로 화면을 생성하고, 서버에서 받은 json, xml형식의 데이터를

여기서 동적인 화면으로 만드는 것이며 예로는 React나 Vue.js가 있다.

그리고 현직에서는 jsp를 많이 사용하지 않는다고 하는데 왜냐하면 스프링에서 자동 설정을 지원하지 않기 때문이라고 합니다!!

그렇다면, Spring에서 자동 설정을 지원하는 템플릿 엔진에는 무엇이 있을까?

FreeMarker, Groovy, Thymeleaf, Mustache가 있다.

그 중 가장 인기가 많은 Thymeleaf가 인기가 가장 많다고 합니다.

Thymeleaf는 스프링부트와 굉장히 궁합이 잘 맞기에 권장하는 템플릿 엔진인 것 같다. Thymeleaf를 사용하기 위해서는 의존성을 추가해주고 view에서 html 언어를 추가해서 문법을 적용시키면 됩니다.

3. 스프링 웹 개발 기초

3.1. 정적 컨텐츠 이미지

  • 웹 브라우저에서 localhost:8080/hello-static.html을 검색하면 제일 먼저 내장 톰켓 서버가 요청을 받고 스프링에 이 요청을 넘깁니다. 그러면 스프링은 컨트롤러쪽에서 먼저 hello-static이라는 컨트롤러를 찾아봅니다. 찾아보다 없으면 내부에 있는 static/hello-static.html을 찾아보고 있으면 반환해줍니다.

3.2. MVC와 템플릿 엔진

  • 웹 브라우저에서 localhost:8080/hello-mvc?name=spring을 검색하면 제일 먼저 내장 톰켓 서버가 요청을 받고 스프링에 이 요청을 넘깁니다. 그러면 스프링은 helloController에 매핑된 메소드(@GetMapping("hello-mvc"))를 확인하고 return 해줄 때 이름을 hello-template으로 하고 model의 name값을 spring으로 viewResolver로 넘겨줍니다. 그러면 viewResolver는 해당 html을 찾아 반영합니다.

3.3. API

  • API방식에서 @responsebody 를 사용한 경우 스프링에서는 viewResolver에서 맞는 템플릿을 찾아서 실행하는 동작을 하지 않고 Http에 이 응답을 그대로 넘기는 작업을 하게 됩니다. 여기서 리턴 값이 문자인 경우(StringConverter)와 객체인 경우(JsonConverter)가 달라집니다.
  • 단순 문자인 경우, StringConverter가 동작하여 그냥 문자를 넘겨주고,
  • 객체인 경우, JsonConverter가 동작하여 기본적으로 json방식으로 객체를 넘겨줍니다.

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

4.1. 비즈니스 요구사항 정리

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

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

  • 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계(가상의 시나리오 상, 아직 데이터 저장소가 선정이 되지 않았기 때문에, 그래도 개발은 진행이 되어야하므로 메모리모드로 단순하게 저장할 수 있는 구현체를 구성)
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

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

회원객체

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

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

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

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

회원 리포지토리 메모리 구현체 테스트

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;

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(result).isEqualTo(member);
    }

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

4.4 회원 서비스 개발 & 테스트

회원 서비스

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;
public class MemberService {

    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) {
        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 hello.hellospring.service;
import hello.hellospring.domain.Member;
import 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.*;
import static org.junit.jupiter.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
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    @Test
    public void 중복_회원_예외() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));
        //예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}
  • @BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.

0개의 댓글