Merge branch 'master' of https://github.com/banjjoknim/TIL
@@ -9,7 +9,7 @@
|
||||
- [codingTest](https://github.com/banjjoknim/codingTest)
|
||||
|
||||
## 강의
|
||||
- [김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술](https://github.com/banjjoknim/spring-introduction/blob/master/Lectures/Contents.md)
|
||||
- [김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Contents.md)
|
||||
|
||||
## Git-GitHub
|
||||
- [직접 만든 Git-GitHub 강의 자료](https://github.com/banjjoknim/RectureForGit/blob/master/README.md)
|
||||
|
||||
37
SpringBoot-Introduction/.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
40
SpringBoot-Introduction/Lectures/Contents.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Contents
|
||||
|
||||
## 프로젝트 환경설정
|
||||
- [프로젝트 생성](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture01.md)
|
||||
- [라이브러리 살펴보기](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture02.md)
|
||||
- [View 환경설정](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture03.md)
|
||||
- [빌드하고 실행하기](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture04.md)
|
||||
|
||||
## 스프링 웹 개발 기초
|
||||
- [정적 컨텐츠](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture05.md)
|
||||
- [MVC와 템플릿 엔진](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture06.md)
|
||||
- [API](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture07.md)
|
||||
|
||||
## 회원 관리 예제 - 백엔드 개발
|
||||
- [비즈니스 요구사항 정리](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture08.md)
|
||||
- [회원 도메인과 리포지토리 만들기](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture09.md)
|
||||
- [회원 리포지토리 테스트 케이스 작성](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture10.md)
|
||||
- [회원 서비스 개발](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture11.md)
|
||||
- [회원 서비스 테스트](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture12.md)
|
||||
|
||||
## 스프링 빈과 의존관계
|
||||
- [컴포넌트 스캔과 자동 의존관계 설정](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture13.md)
|
||||
- [자바 코드로 직접 스프링 빈 등록하기](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture14.md)
|
||||
|
||||
## 회원 관리 예제 - 웹 MVC 개발
|
||||
- [회원 웹 기능 - 홈 화면 추가](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture15.md)
|
||||
- [회원 웹 기능 - 등록](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture16.md)
|
||||
- [회원 웹 기능 - 조회](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture17.md)
|
||||
|
||||
## 스프링 DB 접근 기술
|
||||
- [H2 데이터베이스 설치](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture18.md)
|
||||
- [순수 JDBC](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture19.md)
|
||||
- [스프링 통합 테스트](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture20.md)
|
||||
- [스프링 JdbcTemplate](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture21.md)
|
||||
- [JPA](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture22.md)
|
||||
- [스프링 데이터 JPA](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture23.md)
|
||||
|
||||
## AOP
|
||||
- [AOP가 필요한 상황](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture24.md)
|
||||
- [AOP 적용](https://github.com/banjjoknim/TIL/blob/master/SpringBoot-Introduction/Lectures/Lecture25.md)
|
||||
95
SpringBoot-Introduction/Lectures/Lecture01.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 프로젝트 생성
|
||||
|
||||
---
|
||||
|
||||
## spring initializr
|
||||
- [spring initializr](https://start.spring.io/)에서 스프링부트 기반으로 스프링 프로젝트를 만들어주는 사이트이며, 스프링이 운영하고 있다.
|
||||
|
||||
---
|
||||
|
||||
## Maven, Gradle?
|
||||
- 필요한 라이브러리를 가져오고 빌드하는 라이프사이클까지 관리해주는 툴이다.
|
||||
- 과거에는 `Maven`을 많이 사용했으나 요즘 추세는 `Gradle`로 넘어가는 추세이다.
|
||||
- 레거시 프로젝트나 과거 프로젝트는 `Maven`으로 남아있는 경우가 있다.
|
||||
- 심지어 스프링 라이브러리 관리 자체도 요즘에는 `Gradle`로 넘어온 상황이다.
|
||||
- 따라서 `Gradle`을 추천한다.
|
||||
|
||||
---
|
||||
|
||||
## Language
|
||||
- Java
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot
|
||||
- `SNAPSHOT`은 아직 만들고 있는 버전이다
|
||||
- `M1`은 정식 릴리즈된 버전이 아니다.
|
||||
- 강의상으로는 `2.3.1` 버전을 선택했으나 버전이 바뀌었을 때의 대처는 강의교재를 참고하자.
|
||||
|
||||
---
|
||||
|
||||
## Project Metadata
|
||||
- `Group`에 보통 기업 도메인명 같은 것들을 적어준다.
|
||||
- `Artifact`는 빌드되어 나올 때의 결과물이라고 보면 된다(프로젝트명 같은 것).
|
||||
- 나머지(`Name`, `Description` 등)는 그대로 둔다.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
- 어떤 라이브러리를 가져와서 사용할 것인지를 선택하는 것.
|
||||
- `Spring Web`, HTML을 만들어주는 템플릿 엔진이 필요한데 이것으로 `Thymeleaf`선택(회사마다 다름).
|
||||
|
||||
---
|
||||
|
||||
## GENERATE
|
||||
- 아래의 `GENERATE` 버튼 누르면 `zip` 파일이 다운로드 되는데 이 것을 압축 푼 다음 인텔리제이에서 `Open or import`로 `build.gradle`을 열면된다.
|
||||
- `Open as Project` 선택.
|
||||
|
||||
---
|
||||
|
||||
## 프로젝트 내부 구조
|
||||
|
||||
### `.gradle`
|
||||
- 무시..
|
||||
|
||||
### `.idea`
|
||||
- 인텔리제이가 사용하는 설정파일이다.
|
||||
|
||||
### `gradle` -> `wrapper`
|
||||
- `gradle`과 관련해서 `gradle`을 사용하는 폴더이다.
|
||||
|
||||
### `src` -> `main`, `test`
|
||||
- `main` : `java`(하위에 실제 패키지와 소스파일 존재), `resources`(실제 자바 코드파일을 제외한 `xml`이나 `properties`, `html`등의 설정 파일 등이 존재 -> 자바 파일을 제외한 나머지는 `resources`라 보면됨)
|
||||
|
||||
- `test` : `java`(테스트 코드들과 관련된 소스들이 존재 -> 테스트 코드라는 것이 요즘 개발 트랜드에서는 중요하다는 뜻이다)
|
||||
### `build.gradle`
|
||||
- 중요!! 예전에는 한땀한땀 입력해야 했으나... 앞서 `start.spring.io`에서 선택한 설정들이 지정되어 있는 파일이다.
|
||||
- 버전 설정하고 라이브러리를 가져오는구나.. 정도로 이해하면 된다.
|
||||
|
||||
### `dependencies`
|
||||
- 앞서 설정한 라이브러리들이 입력되어 있고, 여기에 입력하면 해당 라이브러리를 사용할 수 있다.
|
||||
- 요즘에는 기본적으로 테스트 라이브러리가 들어가는데 `JUnit5`가 기본적으로 들어간다.
|
||||
|
||||
### `repositories`
|
||||
- 라이브러리를 사용하기 위해서는 다운로드를 받아야하는데, 어디서(입력된 사이트에서) 다운로드 받을지 설정을 해놓는 항목이다.
|
||||
|
||||
### `.gitignore`
|
||||
- 소스코드를 관리해준다.
|
||||
- `git`에는 필요한 소스코드 파일만 올라가야 하며 빌드된 결과물등은 올라가면 안된다.
|
||||
- 기본적으로 어느정도는 `start.spring.io`에서 다 해준다.
|
||||
|
||||
### `gradlew`, `gradlew.bat`
|
||||
- `gradle`로 빌드할 때 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## Application
|
||||
- `springboot`는 톰캣 웹 서버를 내장하고 있다.
|
||||
- 그래서 `@SpringBootApplication` 어노테이션이 붙어있는 클래스에서 `main` 메서드 내에 `SpringApplication.run()`의 인자로 해당 클래스와 `main`메서드의 `args`를 넣고 실행하면 내장 톰캣 웹 서버를 자체적으로 띄우면서 스프링부트가 같이 올라온다(?).
|
||||
|
||||
---
|
||||
|
||||
## 번외
|
||||
- 인텔리제이를 사용하면 빌드가 자바를 직접 실행하는 것이 아니라 `gradle`을 통해 실행될때가 있다.
|
||||
- 이럴 때는 설정의 `Build, Execution, Deployment`에서 `Build and run using` 항목과 `Run tests using` 항목을 `IntelliJ IDEA`로 바꿔준다.
|
||||
- `gradle`을 통해서 실행하면 느릴 때가 있는데, 이렇게 설정해주면 인텔리제이에서 `gradle`을 통하지 않고 바로 자바를 띄워서 실행하므로 훨씬 빠르다.
|
||||
51
SpringBoot-Introduction/Lectures/Lecture02.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 라이브러리 살펴보기
|
||||
- `build.gradle`의 `dependencies`에서 확인할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## `External Libraries`
|
||||
- 프로젝트 내부에 에서 현재 다운로드된 라이브러리들을 확인할 수 있다(매우 많다!).
|
||||
|
||||
---
|
||||
|
||||
## `Maven`, `Gradle`과 같은 빌드 툴
|
||||
- 의존관계를 관리해준다.
|
||||
- `spring-boot-starter-web` 라이브러리를 가져오면 `spring-boot-starter-tomcat` 등과 같이 추가로 필요한(의존관계가 있는) 라이브러리를 같이 가져온다.
|
||||
|
||||
---
|
||||
|
||||
## Gradle(우측 세로 메뉴)
|
||||
- 현재 가져온 라이브러리들을 확인할 수 있다.
|
||||
- 라이브러리들을 보다보면 `(*)` 표시를 볼 수 있는데, 이미 다른 라이브러리에서 가져온 라이브러리라는 의미로 중복을 제거한 것이다.
|
||||
|
||||
---
|
||||
|
||||
## `spring-boot-starter-tomcat`
|
||||
- 고대에는 톰캣과 같은 웹서버(WAS)를 직접 설치해놓고 자바 코드를 밀어넣는 식 사용하여 웹서버와 개발 라이브러리가 분리되어 있있다.
|
||||
- 최근에는 소스 라이브러리에서 웹서버를 내장하고(임베디드) 있다.
|
||||
- 그래서 실행만 해도 웹서버가 나온다(따라서 설정이 필요가 없다).
|
||||
|
||||
---
|
||||
|
||||
## `spring-boot-starter`
|
||||
- 스프링부트와 관련된 프로젝트를 사용하면 웬만해서는 의존관계가 다 가져와지므로 직접 가져오지 않아도 괜찮다.
|
||||
|
||||
스프링부트와 관계된 라이브러리를 쓰면 `spring-core`까지 다 가져와서 스프링 관련된 세팅이 되면서 프로그램이 돌아간다고 이해하자.
|
||||
|
||||
---
|
||||
|
||||
## `spring-boot-starter-logging`
|
||||
- 현업에서는 로그로 출력을 해야한다.
|
||||
- 로그로 남겨야 심각한 에러를 모아보거나 로그 파일들의 관리가 가능하기 때문이다.
|
||||
- `slf4j`, `logback`의 두 라이브러리를 포함하고 있다.
|
||||
- 로그에 대해 궁금하다면 위의 두 가지 라이브러리를 공부해보자.
|
||||
|
||||
---
|
||||
|
||||
## `spring-boot-starter-test`
|
||||
- 자바진영에서는 `JUnit`이라는 라이브러리를 사용한다.
|
||||
- `mockito`, `assertj` 등 테스트를 편리하게 하도록 도와주는 라이브러리들이 포함되어 있다.
|
||||
- `spring-test`
|
||||
- 스프링과 통합해서 테스트할 수 있도록 해주는 라이브러리이다.
|
||||
|
||||
---
|
||||
62
SpringBoot-Introduction/Lectures/Lecture03.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# View 환경설정
|
||||
|
||||
---
|
||||
|
||||
## Welcome Page 만들기
|
||||
- `Welcome Page` : 도메인만 누르고 들어왔을 때 첫 화면이다.
|
||||
- `main/resources/static` 디렉토리 내에 `index.html` 파일을 만들면 `static/index.html`이 `Welcome Page`기능을 제공한다.
|
||||
|
||||
---
|
||||
|
||||
## spring
|
||||
- 스프링은 자바 엔터프라이즈 웹 애플리케이션 관계된 전반적인 생태계를 다 제공한다.
|
||||
- 따라서 필요한 것을 찾는 것이 중요하다.
|
||||
- [스프링 공식 홈페이지](spring.io)(`spring.io`) -> `Projects` -> `Spring Boot` -> `LEARN` -> `Reference Doc.` -> 필요한 정보에 대한 문서 검색(`index.html`을 검색해보자)
|
||||
|
||||
---
|
||||
|
||||
## 참고할 수 있는 링크
|
||||
- [thymeleaf 공식사이트](https://www.thymeleaf.org/)
|
||||
- [스프링 공식 튜토리얼](https://spring.io/guides/gs/serving-web-content/)
|
||||
- [스프링 부트 메뉴얼](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-spring-mvc-template-engines)
|
||||
|
||||
---
|
||||
|
||||
## Controller
|
||||
- `@Controller` 어노테이션을 명시해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## @GetMapping("경로")
|
||||
- 웹 어플리케이션에서 지정한 경로로 들어오면 해당 메서드를 호출한다.
|
||||
|
||||
---
|
||||
|
||||
## hello.html
|
||||
|
||||
- `th` : `thymeleaf`의 `th`이다.
|
||||
- `thymeleaf` 템플릿 엔진 : `<html xmlns:th="http://www.thymeleaf.org">`
|
||||
- `html` 내에 작성해주면 템플릿 엔진으로써 `thymeleaf` 문법을 사용할 수 있다.
|
||||
- `${data}` : `HelloController`에서 `model`의 키 값으로 준 `data`이고, 값은 마찬가지로 `model`의 `value`를 사용하여 치환되어 웹에서 표시된다.
|
||||
|
||||
---
|
||||
|
||||
## 동작 환경 그림
|
||||

|
||||
|
||||
- 웹 브라우저에서 `hello`로 접근하면 내장 톰캣 서버가 `HelloController`에 존재하는 `hello` url이 매핑되어 있는 `hello` 메서드를 찾아서 실행시킨다.
|
||||
- 이때 `model`에 키는 `data`, 값은 `hello!!`로 넣는다.
|
||||
- 그리고 `hello`(`resources`에 존재하는 `hello`)를 리턴한다.
|
||||
- 이것은 `resources/templates/hello`에 접근해서 랜더링(저 화면을 실행시켜라)하라는 의미이다.
|
||||
- 컨트롤러에서 리턴 값으로 문자를 반환하면 `뷰 리졸버(viewResolver)`가 화면을 찾아서 처리한다.
|
||||
- 스프링부트는 템플릿엔진은 기본적으로 `viewName` 매핑을 한다.
|
||||
- `resources/templates/` + `ViewName` + `.html`과 매핑시켜준다.
|
||||
- 결과적으로, `resources/templates/hello.html`이 열리게 된다.
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
- `spring-boot-devtools` 라이브러리를 추가하면 `html` 파일을 컴파일만 해주면 서버 재시작 없이 `View` 파일 변경이 가능하다.
|
||||
- 인텔리제이 컴파일 방법: 메뉴 `build` -> `Recompile`
|
||||
|
||||
---
|
||||
31
SpringBoot-Introduction/Lectures/Lecture04.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 빌드하고 실행하기
|
||||
- 빌드해서 실행할 수 있는 파일을 만들어본다.
|
||||
- 사실 인텔리제이에서만 해도 되지만, 실제 개발을 처음 하는 분들은 서버에서 빌드할 때 정말 어떻게 해야하는지를 잘 모르는 경우가 많다(서버에서는 cmd만 사용할 수 있다).
|
||||
- 실무에서 개발을 하려면 cmd만으로 빌드를 해야할 때도 많다.
|
||||
|
||||
## Mac 사용자일 경우
|
||||
- 콘솔로 이동
|
||||
- `./gradlew build`
|
||||
- `cd build/libs`
|
||||
- `java -jar hello-spring-0.0.1-SNAPSHOT.jar`
|
||||
- 실행 확인
|
||||
|
||||
## 윈도우 사용자일 경우
|
||||
- 콘솔로 이동 대신 명령 프롬프트(cmd)로 이동.
|
||||
- `./gradlew` 대신 `gradlew.bat`를 실행.
|
||||
- 명령 프롬프트에서 `gradlew.bat`를 실행하려면 `gradlew`하고 엔터를 치면 된다.
|
||||
- `gradlew build`
|
||||
- 폴더 목록 확인 : `ls` 대신 `dir` 사용.
|
||||
- `cd build/libs`
|
||||
- `java -jar hello-spring-0.0.1-SNAPSHOT.jar`
|
||||
- 실행 확인
|
||||
|
||||
[윈도우에서 Git Bash 터미널 사용하기](https://www.inflearn.com/questions/53961)
|
||||
|
||||
서버에 배포할때는 `build`를 통해 만들어진 `jar` 파일만 서버에 넣어준 다음 `java -jar`로 실행시키면 서버에서도 스프링이 동작하게 된다.
|
||||
|
||||
- `./gradlew clean`을 이용하면, `build` 폴더 자체가 제거된다.
|
||||
- `./gradlew clean build`를 이용하면 `build` 폴더를 완전히 지우고 다시 빌드한다.
|
||||
- 서버를 종료하려면 `Ctrl + C` 를 입력한다.
|
||||
|
||||
---
|
||||
36
SpringBoot-Introduction/Lectures/Lecture05.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 정적 컨텐츠
|
||||
|
||||
---
|
||||
|
||||
## 간단한 설명
|
||||
|
||||
### 정적 컨텐츠
|
||||
- `Welcome Page`와 같이 서버에서 파일을 그대로 웹 브라우저에 내려주는 것을 말한다.
|
||||
|
||||
### MVC와 템플릿 엔진
|
||||
- `HTML`을 그냥 주는 것이 아니라 서버에서 프로그래밍(변형)을 해서 `HTML`을 동적으로 바꿔서 내려주는 것을 말한다.
|
||||
- `Model`, `View`, `Controller`의 세 가지를 `MVC`라고 한다.
|
||||
|
||||
|
||||
### API
|
||||
- 최근에는 `json`이라는 데이터 구조 포맷을 이용하여 클라이언트에게 데이터를 전달하는 방식을 말한다.
|
||||
- 최근에는 `vue.js`, `react`등을 사용할 때도 API로 데이터만 내려주면 화면은 클라이언트가 알아서 그리고 정리하는 방식을 이용할 때도 사용한다.
|
||||
- 서버끼리 통신할 때에도 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 부트에서의 정적 컨텐츠
|
||||
|
||||
- 스프링 부트는 정적 컨텐츠 기능을 제공한다.
|
||||
- [스프링 Static Content](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-spring-mvc-static-content)
|
||||
- 스프링 부트는 기본적으로 정적 컨텐츠는 `/static` 폴더에서 찾아서 제공한다.
|
||||
- 원하는 파일을 넣으면(`/static` 폴더 내부에) 정적 파일이 그대로 반환되지만, 어떠한 프로그래밍을 할 수는 없다.
|
||||
|
||||
### 정적 컨텐츠 이미지
|
||||

|
||||
- 웹 브라우저에서 `localhost:8080/hello-static.html` 으로 접근하면 내장 톰캣 서버가 요청을 받아서 스프링한테 넘긴다.
|
||||
- 컨트롤러쪽에서 `hello-static`이 있는지 먼저 찾아본다(컨트롤러가 우선 순위에 있다는 뜻).
|
||||
- 하지만 `hello-static`와 매핑된 컨트롤러 메서드는 없다.
|
||||
- 따라서 `resources/static/hello-static.html`을 찾아서 있으면 그대로 반환한다.
|
||||
|
||||
---
|
||||
38
SpringBoot-Introduction/Lectures/Lecture06.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# MVC와 템플릿 엔진
|
||||
- `MVC` : `Model`, `View`, `Controller`
|
||||
- 과거에는 `View`에서 모든 것을 다 했다(`Model 1 방식`).
|
||||
- 개발을 할 때는 관심사를 분리해야 한다. 역할과 책임.
|
||||
- `View`는 화면을 그리는데 모든 역량을 집중해야 한다.
|
||||
- `Model`, `Controller`과 관련된 부분들은 비즈니스 로직과 관련이 있거나 내부적인 것들을 처리하는데 집중해야 한다.
|
||||
- `View`는 화면에 관계된 일만.
|
||||
- 비즈니스 로직과 서버 뒷단에 관련된 것은 `Controller`나 뒷단 비즈니스 로직에서 처리하고 `Model`에 화면에 필요한 것들을 담아서 화면쪽에 넘겨준다.
|
||||
|
||||
---
|
||||
|
||||
## @RequestParam
|
||||
- 외부에서 파라미터를 받을 때 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## Thymeleaf
|
||||
- `html`을 그대로 쓰고 그 파일을 서버 없이 바로 열어봐도 껍데기를 볼 수 있다는 장점이 있다.
|
||||
- 다음 `html` 코드는 템플릿 엔진으로 동작을 하면 내용(`hello! empty`)이 서버에서 전달해준 데이터로 치환(`'hello ' + ${name}`)된다.
|
||||
- `<p th:text="'hello ' + ${name}">hello! empty</p>`
|
||||
|
||||
---
|
||||
|
||||
## 인텔리제이 옵션보기
|
||||
- 원하는 부분을 선택한 뒤 `ctrl + p`를 누르면 파라미터 정보 옵션을 볼 수 있다.
|
||||
- `@RequestParam`의 `required` 옵션의 기본값은 `true`이기 때문에 파라미터를 무조건 전달해줘야 한다(`false`로 지정하면 값을 넘기지 않아도 된다).
|
||||
|
||||
---
|
||||
|
||||
## MVC, 템플릿 엔진 이미지
|
||||

|
||||
- 웹 브라우저에서 `localhost:8080/hello-mvc`로 접근하면 내장 톰캣 서버가 스프링에게 요청을 던진다.
|
||||
- 스프링은 `helloController`에 해당 url에 매핑되어 있는 메서드를 호출해준다.
|
||||
- 그리고 리턴을 해줄때 `hello-template`와 `model(name:spring)`을 스프링한테 리턴해준다.
|
||||
- 스프링의 `viewResolver`가 뷰를 찾고 템플릿 엔진을 연결시켜준다. templates/`hello-template`.html(메서드가 리턴한 값인 `hello-template`)를 찾아서 `Thymeleaf` 템플릿 엔진한테 처리해달라고 넘긴다.
|
||||
- `Thymeleaf` 템플릿 엔진이 랜더링해서 변환한 `html`을 웹 브라우저에 반환한다.
|
||||
|
||||
---
|
||||
58
SpringBoot-Introduction/Lectures/Lecture07.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# API
|
||||
- API방식은 데이터를 바로 내려주는 방식이다.
|
||||
|
||||
---
|
||||
|
||||
## @ResponseBody
|
||||
- `http`의 통신 프로토콜에서 `header`와 `body` 중에서 `body`에 응답 데이터를 직접 넣어주겠다는 의미이다.
|
||||
- 메서드에서 리턴하는 문자가 요청하는 클라이언트에 그대로 전달된다.
|
||||
- 템플릿 엔진과의 차이는 `View`와 같은 것들이 없이 문자가 그대로 전달되는 것이다.
|
||||
- 즉, `html` 태그와 같은 것들이 하나도 없다...
|
||||
|
||||
---
|
||||
|
||||
## static class
|
||||
- `static class`로 만들면 `class` 내부에서 사용할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 인텔리제이 단축키
|
||||
- `ctrl + shift + enter`를 누르면 코드의 세미콜론을 자동완성해준다.
|
||||
|
||||
---
|
||||
|
||||
## JSON
|
||||
- `키(key)`와 `값(value)`으로 이루어진 구조이다.
|
||||
- `{키 : 값}`의 형식을 가진다.
|
||||
- 과거에는 `XML`도 많이 쓰였다(`<HTML></HTML>`과 같은 것들).
|
||||
- 최근에는 `JSON`으로 거의 통일되었다(거의 `json`이 `default`로 세팅되어 있다).
|
||||
|
||||
---
|
||||
|
||||
## 자바 빈 규약
|
||||
- `getter`, `setter`를 자바 빈 규약이라고 하며, 프로퍼티 접근 방식이라고 하기도 한다.
|
||||
- `private` 필드값인 `name`에 메서드를 통해서 접근하게 된다.
|
||||
|
||||
## @ResponseBody 사용 원리(보통 API를 사용하는 방식)
|
||||

|
||||
- 웹 브라우저에서 `localhost:8080/hello-api`로 접근.
|
||||
- 톰캣 내장 서버가 스프링에 던짐.
|
||||
- 스프링이 `hello-api`에 매핑된 메서드를 찾음.
|
||||
- 근데 `@ResponseBody`라는 어노테이션이 붙어있네? 이럴 경우에는 리턴된 데이터를 응답으로 그대로 `HTTP`의 `BODY`에 반환하도록 동작한다(`view` 없이).
|
||||
- 그런데! 문자가 아니라 객체일 경우에는 `json` 형식으로 데이터를 만들어서 `HTTP`응답에 반환하는 것이 기본정책이다.
|
||||
- `@ResponseBody`가 있으면 우선, `HttpMessageConverter`라는 녀석이 동작한다(기존에는 `viewResolver`가 동작했었다).
|
||||
- 단순 문자일 경우에는 `StringConverter`가 동작한다.
|
||||
- 기본 문자처리 : `StringHttpMessageConverter`
|
||||
- 객체일 경우에는 `JsonConverter`가 동작해서 `json` 형식으로 바꿔서 요청한 웹 브라우저 또는 서버로 반환한다.
|
||||
- 기본 객체처리 : `MappingJackson2HttpMessageConverter`
|
||||
- 실무에서는 `Jackson`, `Gson` 라이브러리를 자주 보게되는데, 스프링에서는 `Jackson` 라이브러리를 기본으로 사용한다(물론 변경 가능하다).
|
||||
- 객체를 `Jackson`이라는 라이브러리로 `json`으로 바꾸는 것을 `MappingJackson2HttpMessageConverter`가 수행한다.
|
||||
- 이렇게 나오는 `json` 데이터를 `HTTP`의 `BODY`에 실어서 웹 브라우저 또는 클라이언트 등에 반환한다.
|
||||
- `byte` 처리 등등 기타 여러 `HttpMessageConverter`가 기본으로 등록되어 있다.
|
||||
|
||||
---
|
||||
|
||||
## 참고사항
|
||||
- 클라이언트의 `HTTP Accept 헤더`와 `서버의 컨트롤러 반환 타입 정보` 둘을 조합해서 `HttpMessageConverter`가 선택된다.
|
||||
|
||||
---
|
||||
28
SpringBoot-Introduction/Lectures/Lecture08.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 비즈니스 요구사항 정리
|
||||
- 데이터 : 회원ID, 이름
|
||||
- 기능 : 회원등록, 조회
|
||||
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
|
||||
|
||||
---
|
||||
|
||||
## 일반적인 웹 어플리케이션 계층 구조
|
||||

|
||||
|
||||
- 컨트롤러 : 웹 MVC의 컨트롤러 역할을 한다.
|
||||
- 서비스 : 서비스 클래스의 핵심 비즈니스 로직을 구현한다(ex. 회원은 중복가입이 안된다거나..).
|
||||
- 서비스는 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 객체이다.
|
||||
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리한다.
|
||||
- 도메인 : 회원, 주문, 쿠폰 등등 주문 데이터베이스에 저장하고 관리되는 비즈니스 도메인 객체이다.
|
||||
|
||||
---
|
||||
|
||||
## 클래스 의존관계
|
||||

|
||||
|
||||
- 회원 비즈니스 로직에는 회원 서비스가 있다.
|
||||
- 회원 리포지토리는 인터페이스로 설계한다.
|
||||
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
|
||||
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 `메모리 기반의 데이터 저장소`를 사용한다(굉장히 단순하며 금방 만들 수 있다).
|
||||
- 이는 향후에 `RDB`, `JPA` 등의 구체적인 기술이 선정되고나면(현재는 다양한 저장소를 고려중인 상황이라고 가정) 바꿔끼운다(바꿔끼우기 위해서는 인터페이스가 필요하므로 리포지토리는 인터페이스로 설계한다).
|
||||
|
||||
---
|
||||
35
SpringBoot-Introduction/Lectures/Lecture09.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 회원 도메인과 리포지토리 만들기
|
||||
|
||||
---
|
||||
|
||||
## Member class
|
||||
- id : 임의의 값(고객이 정하는 아이디가 아닌 데이터 구분을 위해 시스템이 저장하는 ID)
|
||||
|
||||
---
|
||||
|
||||
## Optional
|
||||
- 자바8에 추가된 기능이다.
|
||||
- 최근에는 `null`을 처리하는 방법 중에서 `null`을 `Optional`로 감싸서 반환하는 방법을 선호한다.
|
||||
|
||||
---
|
||||
|
||||
## HashMap, long
|
||||
- 실무에서는 동시성 문제가 발생할 수 있으므로 공유되는 변수에는 `ConcurrentHashMap<>`을 사용해야 한다.
|
||||
- 이 역시 동시성 문제를 고려해서 `atomicLong` 등을 사용해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## Optional.ofNullable(store.get(id))
|
||||
- `store.get(id)`가 `null`이어도 감싸서 반환할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## findAll()
|
||||
- 실무에서는 리스트를 많이 사용한다(편리하다고 함).
|
||||
|
||||
---
|
||||
|
||||
## store.values()
|
||||
- `store`라는 `Map`의 값들을 불러온다.
|
||||
|
||||
---
|
||||
32
SpringBoot-Introduction/Lectures/Lecture10.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 회원 리포지토리 테스트 케이스 작성
|
||||
- 개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다.
|
||||
- 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다.
|
||||
- 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
|
||||
|
||||
---
|
||||
|
||||
## 테스트 클래스 명명법 관례
|
||||
- `테스트하고자 하는 클래스의 이름` + `Test`로 짓는다.
|
||||
|
||||
---
|
||||
|
||||
## 테스트가 실패한다면?
|
||||
- 실무에서는 빌드 툴에서 빌드할 때 테스트가 통과하지 않으면 다음 단계로 못 넘어가게 막아버린다.
|
||||
|
||||
---
|
||||
|
||||
## 테스트는 순서가 보장이 안된다
|
||||
- 모든 테스트는 순서에 관계없이 메서드별로 각자 동작하게끔 만들어야 한다.
|
||||
- 순서의 의존적으로 설계하면 절대 안된다.
|
||||
|
||||
---
|
||||
|
||||
## 테스트가 끝날 때마다 데이터를 클리어 해줘야 한다.
|
||||
- `@AfterEach` 어노테이션을 붙여서 메서드를 만들면 테스트 하나가 끝날 때마다 동작하는 메서드를 정의할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## TDD(Test Driven Development)
|
||||
- 테스트를 먼저 만들고 구현 클래스를 만들어서 테스트를 실행하는 방식으로 개발하는 방법을 말한다.
|
||||
|
||||
---
|
||||
33
SpringBoot-Introduction/Lectures/Lecture11.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 회원 서비스 개발
|
||||
|
||||
---
|
||||
|
||||
## 회원 서비스
|
||||
- 회원 리포지토리와 도메인을 사용하는 실제 비즈니스 로직을 작성한다.
|
||||
|
||||
---
|
||||
|
||||
## ifPresent
|
||||
- `Optional`에서 `null`이 아닌 값이 있으면 정의된 로직이 동작한다.
|
||||
- `null`일 가능성이 있으면 `Optional`로 감싸주고 사용할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 메서드 추출
|
||||
- `Ctrl + Alt + m`(window)
|
||||
- `Command + Option + m`(Mac)
|
||||
|
||||
---
|
||||
|
||||
## 리포지토리
|
||||
- 단순히 기계적으로 개발스럽게 용어들을 보통 선택한다.
|
||||
- 단순히 데이터베이스에 접근(데이터를 넣었다 뺐다)하는 역할을 수행한다.
|
||||
|
||||
---
|
||||
|
||||
## 서비스 클래스
|
||||
- 비즈니스에 가까운 용어를 사용해야 한다.
|
||||
- 비즈니스에 의존적으로 설계한다.
|
||||
- 비즈니스를 처리하는 역할을 수행한다.
|
||||
|
||||
---
|
||||
23
SpringBoot-Introduction/Lectures/Lecture12.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 회원 서비스 테스트
|
||||
|
||||
---
|
||||
|
||||
## 테스트 클래스 작성
|
||||
- `Ctrl + Shift + T`
|
||||
- 테스트 코드는 과감하게 한글을 사용해도 된다(영어권 사람들과 일하는 것이 아니라면).
|
||||
- 빌드될 때 테스트 코드는 실제 코드에 포함되지 않는다.
|
||||
- `given`, `when`, `then` 주석을 권장하는 편이다.
|
||||
- `given` : 무언가 상황이 주어졌을 때
|
||||
- `when` : 이것을 실행하면
|
||||
- `then` : 이러한 결과가 나와야 한다.
|
||||
- 테스트는 예외상황이 정상상황보다 더 중요하다.
|
||||
- `Ctrl + R` : 이전에 실행했던 것을 그대로 실행해준다(Mac).
|
||||
- 윈도우는 `Shift + F10`을 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection(의존성 주입)
|
||||
- 직접 인스턴스를 만드는 것이 아니라 외부에서 넣어주는 것을 말한다.
|
||||
- `MemberSerivce` 입장에서는 직접 `new`로 인스턴스를 만드는 것이 아니라 외부에서 `MemberRepository`를 넣어주는 것이다.
|
||||
|
||||
---
|
||||
58
SpringBoot-Introduction/Lectures/Lecture13.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 컴포넌트 스캔과 자동 의존관계 설정
|
||||
- 화면을 고치려면 먼저 컨트롤러와 뷰 템플릿이 필요하다.
|
||||
- 그러려면 우선 컨트롤러가 있어야 하는데 컨트롤러가 서비스를 통해서 회원가입과 조회를 할 수 있어야 한다.
|
||||
- 이러한 관계를 의존관계가 있다고 한다(컨트롤러가 서비스를 의존한다).
|
||||
|
||||
---
|
||||
|
||||
## 컨트롤러 선언시 발생하는 일
|
||||
- 스프링은 시작할 때 `스프링 컨테이너`라는 것이 생기는데 `@Controller` 어노테이션이 있으면 해당 객체를 생성해서 `스프링 컨테이너`에 넣어두고 관리한다.
|
||||
- 이를 두고 `'스프링 컨테이너에서 스프링 빈이 관리된다'` 라고 표현한다.
|
||||
- 스프링이 관리하게 되면 스프링 컨테이너에 등록되고, 스프링 컨테이너에서 받아서 쓰도록 바꿔야 한다. 불필요하게 인스턴스를 생성할 필요가 없다는 뜻이다.
|
||||
- 즉, 스프링 컨테이너에 등록하면 된다. 스프링 컨테이너에 등록하면 단 하나만 등록된다.
|
||||
- 스프링 컨테이너와의 연결 방법은 생성자를 만들고 거기에 `@Autowired` 어노테이션을 선언하면 된다.
|
||||
- 스프링을 시작하고 컨테이너가 뜰 때 `Controller`가 생성되는데 이때 생성자 호출을 하게된다.
|
||||
- 그런데 생성자에 `@Autowired`라고 되어있으면 생성자로 주입되는 의존성 객체를 스프링이 스프링 컨테이너에서 가져와서 `Controller`에 연결시켜 준다(`Dependency Injection`), 의존성 주입.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## @Autowired
|
||||
- 컨트롤러와 서비스등 컨테이너에 등록된 객체를 가져와서 연결시켜줄 때 사용한다.
|
||||
- 스프링 컨테이너에서 의존성(생성자 파라미터)을 가져온다.
|
||||
- 하지만 스프링에서 관리하지 않는 순수한 클래스는 가져오지 못한다.
|
||||
- 서비스의 경우, `@Service`를 클래스에 선언해주면 스프링이 컨테이너에 서비스를 등록해준다.
|
||||
- 마찬가지로 리포지토리의 경우, `@Repository`를 구현체에 선언해주면 스프링이 컨테이너에 리포지토리를 등록해준다.
|
||||
|
||||
---
|
||||
|
||||
## Controller, Service, Repository
|
||||
- 정형화된 패턴이다.
|
||||
- `Controller`에서 외부 요청을 받는다.
|
||||
- `Service`에서 비즈니스 로직을 만든다.
|
||||
- `Repository`에서 데이터를 저장한다.
|
||||
- 스프링을 실행할 때 위 세 가지를 모두 컨테이너로 가져온다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 빈을 등록하는 2가지 방법
|
||||
- 컴포넌트 스캔(`@Controller, @Service, @Repository 등의 어노테이션 선언`, `각각의 어노테이션은 @Component를 포함하고 있다.`)과 자동 의존관계 설정(`@Autowired`)
|
||||
- 스프링은 `@Component`를 포함하는 어노테이션이 있으면 해당하는 것들은 전부 객체를 생성해서 스프링 컨테이너에 스프링 빈으로 등록한다.
|
||||
- 자바 코드로 직접 스프링 빈 등록하기
|
||||
|
||||
---
|
||||
|
||||
## 아무데나 @Component를 선언해도 빈으로 등록되는가?
|
||||
- 아니다.
|
||||
- `@SpringBootApplication` 어노테이션이 선언된 패키지부터 시작(포함)해서 하위는 스프링이 다 뒤져서 빈으로 등록한다.
|
||||
- 하지만 하위 패키지가 위의 패키지와 동일하거나 하위 패키지가 아닌 것들은 스프링 빈으로 컴포넌트 스캔을 하지 않으므로 컨테이너에 등록되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
- 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다).
|
||||
- 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다.
|
||||
- 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
|
||||
|
||||
---
|
||||
42
SpringBoot-Introduction/Lectures/Lecture14.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 자바 코드로 직접 스프링 빈 등록하기
|
||||
직접 설정 파일을 등록하는 방법이다.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## @Configuration, @Bean
|
||||
- 스프링이 실행될 때 `@Configuration` 어노테이션이 붙은 클래스를 읽고 `@Bean`이 선언된 메서드를 확인한다.
|
||||
- 다음으로 확인한 메서드(`@Bean` 어노테이션이 선언되어 있는)를 호출해서 해당 메서드의 로직을 호출해서 스프링 빈에 등록해준다.
|
||||
- 이후 의존관계 주입을 할 때 등록된 빈을 사용하게 한다.
|
||||
- `Controller`의 경우는 어차피 스프링이 관리를 하기때문에 `@Controller`를 그대로 둔다. 또한 컴포넌트 스캔이기 때문에 `@Autowired` 어노테이션을 사용한다(다른 곳에서 설정할 수 있는게 아니므로).
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
|
||||
### `DI`에는 `필드 주입`, `setter 주입`, `생성자 주입`의 3가지 방법이 있다.
|
||||
|
||||
#### 필드 주입
|
||||
- 스프링을 사용할 때만 넣어주고 이후에 변경할 수 있는 방법이 없으므로 권장하지 않는다.
|
||||
|
||||
#### setter 주입(setter 메서드에 @Autowired 선언)
|
||||
- `setter`를 통해서 의존성이 주입된다.
|
||||
- 생성은 생성대로 되고 `setter`를 나중에 호출해서 의존성을 주입해준다.
|
||||
- 단점은 누군가가 호출했을 때 `public`으로 열려있어야 한다(처음에 세팅이 되어있으면 중간에 바꿀 일이 없는데 노출이 되어있기 때문에 잘못 변경하면 문제가 발생할 수 있다).
|
||||
|
||||
#### 생성자 주입(가장 권장하는 방법)
|
||||
- 처음에 애플리케이션이 조립(세팅)될 때 생성자로 의존성이 주입되고 끝난다(이후 변경이 불가능하게 막을 수 있다).
|
||||
- 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
|
||||
|
||||
### 컴포넌트 스캔 vs 직접 자바코드로 설정파일 만들어서 빈 등록하기
|
||||
- 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. **그리고 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.**
|
||||
|
||||
직접 설정파일을 만들어서 등록하면 설정파일만 수정하면 다른 코드를 전혀 수정할 필요 없이 변경점을 적용할 수 있다.
|
||||
|
||||
### 주의
|
||||
- `@Autowired` 를 통한 `DI`는 helloConroller , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다.
|
||||
- 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
|
||||
- 직접 `new`로 인스턴스를 생성하는 경우에도 `@Autowired`가 동작하지 않는다(스프링 컨테이너에 올라가지 않았기 때문).
|
||||
|
||||
---
|
||||
12
SpringBoot-Introduction/Lectures/Lecture15.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 회원 웹 기능 - 홈 화면 추가
|
||||
|
||||
---
|
||||
|
||||
## @GetMapping
|
||||
- `("/")`에서 `/`는 도메인에 아무것도 입력하지 않고 접근한 url이다.
|
||||
- 우선순위가 있다.
|
||||
- 컨트롤러에 매핑된 메서드 -> 스태틱 파일 순으로 찾는다.
|
||||
- 이는 정적 컨텐츠에서 설명한 것과 같은 맥락이다.
|
||||
- 매핑된 메서드를 찾으면 해당 메서드를 호출한다.
|
||||
|
||||
---
|
||||
19
SpringBoot-Introduction/Lectures/Lecture16.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 회원 웹 기능 - 등록
|
||||
|
||||
---
|
||||
|
||||
## Get방식
|
||||
- `url`에서 직접 접근하는 방식
|
||||
- 조회할 때 주로 사용하는 방식
|
||||
- 전달(데이터 등록)할 때는 `post`방식을 주로 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## form 태그
|
||||
- 값을 입력할 수 있는 `HTML` 태그
|
||||
- `method`에 `get`, `post`등을 지정한다.
|
||||
- `input 태그`에서는 `name`이 서버로 넘어갈 때 `키(key)`가 된다.
|
||||
- `action`에 지정된 `url`로 `post` 방식으로 값이 넘어간다.
|
||||
- 값이 넘어갈 때 스프링이 알아서 `setName`을 통해서 `MemberForm`의 `name`에 값을 넣어준다(우리는 `getName`으로 꺼내면 된다).
|
||||
|
||||
---
|
||||
12
SpringBoot-Introduction/Lectures/Lecture17.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 회원 웹 기능 - 조회
|
||||
|
||||
---
|
||||
|
||||
## 템플릿 엔진
|
||||
- `${키}`은 모델안에 있는 해당 키의 값을 꺼내는 것이다.
|
||||
- ex. `${members}`는 모델에서 `members`라는 키에 해당하는 값을 꺼내온다.
|
||||
- `th:each="member : ${members}"` : `members` 리스트의 루프를 돌면서 객체를 하나씩 `member`에 할당한 뒤, 해당 태그 내부의 로직을 실행한다. 자바의 `for-each`문과 비슷하다.
|
||||
- `${member.id}` : 위에서 할당된 `member` 객체에서 `getId`를 호출한 결과값이 할당된다.
|
||||
- `${member.name}` : 위에서 할당된 `member` 객체에서 `getName`를 호출한 결과값이 할당된다.
|
||||
|
||||
---
|
||||
40
SpringBoot-Introduction/Lectures/Lecture18.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# H2 데이터베이스 설치
|
||||
- 실무에서는 데이터베이스에 데이터를 저장하고 관리한다.
|
||||
- `H2 데이터베이스`는 교육용으로 굉장히 좋다. 용량도 작고 가볍고 웹으로 어드민 화면도 제공해준다.
|
||||
|
||||
---
|
||||
|
||||
## H2 데이터베이스
|
||||
- [H2 데이터베이스](https://www.h2database.com) 압축파일 다운로드 및 설치, h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.
|
||||
- 실행파일을 실행(Mac의 경우 권한추가 필요)하면 `H2 콘솔` 창이 뜬다.
|
||||
- 권한 주기(Mac): chmod 755 h2.sh
|
||||
- 실행(Mac): ./h2.sh
|
||||
- 이때 포트번호 앞의 ip부분을 `localhost`로 변경하여 접근한다.
|
||||
- 가장 먼저 데이터베이스 파일을 만들어야 한다.
|
||||
|
||||
### 데이터베이스 파일 생성 방법
|
||||
- `JDBC URL`에 `jdbc:h2:~/test` (최초 한번) 입력후 연결.
|
||||
- `~/test.mv.db` 파일 생성되었는지 확인(여기서 `~`은 C드라이브의 사용자 폴더(`home`)를 말함)
|
||||
- 이후부터는 `jdbc:h2:tcp://localhost/~/test` 이렇게 접속한다(파일로 접근하면 애플리케이션이랑 웹 콘솔이랑 동시에 접근이 안되거나 오류가 날 수 있기 때문이다).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## H2 데이터베이스 테이블 생성하기
|
||||
|
||||
테이블 관리를 위해 프로젝트 루트에 `sql/ddl.sq` 파일을 생성하여 정리한다.
|
||||
```
|
||||
drop table if exists member CASCADE;
|
||||
create table member
|
||||
(
|
||||
id bigint generated by default as identity,
|
||||
name varchar(255),
|
||||
primary key (id)
|
||||
);
|
||||
```
|
||||
- 자바의 `Long` 타입은 `DB`에서는 `bigint` 타입으로 선언한다.
|
||||
- `generated by default as identity`를 선언하면 만약 `null`값, 즉 해당 컬럼에 값을 셋팅하지 않았다면 데이터가 들어왔을 때 `DB`가 자동으로 값을 채워준다.
|
||||
- 쉽게 말하면 `DB야 너가 알아서 PK값 넣어줘`와 같다.
|
||||
|
||||
---
|
||||
237
SpringBoot-Introduction/Lectures/Lecture19.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# 순수 JDBC
|
||||
|
||||
---
|
||||
|
||||
## 환경 설정
|
||||
|
||||
### build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
|
||||
```
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
```
|
||||
- 자바는 기본적으로 `DB`와 연결하려면 `jdbc` 드라이버(여기서는 `'org.springframework.boot:spring-boot-starter-jdbc'`)가 반드시 있어야 하며, 이를 통해 연동한다.
|
||||
|
||||
### 스프링 부트 데이터베이스 연결 설정 추가(`resources/application.properties`에 추가)
|
||||
```
|
||||
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
```
|
||||
- `DB`와 연결될 때 데이터베이스가 제공하는 클라이언트(여기서는 `'com.h2database:h2'`)가 필요하다.
|
||||
- `DB`와 연결하려면 접속정보를 넣어줘야 한다(`application.properties`에 기재).
|
||||
- 이렇게 기재하면 스프링이 데이터베이스와 연결하는 작업을 다 해준다. 이제 갖다 쓰기만 하면된다.
|
||||
|
||||
> **주의!**
|
||||
>
|
||||
>스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다. spring.datasource.username=sa 공백 주의, 공백은 모두 제거해야 한다.
|
||||
|
||||
> **참고**
|
||||
>
|
||||
> 인텔리J 커뮤니티(무료) 버전의 경우 application.properties 파일의 왼쪽이 다음 그림과 같이 회색으로 나온다. 엔터프라이즈(유료) 버전에서 제공하는 스프링의 소스 코드를 연결해주는 편의 기능이 빠진 것인데, 실제 동작하는데는 아무런 문제가 없다.
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
## javax.sql.DataSource
|
||||
- `DB`와 연동하기 위해 필요하다.
|
||||
- 스프링한테 주입받아야 한다.
|
||||
|
||||
---
|
||||
|
||||
## Jdbc 리포지토리 구현
|
||||
```java
|
||||
public class JdbcMemberRepository implements MemberRepository {
|
||||
private final DataSource dataSource;
|
||||
|
||||
public JdbcMemberRepository(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member save(Member member) {
|
||||
String sql = "insert into member(name) values(?)";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql,
|
||||
Statement.RETURN_GENERATED_KEYS);
|
||||
pstmt.setString(1, member.getName());
|
||||
pstmt.executeUpdate();
|
||||
rs = pstmt.getGeneratedKeys();
|
||||
if (rs.next()) {
|
||||
member.setId(rs.getLong(1));
|
||||
} else {
|
||||
throw new SQLException("id 조회 실패");
|
||||
}
|
||||
return member;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findById(Long id) {
|
||||
String sql = "select * from member where id = ?";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
pstmt.setLong(1, id);
|
||||
rs = pstmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return Optional.of(member);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Member> findAll() {
|
||||
String sql = "select * from member";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
rs = pstmt.executeQuery();
|
||||
List<Member> members = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
members.add(member);
|
||||
}
|
||||
return members;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findByName(String name) {
|
||||
String sql = "select * from member where name = ?";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
pstmt.setString(1, name);
|
||||
rs = pstmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return Optional.of(member);
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection() {
|
||||
return DataSourceUtils.getConnection(dataSource);
|
||||
}
|
||||
|
||||
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (pstmt != null) {
|
||||
pstmt.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
close(conn);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void close(Connection conn) throws SQLException {
|
||||
DataSourceUtils.releaseConnection(conn, dataSource);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
>**주의!**
|
||||
>
|
||||
>이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 프레임워크에서 데이터베이스 Connection을 사용할 때 주의할 점
|
||||
- 반드시 `DataSourceUtils.getConnection()`을 통해서 `Connection`을 획득해야 한다.
|
||||
- 트랜잭션이 걸리거나 했을 때 똑같은 데이터베이스 커넥션을 유지해야 하는데, 위의 방법으로 해야 유지시켜준다.
|
||||
|
||||
```java
|
||||
private Connection getConnection() {
|
||||
return DataSourceUtils.getConnection(dataSource);
|
||||
}
|
||||
```
|
||||
|
||||
- 닫을 때도 `DataSourceUtils`를 이용해서 릴리즈 해줘야 한다.
|
||||
|
||||
```java
|
||||
private void close(Connection conn) throws SQLException {
|
||||
DataSourceUtils.releaseConnection(conn, dataSource);
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## 추가 설명
|
||||
|
||||
- 스프링 부트는 데이터베이스 설정 파일를 보고 스프링 자체적으로 빈(데이터베이스와 연결할 수 있는 정보가 담긴 `DataSource`)도 생성해준다.
|
||||
|
||||
- `DataSource`는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.
|
||||
|
||||
- `다형성을 활용한다` : 인터페이스를 두고 구현체를 바꿔끼우기 하는 것.
|
||||
- 스프링은 이를 편리하게 할 수 있도록 스프링 컨테이너가 지원해준다.
|
||||
- `DI` 덕분에 다형성을 편리하게 활용할 수 있다.
|
||||
|
||||
- `어셈블리(조립)` : 기존의 코드는 하나도 손대지 않고 애플리케이션을 설정하는 것, 조립하는 코드만 약간 수정하면 실제 애플리케이션에 관련된 코드는 하나도 손대지 않아도 된다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 클래스 추가 이미지
|
||||

|
||||
|
||||
## 스프링 설정 이미지
|
||||

|
||||
|
||||
- 개방-폐쇄 원칙(OCP, Open-Closed Principle)
|
||||
- 확장에는 열려있고, 수정, 변경에는 닫혀있다.
|
||||
- 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 **구현 클래스를 변경**할 수 있다.
|
||||
- 회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.
|
||||
- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
|
||||
|
||||
---
|
||||
38
SpringBoot-Introduction/Lectures/Lecture20.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 스프링 통합 테스트
|
||||
- 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.
|
||||
- 순수한 자바 코드로 테스트할 수 없다(데이터베이스 커넥션 정보도 스프링 부트가 들고있는등의 이유로 인해).
|
||||
|
||||
---
|
||||
|
||||
## 추가 설명
|
||||
|
||||
- 통합 테스트를 위해서는 직접 객체를 생성하는 것이 아니라 스프링 컨테이너에서 의존성을 가져와서 진행해야 한다.
|
||||
- 사실 테스트는 제일 끝단에 있는 것이기 때문에 테스트 코드를 작성할 때는 가장 편한 방법을 사용하면 된다.
|
||||
- 기존 코드들은 생성자를 통해 의존성을 주입하는 것이 좋지만 테스트 코드는 필요한 의존성을 주입한 뒤 테스트를 진행하면 끝이기 때문에 테스트 코드를 작성할 때 필드 기반으로 `@Autowired`를 사용하는 것도 편한 방법이다.
|
||||
|
||||
---
|
||||
|
||||
## @SpringBootTest
|
||||
- 스프링 컨테이너와 테스트를 함께 실행한다.
|
||||
- 테스트를 실행하면 스프링 서버가 올라가고 테스트가 완료되면 서버가 내려간다.
|
||||
|
||||
---
|
||||
|
||||
## @Transactional
|
||||
- 데이터베이스는 기본적으로 트랜잭션이라는 개념이 있다. DB의 데이터를 수정할 경우 커밋을 해야 DB에 반영이 된다(`오토커밋` 이라는 것도 있다).
|
||||
- 테스트가 끝난 뒤(DB의 데이터 수정을 마친 뒤)에 롤백해버리면 DB의 변경사항들이 반영이 되지 않는다.
|
||||
- `@Transactional`를 테스트 클래스에 선언하면 테스트를 실행할 때 트랜잭션을 먼저 실행하고 DB의 데이터를 변경한 다음에 테스트가 끝나면 해당 데이터 변경사항들을 롤백해준다. 즉, 독립적으로 다음 테스트를 반복해서 실행할 수 있게 된다(이후 추가적으로 처리해주는 코드 없이도!).
|
||||
- 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
|
||||
- `@Transactional` 어노테이션을 서비스와 같은 것들에 선언하면 롤백하지 않고 정상적으로 동작하며, 테스트 케이스에 선언했을 때만 항상 롤백하도록 동작한다.
|
||||
- `@Commit` 같은 어노테이션도 있다.
|
||||
|
||||
---
|
||||
|
||||
## 단위 테스트
|
||||
- 순수하게 자바 코드로 진행하며, 최소단위로 진행되는 테스트이다.
|
||||
- 가급적이면 순수한 단위 테스트가 훨씬 좋은 테스트일 확률이 높다.
|
||||
- 그러므로 단위별로 쪼개서 테스트를 잘 할 수 있도록 해야하고 스프링 컨테이너 없이 테스트할 수 있도록 훈련해야 한다.
|
||||
- 만약 스프링 컨테이너까지 올려서 테스트를 해야하는 상황이 발생한다면 그 테스트는 설계가 잘못되었을 확률이 높다.
|
||||
- 통합 테스트도 물론 필요하지만 단위 테스트를 잘 만드는 것이 훨씬 좋은 테스트이다.
|
||||
|
||||
---
|
||||
34
SpringBoot-Introduction/Lectures/Lecture21.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 스프링 JdbcTemplate
|
||||
- 순수 Jdbc와 동일한 환경설정을 하면 된다.
|
||||
- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
|
||||
- 실무에서도 많이 사용한다.
|
||||
- 사용하기 위해서는 `org.springframework.jdbc.core.JdbcTemplate`가 필요하다.
|
||||
- `org.springframework.jdbc.core.JdbcTemplate`는 주입 받을 수 있는 것은 아니며, `DataSource`를 주입 받는다.
|
||||
|
||||
```java
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public JdbcTemplateMemberRepository(DataSource dataSource) {
|
||||
jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
}
|
||||
```
|
||||
- 위와 같은 형태로 사용한다.
|
||||
- 이때 생성자가 단 하나만 있으면 스프링 빈으로 등록할 때 `@Autowired`를 생략할 수 있다.
|
||||
|
||||
```java
|
||||
private RowMapper<Member> memberRowMapper() {
|
||||
return (rs, rowNum) -> {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return member;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- `디자인 패턴` 중에서 `템플릿 메서드 패턴`을 사용해서 중복을 제거하고 코드를 줄인 결과가 위의 코드이다.
|
||||
- `JdbcTemplate`에서 쿼리를 날려서 나온 결과를 `RowMapper`를 통해서 매핑한 뒤, 그 결과를 리스트로 받아서 `Optional`로 변환시켜서 반환한다.
|
||||
- `RowMapper`는 `resultSet` 결과를 `Member` 객체로 매핑하고 생성한 다음에 반환해준다.
|
||||
|
||||
---
|
||||
145
SpringBoot-Introduction/Lectures/Lecture22.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# JPA
|
||||
- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
|
||||
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
|
||||
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
|
||||
- 객체를 JPA에 넣으면 JPA가 중간에서 DB에 SQL을 날리고 데이터를 가져오는 등의 처리를 JPA가 다 해준다.
|
||||
|
||||
---
|
||||
|
||||
## build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
|
||||
```java
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `spring-boot-starter-data-jpa` 는 내부에 `jdbc 관련 라이브러리를 포함`한다. 따라서 jdbc는 제거해도 된다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 부트에 JPA 설정 추가
|
||||
`resources/application.properties`
|
||||
```java
|
||||
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
```
|
||||
|
||||
>**주의**
|
||||
>
|
||||
>스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지 않으면 오류가 발생한다.
|
||||
|
||||
- `show-sql` : JPA가 생성하는 SQL을 출력한다.
|
||||
- `ddl-auto` : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 `none` 를 사용하면 해당 기능을 끈다.
|
||||
- `create` 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.
|
||||
|
||||
---
|
||||
|
||||
## JPA 엔티티 매핑
|
||||
- `JPA`를 사용하려면 먼저 `Entity`를 매핑해야한다.
|
||||
- `JPA`는 인터페이스만 제공한다. 그리고 구현 기술들이 여러개가 있는데, 우리는 `Hibernate`만 사용한다고 생각하면 된다.
|
||||
- `JPA`는 자바진영의 표준 인터페이스이고 구현은 여러 업체들이 하는 것이라 생각하면 된다. 각 업체별로 차이점이 조금씩 있다.
|
||||
- `ORM(Object Relational Mapping)` : 객체와 관계형 데이터베이스의 테이블을 매핑한다는 뜻이다.
|
||||
- `@Entity(javax.persistence.Entity)`를 클래스에 선언하면 해당 클래스는 `JPA`가 관리하는 엔티티가 된다.
|
||||
- 그 다음으로는 `PK`를 매핑해줘야 한다(`@Id` 사용). 또, 현재 우리는 데이터베이스에서 자동으로 `ID`를 생성해주고 있는데 이를 `IDENTITY`전략이라고 부르며 `@GeneratedValue(strategy = GenerationType.IDENTITY)`로 선언한다.
|
||||
- 특정 필드를 DB에 있는 특정 컬럼명과 매핑할 때, 해당 필드에 `@Column(name = "컬럼명")`을 선언해주면 된다.
|
||||
- 이렇게 어노테이션들을 통해서 데이터베이스의 테이블과 매핑하면 `JPA`가 해당 정보를 가지고 각종 쿼리들을 처리해준다.
|
||||
|
||||
---
|
||||
|
||||
## JPA 회원 리포지토리
|
||||
```java
|
||||
@Transactional
|
||||
public class JpaMemberRepository implements MemberRepository {
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public JpaMemberRepository(EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member save(Member member) {
|
||||
entityManager.persist(member);
|
||||
return member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findById(Long id) {
|
||||
Member member = entityManager.find(Member.class, id);
|
||||
return Optional.ofNullable(member);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findByName(String name) {
|
||||
List<Member> result = entityManager.createQuery("select m from Member m where m.name = :name", Member.class)
|
||||
.setParameter("name", name)
|
||||
.getResultList();
|
||||
|
||||
return result.stream().findAny();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Member> findAll() {
|
||||
return entityManager.createQuery("select m from Member m", Member.class)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `'org.springframework.boot:spring-boot-starter-data-jpa'` 라이브러리를 받으면 스프링 부트가 자동으로 설정 정보와 데이터베이스 커넥션 정보등을 이용해서 `EntityManager`라는 것을 생성해준다. 그럼 우리는 이걸 그대로 주입받으면 된다.
|
||||
- `JPA`는 `EntityManager`라는 것으로 모든게 동작한다. 결론적으로 `JPA`를 사용하려면 `EntityManager`를 주입받아야 한다.
|
||||
- `EntityManager`는 내부적으로 `DataSource`를 가지고 있어서 DB와 통신하는 등의 행위를 알아서 처리한다.
|
||||
- `persist` 는 영구저장(영속화)하다 라는 뜻이다. 이렇게 하면`JPA`가 알아서 `insert` 쿼리를 만들어 DB에 집어넣고 `setId`까지 모든걸 다 해준다.
|
||||
- `inline` 단축키 : `Ctrl + Alt + N`
|
||||
- `PK`의 경우는 그냥 조회할 수 있지만(`EntityManager.find()` 이용) 그 외에 `PK` 기반이 아닌 것들은 `JPQL`이라는 객체지향 쿼리언어를 사용해야 한다(거의 SQL과 똑같다).
|
||||
- 객체를 대상으로 쿼리를 날리면 SQL로 번역이 된다.
|
||||
- `select`의 대상이 객체(`Entity`) 그 자체이다.
|
||||
- `JPA`를 사용하려면 항상 `@Transactional`이 선언되어 있어야 한다.
|
||||
- `org.springframework.transaction.annotation.Transactional` 를 사용하자.
|
||||
- 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
|
||||
- JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## JPA를 사용하도록 스프링 설정 변경
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class SpringConfig {
|
||||
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
public SpringConfig(EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MemberService memberService() {
|
||||
return new MemberService(memberRepository());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MemberRepository memberRepository() {
|
||||
// return new MemoryMemberRepository();
|
||||
// return new JdbcMemberRepository(dataSource);
|
||||
// return new JdbcTemplateMemberRepository(dataSource);
|
||||
return new JpaMemberRepository(entityManager);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 추가 설명
|
||||
- `spring-data-jpa`를 세팅하면 기본적으로 `Hibernate`라는 오픈소스 구현체가 사용된다.
|
||||
82
SpringBoot-Introduction/Lectures/Lecture23.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 스프링 데이터 JPA
|
||||
- `스프링 부트`와 `JPA`만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어든다.
|
||||
-여기에 `스프링 데이터 JPA`를 사용하면 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.
|
||||
- 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공합니다.
|
||||
- 스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어든다.
|
||||
- 따라서 개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있다.
|
||||
- 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수이다.
|
||||
|
||||
>**주의**
|
||||
>
|
||||
>스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## SpringDataJpaRepository 인터페이스 생성
|
||||
```java
|
||||
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
|
||||
|
||||
@Override
|
||||
Optional<Member> findByName(String name);
|
||||
}
|
||||
```
|
||||
|
||||
- `org.springframework.data.jpa.repository.JpaRepository`를 상속받는다.
|
||||
- 인터페이스는 인터페이스를 `상속(extends)` 받는다(`구현(implements)`-말고).
|
||||
- `JpaRepository<Entity, ID>`의 제네릭 타입은 `Entity`와 `ID`로 지정해야 하며, 각각 `Entity`가 선언된 클래스 타입과 해당 클래스의 `PK` 타입을 지정한다.
|
||||
- 인터페이스는 다중 상속이 된다.
|
||||
- `스프링 데이터 JPA`는 `JpaRepository<Entity, ID>`를 상속받는 인터페이스의 구현체를 `프록시`라는 기능을 이용해서 자동으로 만들어 스프링 빈에 자동으로 등록해준다. 우리는 그걸 그냥 가져다가 쓰는 것이다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class SpringConfig {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
@Autowired
|
||||
public SpringConfig(MemberRepository memberRepository) {
|
||||
this.memberRepository = memberRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MemberService memberService() {
|
||||
return new MemberService(memberRepository);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 위의 `SpringConfig` 생성자에 `MemberRepository`는 다음과 같이 주입된다.
|
||||
- 스프링 컨테이너에서 `MemberRepository`를 찾는다. 이때 `MemberRepository`는 `스프링 데이터 JPA`가 만들고 빈으로 등록해줬던 구현체가(를) 주입된다(주입 받을 수 있다).
|
||||
- `스프링 데이터 JPA`는 `JPA`의 기술을 그대로 가져다 쓰기 때문에 로그가 동일하게 나온다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 데이터 JPA 제공 클래스
|
||||

|
||||
|
||||
- 기본적으로 `JpaRepository` 인터페이스에서 공통적인 메서드를 제공해준다.
|
||||
- 하지만 예를 들어, 이름으로 찾는다던지 이메일로 찾는다던지 공통적이지 않은 경우 메서드를 정의해줘야 한다.
|
||||
- ex. 메서드명이 `findByName`일 경우, `스프링 데이터 JPA`가 규칙에 따라 되어있는 `select m from Member m where m.name = ?`라는 `JPQL` 쿼리를 짜준다.
|
||||
- 이와 같이 인터페이스 이름만으로 개발을 끝낼 수 있다.
|
||||
- 메서드명과 반환타입, 파라미터 등을 `리플렉션` 기술로 읽어들여서 쿼리로 풀어낸다.
|
||||
|
||||
---
|
||||
|
||||
## 스프링 데이터 JPA 제공 기능
|
||||
- 인터페이스를 통한 기본적인 `CRUD` 기능 제공
|
||||
- `findByName()` , `findByEmail()` 처럼 `메서드 이름 만으로 조회 기능` 제공
|
||||
- `페이징 기능` 자동 제공
|
||||
|
||||
---
|
||||
|
||||
## 참고
|
||||
- 실무에서는 `JPA`와 `스프링 데이터 JPA`를 기본으로 사용하고, `복잡한 동적 쿼리`는 `Querydsl`이라는 라이브러리를 사용하면 된다.
|
||||
- `Querydsl`을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적
|
||||
쿼리도 편리하게 작성할 수 있다.
|
||||
- 이 조합으로 해결하기 어려운 쿼리는 `JPA`가 제공하는 `네이티브 쿼리(직접 쿼리를 짜는 것)`를 사용하거나, 앞서 학습한 스프링 `JdbcTemplate`를 사용하면 된다.
|
||||
|
||||
---
|
||||
21
SpringBoot-Introduction/Lectures/Lecture24.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# AOP가 필요한 상황
|
||||
- 만약 모든 메소드의 호출 시간을 측정하고 싶다면?
|
||||
- `공통 관심 사항(cross-cutting concern)` vs `핵심 관심 사항(core concern)`
|
||||
- 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?
|
||||
- 즉, `공통 관심 사항`과 `핵심 관심 사항`을 분리해서 적용하고 싶을 때 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## 추가설명
|
||||
- 실제 운영에서는 처음에 서버를 올리고 이것저것 호출하는 `warmUp`이라는 것을 한다.
|
||||
|
||||
---
|
||||
|
||||
## 문제
|
||||
- 회원가입, 회원 조회에 시간을 측정하는 기능은 `핵심 관심 사항(핵심 비즈니스 로직)`이 아니다.
|
||||
- 시간을 측정하는 로직은 `공통 관심 사항(공통 비즈니스 로직)`이다.
|
||||
- 시간을 측정하는 로직과 `핵심 비즈니스의 로직`이 섞여서 유지보수가 어렵다.
|
||||
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
|
||||
- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.
|
||||
|
||||
---
|
||||
73
SpringBoot-Introduction/Lectures/Lecture25.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# AOP 적용
|
||||
- `AOP`: `Aspect Oriented Programming(관점지향 프로그래밍)`
|
||||
- `공통 관심 사항(cross-cutting concern)` vs `핵심 관심 사항(core concern)` 분리
|
||||
|
||||

|
||||
|
||||
- `AOP`를 이용하면 `공통 관심 사항`에 대한 로직을 한 군데에 다 모아두고 원하는 곳에만 지정하여 적용할 수 있다.
|
||||
|
||||
```java
|
||||
@Aspect
|
||||
@Component
|
||||
public class TimeTraceAop {
|
||||
|
||||
@Around("execution(* hello.springintroduction..*(..))")
|
||||
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("START: " + joinPoint.toString());
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
long finish = System.currentTimeMillis();
|
||||
long timeMs = finish - start;
|
||||
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `AOP`는 `@Aspect` 어노테이션을 선언해야 사용할 수 있다.
|
||||
- `ProceedingJoinPoint`????
|
||||
- `joinPoint.proceed()`를 호출하면 다음 메서드로 진행할 수 있다.
|
||||
- `joinPoint.toString()`를 통해서 어떤 메서드를 호출했는지 이름을 다 얻을 수 있다.
|
||||
- `AOP`는 설정 파일을 이용해 스프링의 빈으로 등록해서 사용하는 것을 선호한다. 평범한 서비스, 리포지토리와 같은 것들은 정형화해서 사용할 수 있지만, `AOP`의 경우에는 `AOP`임을 인지할 수 있도록 빈으로 직접 등록하는 것이 낫다(강의에서는 간단하게 컴포넌트스캔 사용).
|
||||
- 그리고 `AOP`를 사용하기 위해서는 `@Around()`를 선언해서 괄호 안에 공통 관심사를 어디에 적용할 것인지를 지정해줘야 한다.
|
||||
- `@Around("execution(* hello.springintroduction..*(..))")` 에서 `"execution(* hello.springintroduction..*(..))"`는 `hello.springintroduction` 패키지의 하위에는 전부 적용하라 라는 뜻이다.
|
||||
- `joinPoint`는 메서드가 호출될 때마다 중간에서 인터셉트(?)가 걸려서 메서드 `execute`가 호출된다. 조건을 걸어서 메서드 호출 등을 조작할 수도 있다.
|
||||
|
||||
---
|
||||
|
||||
## 해결
|
||||
- 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
|
||||
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
|
||||
- 핵심 관심 사항을 깔끔하게 유지할 수 있다.
|
||||
- 변경이 필요하면 이 로직만 변경하면 된다.
|
||||
- 원하는 적용 대상을 선택할 수 있다(보통 패키지레벨로 많이 한다).
|
||||
|
||||
---
|
||||
|
||||
## 스프링의 AOP 동작 방식
|
||||
|
||||
### AOP 적용 전 의존관계
|
||||

|
||||
- 컨트롤러에서 서비스를 호출할 때 의존관계를 이용해서 호출한다.
|
||||
|
||||
### AOP 적용 후 의존관계
|
||||

|
||||
- `AOP`를 적용하고 어디에 적용할지 지정을 하면 의존관계가 있는 서비스가 지정된다.
|
||||
- 스프링은 `AOP`가 있으면 가짜 서비스를 만들어낸다(프록시라는 기술을 사용한다).
|
||||
- `AOP`를 적용하면 스프링 컨테이너는 스프링이 올라올 때(스프링 빈을 등록할 때) 진짜 스프링 빈 말고 가짜 스프링 빈을 앞에 세워둔다.
|
||||
- 그리고 가짜 스프링 빈이 끝나고(?), `joinPoint.proceed()`를 호출하면 내부적인 처리를 거쳐 진짜 스프링 빈을 호출한다. 즉, 컨트롤러가 호출하는 건 진짜 서비스가 아닌 프록시라는 기술로 발생하는 가짜 서비스이다.
|
||||
- 실제로 서비스가 주입될 때 프록시(가짜 서비스)를 콘솔에서 확인할 수도 있다.
|
||||
- 콘솔 로그를 보면 `memberService = class hello.springintroduction.service.MemberService$$EnhancerBySpringCGLIB$$6974282c` 라고 해서, `EnhancerBySpringCGLIB`을 확인할 수 있다. 여기서는 `MemberService`를 복제해서 코드를 조작하는 기술이다.
|
||||
- 스프링 컨테이너가 `AOP`가 적용되면 프록시(가짜)를 통해서 `AOP`가 실행이 되고 다음으로 `joinPoint.proceed()`가 호출되면 진짜 서비스가 호출된다.
|
||||
- 컨테이너에서 스프링 빈을 관리하면 가짜를 만들어서 `DI`를 해줄 수 있다. 그렇기 때문에 `DI`함으로써(이때 프록시가 주입된다) `AOP`가 가능해진다.
|
||||
- 스프링에서는 이를 `프록시 방식의 AOP`라고 하며, 아예 자바에서 컴파일할 때 코드를 생성해서 코드를 위아래로 박아서 넣어주는 기술들도 있다(?).
|
||||
|
||||
### AOP 적용 전 전체그림
|
||||

|
||||
|
||||
### AOP 적용 후 전체그림
|
||||

|
||||
|
||||
---
|
||||
BIN
SpringBoot-Introduction/Lectures/images/@ResponseBody 사용 원리.PNG
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
SpringBoot-Introduction/Lectures/images/AOP 적용 전 의존관계.PNG
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
SpringBoot-Introduction/Lectures/images/AOP 적용 전 전체그림.PNG
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
SpringBoot-Introduction/Lectures/images/AOP 적용 후 의존관계.PNG
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
SpringBoot-Introduction/Lectures/images/AOP 적용 후 전체그림.PNG
Normal file
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
SpringBoot-Introduction/Lectures/images/MVC,-템플릿-엔진-이미지.PNG
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
SpringBoot-Introduction/Lectures/images/공통 관심 사항.PNG
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
SpringBoot-Introduction/Lectures/images/구현 클래스 추가 이미지.PNG
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
SpringBoot-Introduction/Lectures/images/동작-환경-그림.PNG
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
SpringBoot-Introduction/Lectures/images/스프링 데이터 JPA 제공 클래스.PNG
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
SpringBoot-Introduction/Lectures/images/스프링 설정 이미지.PNG
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
SpringBoot-Introduction/Lectures/images/스프링-컨테이너.PNG
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
SpringBoot-Introduction/Lectures/images/일반적인-웹-어플리케이션-계층-구조.PNG
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
SpringBoot-Introduction/Lectures/images/정적-컨텐츠-이미지.PNG
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
SpringBoot-Introduction/Lectures/images/클래스-의존관계.PNG
Normal file
|
After Width: | Height: | Size: 16 KiB |
26
SpringBoot-Introduction/build.gradle
Normal file
@@ -0,0 +1,26 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.4.1'
|
||||
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'hello'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
sourceCompatibility = '11'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
BIN
SpringBoot-Introduction/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
SpringBoot-Introduction/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
SpringBoot-Introduction/gradlew
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
SpringBoot-Introduction/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
SpringBoot-Introduction/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'spring-introduction'
|
||||
7
SpringBoot-Introduction/sql/ddl.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
drop table if exists member CASCADE;
|
||||
create table member
|
||||
(
|
||||
id bigint generated by default as identity,
|
||||
name varchar(255),
|
||||
primary key (id)
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
package hello.springintroduction;
|
||||
|
||||
import hello.springintroduction.aop.TimeTraceAop;
|
||||
import hello.springintroduction.repository.MemberRepository;
|
||||
import hello.springintroduction.service.MemberService;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SpringConfig {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
@Autowired
|
||||
public SpringConfig(MemberRepository memberRepository) {
|
||||
this.memberRepository = memberRepository;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MemberService memberService() {
|
||||
return new MemberService(memberRepository);
|
||||
}
|
||||
|
||||
// @Bean
|
||||
// public TimeTraceAop timeTraceAop() {
|
||||
// return new TimeTraceAop();
|
||||
// }
|
||||
|
||||
// @Bean
|
||||
// public MemberRepository memberRepository() {
|
||||
// return new MemoryMemberRepository();
|
||||
// return new JdbcMemberRepository(dataSource);
|
||||
// return new JdbcTemplateMemberRepository(dataSource);
|
||||
// return new JpaMemberRepository(entityManager);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package hello.springintroduction;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringIntroductionApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringIntroductionApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package hello.springintroduction.aop;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class TimeTraceAop {
|
||||
|
||||
@Around("execution(* hello.springintroduction..*(..))")
|
||||
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("START: " + joinPoint.toString());
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
long finish = System.currentTimeMillis();
|
||||
long timeMs = finish - start;
|
||||
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package hello.springintroduction.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("hello")
|
||||
public String hello(Model model) {
|
||||
model.addAttribute("data", "spring!!");
|
||||
return "hello";
|
||||
}
|
||||
|
||||
@GetMapping("hello-mvc")
|
||||
public String helloMvc(@RequestParam("name") String name, Model model) {
|
||||
model.addAttribute("name", name);
|
||||
return "hello-template";
|
||||
}
|
||||
|
||||
@GetMapping("hello-string")
|
||||
@ResponseBody
|
||||
public String helloString(@RequestParam("name") String name) {
|
||||
return "hello" + name;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package hello.springintroduction.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home() {
|
||||
return "home";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package hello.springintroduction.controller;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import hello.springintroduction.service.MemberService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class MemberController {
|
||||
|
||||
private final MemberService memberService;
|
||||
|
||||
@Autowired
|
||||
public MemberController(MemberService memberService) {
|
||||
this.memberService = memberService;
|
||||
System.out.println("memberService = " + memberService.getClass());
|
||||
}
|
||||
|
||||
@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());
|
||||
|
||||
memberService.join(member);
|
||||
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@GetMapping("/members")
|
||||
public String list(Model model) {
|
||||
List<Member> members = memberService.findMembers();
|
||||
model.addAttribute("members", members);
|
||||
return "members/memberList";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package hello.springintroduction.controller;
|
||||
|
||||
public class MemberForm {
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package hello.springintroduction.domain;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
public class Member {
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import org.springframework.jdbc.datasource.DataSourceUtils;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class JdbcMemberRepository implements MemberRepository {
|
||||
private final DataSource dataSource;
|
||||
|
||||
public JdbcMemberRepository(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member save(Member member) {
|
||||
String sql = "insert into member(name) values(?)";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql,
|
||||
Statement.RETURN_GENERATED_KEYS);
|
||||
pstmt.setString(1, member.getName());
|
||||
pstmt.executeUpdate();
|
||||
rs = pstmt.getGeneratedKeys();
|
||||
if (rs.next()) {
|
||||
member.setId(rs.getLong(1));
|
||||
} else {
|
||||
throw new SQLException("id 조회 실패");
|
||||
}
|
||||
return member;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findById(Long id) {
|
||||
String sql = "select * from member where id = ?";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
pstmt.setLong(1, id);
|
||||
rs = pstmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return Optional.of(member);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Member> findAll() {
|
||||
String sql = "select * from member";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
rs = pstmt.executeQuery();
|
||||
List<Member> members = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
members.add(member);
|
||||
}
|
||||
return members;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findByName(String name) {
|
||||
String sql = "select * from member where name = ?";
|
||||
Connection conn = null;
|
||||
PreparedStatement pstmt = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
conn = getConnection();
|
||||
pstmt = conn.prepareStatement(sql);
|
||||
pstmt.setString(1, name);
|
||||
rs = pstmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return Optional.of(member);
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
close(conn, pstmt, rs);
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection() {
|
||||
return DataSourceUtils.getConnection(dataSource);
|
||||
}
|
||||
|
||||
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (pstmt != null) {
|
||||
pstmt.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
if (conn != null) {
|
||||
close(conn);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void close(Connection conn) throws SQLException {
|
||||
DataSourceUtils.releaseConnection(conn, dataSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class JdbcTemplateMemberRepository implements MemberRepository {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public JdbcTemplateMemberRepository(DataSource dataSource) {
|
||||
jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member save(Member member) {
|
||||
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
|
||||
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("name", member.getName());
|
||||
|
||||
Number key = jdbcInsert.executeAndReturnKey(new
|
||||
MapSqlParameterSource(parameters));
|
||||
member.setId(key.longValue());
|
||||
return member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findById(Long id) {
|
||||
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
|
||||
return result.stream().findAny();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findByName(String name) {
|
||||
List<Member> result = jdbcTemplate.query("select * from member where name = ? ", memberRowMapper(), name);
|
||||
return result.stream().findAny();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Member> findAll() {
|
||||
return jdbcTemplate.query("select * from member", memberRowMapper());
|
||||
}
|
||||
|
||||
private RowMapper<Member> memberRowMapper() {
|
||||
return (rs, rowNum) -> {
|
||||
Member member = new Member();
|
||||
member.setId(rs.getLong("id"));
|
||||
member.setName(rs.getString("name"));
|
||||
return member;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Transactional
|
||||
public class JpaMemberRepository implements MemberRepository {
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public JpaMemberRepository(EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Member save(Member member) {
|
||||
entityManager.persist(member);
|
||||
return member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findById(Long id) {
|
||||
Member member = entityManager.find(Member.class, id);
|
||||
return Optional.ofNullable(member);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Member> findByName(String name) {
|
||||
List<Member> result = entityManager.createQuery("select m from Member m where m.name = :name", Member.class)
|
||||
.setParameter("name", name)
|
||||
.getResultList();
|
||||
|
||||
return result.stream().findAny();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Member> findAll() {
|
||||
return entityManager.createQuery("select m from Member m", Member.class)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.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();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
|
||||
|
||||
@Override
|
||||
Optional<Member> findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package hello.springintroduction.service;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import hello.springintroduction.repository.MemberRepository;
|
||||
|
||||
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) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 같은 이름이 있는 중복 회원X
|
||||
validateDuplicateMember(member); // 중복 회원 검증
|
||||
memberRepository.save(member);
|
||||
return member.getId();
|
||||
} finally {
|
||||
long finish = System.currentTimeMillis();
|
||||
long timeMs = finish - start;
|
||||
System.out.println("join = " + timeMs + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDuplicateMember(Member member) {
|
||||
memberRepository.findByName(member.getName())
|
||||
.ifPresent(mem -> {
|
||||
throw new IllegalArgumentException("이미 존재하는 회원입니다.");
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* 전체 회원 조회
|
||||
*/
|
||||
public List<Member> findMembers() {
|
||||
return memberRepository.findAll();
|
||||
}
|
||||
|
||||
/*
|
||||
* 아이디로 회원 조회
|
||||
*/
|
||||
public Optional<Member> findOne(Long memberId) {
|
||||
return memberRepository.findById(memberId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head><title>static content</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
</head>
|
||||
<body> 정적 컨텐츠 입니다.</body>
|
||||
</html>
|
||||
11
SpringBoot-Introduction/src/main/resources/static/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
<a href="/hello">hello</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<p th:text="'hello ' + ${name}">hello! empty</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!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>
|
||||
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,13 @@
|
||||
package hello.springintroduction;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class SpringIntroductionApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package hello.springintroduction.repository;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MemoryMemberRepositoryTest {
|
||||
|
||||
MemoryMemberRepository repository = new MemoryMemberRepository();
|
||||
|
||||
@AfterEach
|
||||
public void afterEach() {
|
||||
repository.clearStore();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void save() {
|
||||
// given
|
||||
Member member = new Member();
|
||||
member.setName("spring");
|
||||
|
||||
// when
|
||||
repository.save(member);
|
||||
|
||||
// then
|
||||
Member result = repository.findById(member.getId()).get();
|
||||
assertThat(member).isEqualTo(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByName() {
|
||||
// given
|
||||
Member member1 = new Member();
|
||||
member1.setName("spring1");
|
||||
repository.save(member1);
|
||||
|
||||
Member member2 = new Member();
|
||||
member2.setName("spring2");
|
||||
repository.save(member2);
|
||||
|
||||
// when
|
||||
Member result = repository.findByName("spring1").get();
|
||||
|
||||
// then
|
||||
assertThat(result).isEqualTo(member1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAll() {
|
||||
// given
|
||||
Member member1 = new Member();
|
||||
member1.setName("spring1");
|
||||
repository.save(member1);
|
||||
|
||||
Member member2 = new Member();
|
||||
member2.setName("spring2");
|
||||
repository.save(member2);
|
||||
|
||||
// when
|
||||
List<Member> result = repository.findAll();
|
||||
|
||||
// then
|
||||
assertThat(result.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package hello.springintroduction.service;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import hello.springintroduction.repository.MemberRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.Commit;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class MemberServiceIntegrationTest {
|
||||
|
||||
@Autowired MemberService memberService;
|
||||
@Autowired MemberRepository memberRepository;
|
||||
|
||||
@Test
|
||||
void 회원가입() {
|
||||
// given
|
||||
Member member = new Member();
|
||||
member.setName("spring");
|
||||
|
||||
// when
|
||||
Long saveId = memberService.join(member);
|
||||
|
||||
// then
|
||||
Member findMember = memberService.findOne(saveId).get();
|
||||
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);
|
||||
assertThatThrownBy(() -> memberService.join(member2))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("이미 존재하는 회원입니다.");
|
||||
|
||||
// then
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package hello.springintroduction.service;
|
||||
|
||||
import hello.springintroduction.domain.Member;
|
||||
import hello.springintroduction.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.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
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 중복_회원_예외() {
|
||||
// given
|
||||
Member member1 = new Member();
|
||||
member1.setName("spring");
|
||||
|
||||
Member member2 = new Member();
|
||||
member2.setName("spring");
|
||||
|
||||
// when
|
||||
memberService.join(member1);
|
||||
assertThatThrownBy(() -> memberService.join(member2))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("이미 존재하는 회원입니다.");
|
||||
|
||||
// then
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void findMembers() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOne() {
|
||||
}
|
||||
}
|
||||