1. 스프링 부트 스타터 사이트로 스프링 프로젝트 생성
- https://start.spring.io 에서 스프링부트 프로젝트 생성 가능
- project : Gradle
- Language : Java
- Spring Boot : 2.7.3
- project Metadata
- Group : hello
- Artifact : hello-spring
- Dependencies : Spring Web, Thymeleaf
스프링부트 프로젝트
- main함수 실행 -> Tomcat started on port(s): 8080 (http) with context path '' 참고
- 웹 페이지에서 localhost:8080 입력
2. 스프링부트 라이브러리 살펴보기
- VSCode에선 왼쪽 칸에 'Gradle' 이란 코끼리 모양 클릭
- System.out.println 말고 log 형식으로 남기도록..
- Gradle : 의존관계가 있는 라이브러리를 함께 다운로드함
스프링부트 라이브러리
- spring-boot-starter-web
- spring-boot-starter-tomcat : 톰캣(웹서버)
- spring-webmvc : 스프링 웹 MVC
- spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진(View)
- spring-boot-starter(공통) : 스프링 부트 + 스프링 코어 + 로깅
- spring-boot
- spring-boot-starter-logging
테스트 라이브러리
- spring-boot-starter-test
- junit : 테스트 프레임워크( junit5 를 자주 씀 )
- mockito : 목 라이브러리
- assertj : 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
- spring-test : 스프링 통합 테스트 지원
3. View 환경 설정
Welcom page 만들기
- static/index.html 을 올려두면 스프링 부트가 제공하는 Welcome page 기능을 제공
- src - main - resources - static - 파일 생성( index.html )
- 스프링 부트는 index.html 파일을 먼저 찾음
- 아래 내용 작성
<!DOCTYPE HTML>
<html>
<head>
<title>Hello</title>
<meta http-equiv = "Content-Type" content="test/html"; charset=UTF-8" />
</head>
<body>
Hello
<a href = "/hello">hello</a>
</body>
</html><!DOCTYPE HTML>
<html>
<head>
<title>Hello</title>
<meta http-equiv = "Content-Type" content="test/html"; charset=UTF-8" />
</head>
<body>
Hello
<a href = "/hello">hello</a>
</body>
</html>
thymeleaf 템플릿 엔진
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model) {
model.addAttribute("data", "hello!!");
return "hello";
}
}
- Model
- @Controller
- @GetMapping
- resources - templates - hello.html 생성
<!DOCTYPE HTML>
<html xmlns=th="http://www.thymeleaf.org">
<html>
<head>
<title>Hello</title>
<meta http-equiv = "Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>
- 컨트롤러에서 리턴 값으로 문자를 반환하면 viewResolver 가 화면을 찾아서 처리함
- 스프링 부트 템플릿엔진 기본 viewName 매핑
- 'resource:templates/' + {Viewname} + .'html'
4. 빌드하고 실행하기
window에서 cmd창으로 springBoot 빌드
- cmd창 오픈 -> cd (gradle.bat 파일 있는 위치로 이동)
- 빌드 : gradlew build 명령어 통해 빌드됨
- build 했던 폴더 삭제 : gradlew clean
- build 했던 폴더 삭제하고 다시 빌드 : gradlew clean build
- 실행 : java -jar (프로젝트명)-0.0.1-SNAPSHOT.jar 명령어 통해 실행됨
- 실행 종료는 Ctrl + C 입력
5. 정적 컨텐츠
- 파일을 그대로 웹브라우저에 보여줌
- 스프링 부트는 정적 컨텐츠 지원함
- resource - static 에 'hello-static.html' 파일 생성
<!DOCTYPE HTML>
<html>
<head>
<title>static content</title>
<meta http-equiv = "Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>
6. MVC와 템플릿 엔진
- MVC : Model, View, Controller
- controller -> HelloController.java 에 아래 내용 추가
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
- resources -> templates -> hello-template.html 생성
<html xmlns=th="http://www.thymeleaf.org">
<body>
<p th:text="'hello. ' + ${name}" >hello! empty</p>
</body>
</html>
7. API
@ResponseBody
- @ResponseBody 사용하면 viewResolver 사용 안함
- viewResolver 대신 HttpMessageConverter 동작
- 기본 문자처리 : StringHttpMessageConverter
- 기본 객체처리 : MappingJackson2HttpMessageConverter
- byte 처리 등등 기타 여러 HttpMessageConverter 가 기본으로 등록되어 있음
- 대신 HTTP의 BODY에 문자 내용을 직접 반환( HTML BODY TAG를 말하는 것 아님 )
예제 1
- controller -> HelloController.java 에 아래 내용 추가
import org.springframework.web.bind.annotation.ResponseBody;
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
- @ResponseBody
- @RequestParam
예제 2
- controller -> HelloController.java 에 아래 내용 추가
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
8. 회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음( 가상의 시나리오 )
일반적인 웹 애플리케이션 계층 구조
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체
- 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장되고 관리됨
클래스 의존관계
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인, 레포지토리
도메인
- hellospring -> domain 폴더 생성( pcakage 생성 )
- domain 폴더 안에 Member.java 생성
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;
}
}
레포지토리
- hellospring -> repository 폴더 생성( pcakage 생성 )
- repository 폴더 안에 MemberRepository.java 생성 -> interface로 선언
package hello.hellospring.repository;
import java.util.List;
import java.util.Optional;
import hello.hellospring.domain.Member;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
- Optional : Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스(Wrapper class)
- 복잡한 조건문 없이도 널(null) 값으로 인해 발생하는 예외를 처리할 수 있음
- interface
- repository폴더 안에 MemoryMemberRepository.java 생성 -> MemberRepository.java를 implements 함
package hello.hellospring.repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import hello.hellospring.domain.Member;
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());
}
}
회원 리포지토리 테스트 케이스 작성
- 자바는 JUnit 이라는 프레임워크로 테스트 실행
회원 리포지토리 테스트 코드 작성
- src -> test -> java -> hello -> hellospring 안에 repository 패키지(폴더) 생성
- repository 패키지 안에 MemoryMemberRepositoryTest.java 생성
package hello.hellospring.repository;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import hello.hellospring.domain.Member;
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();
Assertions.assertThat(member).isEqualTo(result);
}
@Test
public void fineByName() {
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();
Assertions.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();
Assertions.assertThat(result.size()).isEqualTo(2);
}
}
- MemoryMemberRepository.java 추가
public void clearStore() {
store.clear();
}
회원 서비스 개발
서비스 로직
- main -> jave/hello/hellospring -> service 패키지(폴더) 생성
- MemberService.java 생성
package hello.hellospring.service;
import java.util.Optional;
import java.util.List;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
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())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원 서비스 테스트 코드
- src -> test -> java -> hello -> hellospring 안에 service 패키지(폴더) 생성
- service 패키지 안에 MemberServiceTest.java 생성
package hello.hellospring.service;
import static org.junit.jupiter.api.Assertions.fail;
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 hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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 회원가입() {
Member member = new Member();
member.setName("hello");
Long saveId = memberService.join(member);
Member findMember = memberService.findOne(saveId).get();
Assertions.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) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
@Test
public void findMembers() {
}
@Test
public void findOne() {
}
}
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}