Spring Boot DBCP
- DBCP : Data Base Connection Pool
- 매번 Connection을 만드는 것이 아니라 서버가 감당할 수 있을 만큼의 Connection Pool을 미리 만들어두고 이를 사용-반환 하면서 데이터베이스를 사용하는 것
- Spring boot 프로젝트는 기본적으로 HikariCP를 사용한다.
- 설정 항목 application.propertites
- DB url, username, password(필수로 작성)
- driverClassName : jdbc class name (사용 DBMS에 따라서 달라짐)
- connectionTimeout : default (30초)
- maximumPoolSize : 최대 Connection 수 default 10
Database libraries
- JPA, MyBatis 등의 라이브러리를 사용한다
- 반복적으로 작업해야 하는 getConnection, close 등을 자동화해서 실제로 의미 있는 SQL 문과 Java 코드에서 필요한 객체의 타입에 집중할 수 있게 해준다.
- MyBatis : XML 로 SQL 문과 객체를 주로 정의한다. Spring 초기부터 사용
- JPA : ORM 라이브러리로 Java class와 테이블을 맵핑하여 사용한다.
ORM(Object Relational Mapping)
- 관계형 데이터베이스를 객체로 연결해 준다.
- 여러 언어에서 ORM을 지원한다.
- JPA(Java Persistence API)
- 자바 언어에서 지원해야 할 ORM 기능을 정의 - interface
- Hibernate : JPA의 구현체 중 하나
- Spring-data-jpa : JPA를 사용하기 쉽게 스프링에서 제공하는 프레임워크
JPA와 DB 드라이버 의존성 추가
이클립스 기준
프로젝트 최상단 우클릭 > Spring > Add Starters
- Spring Data JPA + 자신의 DBMS Driver
직접 추가
- build.gradle 파일에서 수정
dependencies {
......
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
......
}
해당 코드는 MariaDB 기준
데이터베이스 관련 설정
프로젝트 > src > main > resources > application.properties 파일 수정
# JPA 초기화 설정
# update - 없으면 create, 있는데 수정 사항이 있으면 alter table
# create - 매번 생성을 시도
# create-drop - 꺼질 때태이블 삭제
# none - 아무 것도 안함.
spring.jpa.hibernate.ddl-auto=update
# Database
# mariadb
spring.datasource.url=jdbc:mariadb://localhost:3306/데이터베이스이름
# mysql
#spring.datasource.url=jdbc:mysql://localhost:3306/데이터베이스이름
spring.datasource.username=DB_ID
spring.datasource.password=DB_PW
# SQL Debug
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
JPA의 구성 요소
- Entity
- 테이블의 맵핑되는 객체
- @Entity 어노테이션으로 표기한다.
- Dto가 View 혹은 API에서 사용되는 객체라면 Entity는 도메인 본연의 데이터에 가깝다.
- Repository
- Entity 객체를 이용해 실제 CRUD 연산을 수행해주는 객체
- interface로 선언하면 Spring data jpa가 해당 내용을 수행해준다.
Controller에서 바로 데이터베이스에 접근하여 정보를 얻고 가공해서 가져가는 것은 위험하다.
정보를 직접 CRUD하고 가공하는 과정에서 DB 테이블에 저장된 원본의 정보가 손상될 우려가 크다
따라서 정보 변동의 위험이 큰 로직은 Service에서 진행한다.
추가로 이때 원본의 데이터를 바로 사용하지 않고 추출한 정보의 복사복인 DTO를 만들어서 로직을 조작
Entity 클래스 추가
Domain == Entity
- Entity : table에 매칭
package com.example.myschool.teacher.domain;
import ...
@Getter
@Setter
@Entity
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer teacherId;
@Column(length = 50)
private String name;
@Column(length = 200)
private String description;
// 선생님을 조회했을 때 해당 선생님의 수업 목록을 보기 위해 같이 불러오는 의미
@OneToMany(mappedBy="teacher")
private List<Lecture> lectures;
}
@OneToMany
- Teacher 하나가 여러 개의 수업을 가질 수 있다.
- 1:N 관계
@Getter
@Setter
@Entity
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer lectureId;
@Column(length = 50)
private String title;
@Column(length = 200)
private String description;
//
@ManyToOne
@JoinColumn(name="teacher_id")
private Teacher teacher;
}
@ManyToOne
- 수업 여러개가 선생님 하나에 대응된다.
- N:1 관계
- @JoinColumn을 이용하여 Teacher 클래스가 다대일 관계의 소유측이고 데이터베이스의 teacher_id 열에 매핑됨을 의미
Repository 만들기
- Repository : SQL 매칭
package com.example.myschool.teacher.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.myschool.teacher.domain.Teacher;
public interface TeacherRepository extends JpaRepository<Teacher, Integer> {
}
- JPA를 사용할 때는 Repository를 생성만 해두어도 기본적인 CRUD 함수가 추가됩니다.
- findAll, findById, save 등등
<Teacher, Integer>
Entity class, Primary key class
Repository 기능 추가
- 전체 검색은 자동으로 제공하므로 필요한 기능만 추가한다.
- 이름 검색 - 주어진 이름 포함, 대소문자 무시
- Spring data jpa는 함수 이름을 이용해 SQL문을 생성한다
public interface TeacherRepository extends JpaRepository<Teacher, Integer>{
public List<Teacher> findByNameContainsIgnoreCase(String name);
}
public List findByNameContainsIgnoreCase(String name);
- findBy : 데이터를 찾는다
- Name : DB 열 이름(Snake Case 사용)
- Contains : 일치하는
- IgnoreCase : 대소문자 구분 안함
Service 작성
- Service 레이어가 왜 필요한가?
- DB에서 쿼리된 원본 데이터를 모두 response할 필요는 없다
- 각 Controller에서 Repository 함수를 각각 구현할 경우 Repository에 대한 의존도가 높아지고 비슷한 코드가 반복될 수 있다.
- service에서 Entity - Dto 변환을 수행하면 된다.
- Client 요청에 대한 올바른 정보를 제공하기 위한 처리 → “비즈니스 로직을 수행한다”
package com.example.myschool.lecture.service;
import org.springframework.stereotype.Service;
import com.example.myschool.lecture.domain.Lecture;
import com.example.myschool.lecture.dto.LectureDto;
import com.example.myschool.lecture.repository.LectureRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // 필수 필드가 있는 생성자를 자동으로 생성
@Service // Spring 에서 Service 클래스이라는 걸 명시하는 어노테이션
public class LectureService {
private final LectureRepository lectureRepository; // 생성자 주입을 통해 주입
private LectureDto entityToDto(Lecture lecture) { // Entity 객체를 DTO로 변환하는 메소드
return LectureDto.builder()
.lectureId(lecture.getLectureId())
.descriptiton(lecture.getDescription())
.title(lecture.getTitle())
.teacherId(lecture.getTeacher().getTeacherId())
.build();
}
public List<LectureDto> getLectures(String title) { // 수업목록을 리스트로 가져오는 메소드
ArrayList<LectureDto> lectures = new ArrayList<LectureDto>(); // 수업목록을 담을 리스트 생성
if(title == null) { // 파라미터가 null 일때는
lectureRepository.findAll().forEach(lecture->{ // 수업목록에 대해 모든 결과를 가져온다
lectures.add(entityToDto(lecture)); // Entity를 DTO로 변환하여 수업목록 리스트에 넣는다.
});
} else {
// 파라미터가 있으면 Title Column에 대해 대소문자 구분을 하지 않고 검색결과 값을
lectureRepository.findByTitleContainsIgnoreCase(title).forEach(lecture->{
lectures.add(entityToDto(lecture)); // Entity를 DTO로 변환하여 수업 목록 리스트에 넣는다.
});
}
return lectures; // 수업 목록 리스트 반환
}
public LectureDto getLecture(int id) { // id 값과 동일한 수업목록을 찾는 메소드
Optional<Lecture> result = lectureRepository.findById(id); // 결과가 없거나 하나일 때 Opional 사용
// lecture Entity의 Primary Key에 대해 파라미터 id와 동일한 값 검색
if (result.isPresent()) { // 존재하면
return entityToDto(result.get()); // Entity를 DTO로 변환하여 반환
} else {
return null;
}
}
}
Controller에서 사용
@RequiredArgsConstructor
@RestController
public class LectureController {
private final LectureService lectureService; // Serivce를 사용하기 위해 객체를 만들어줌
@GetMapping("/lectures")
public List<LectureDto> getLectures(
@RequestParam(name="title", required=false) String title) {
return lectureService.getLectures(title); // 메소드 호출
}
@GetMapping("/lectures/{id}")
public LectureDto getLecture(@PathVariable("id") int id) {
return lectureService.getLecture(id); // 메소드 호출
}
}
Spring Boot DBCP
- DBCP : Data Base Connection Pool
- 매번 Connection을 만드는 것이 아니라 서버가 감당할 수 있을 만큼의 Connection Pool을 미리 만들어두고 이를 사용-반환 하면서 데이터베이스를 사용하는 것
- Spring boot 프로젝트는 기본적으로 HikariCP를 사용한다.
- 설정 항목 application.propertites
- DB url, username, password(필수로 작성)
- driverClassName : jdbc class name (사용 DBMS에 따라서 달라짐)
- connectionTimeout : default (30초)
- maximumPoolSize : 최대 Connection 수 default 10
Database libraries
- JPA, MyBatis 등의 라이브러리를 사용한다
- 반복적으로 작업해야 하는 getConnection, close 등을 자동화해서 실제로 의미 있는 SQL 문과 Java 코드에서 필요한 객체의 타입에 집중할 수 있게 해준다.
- MyBatis : XML 로 SQL 문과 객체를 주로 정의한다. Spring 초기부터 사용
- JPA : ORM 라이브러리로 Java class와 테이블을 맵핑하여 사용한다.
ORM(Object Relational Mapping)
- 관계형 데이터베이스를 객체로 연결해 준다.
- 여러 언어에서 ORM을 지원한다.
- JPA(Java Persistence API)
- 자바 언어에서 지원해야 할 ORM 기능을 정의 - interface
- Hibernate : JPA의 구현체 중 하나
- Spring-data-jpa : JPA를 사용하기 쉽게 스프링에서 제공하는 프레임워크
JPA와 DB 드라이버 의존성 추가
이클립스 기준
프로젝트 최상단 우클릭 > Spring > Add Starters
- Spring Data JPA + 자신의 DBMS Driver
직접 추가
- build.gradle 파일에서 수정
dependencies {
......
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
......
}
해당 코드는 MariaDB 기준
데이터베이스 관련 설정
프로젝트 > src > main > resources > application.properties 파일 수정
# JPA 초기화 설정
# update - 없으면 create, 있는데 수정 사항이 있으면 alter table
# create - 매번 생성을 시도
# create-drop - 꺼질 때태이블 삭제
# none - 아무 것도 안함.
spring.jpa.hibernate.ddl-auto=update
# Database
# mariadb
spring.datasource.url=jdbc:mariadb://localhost:3306/데이터베이스이름
# mysql
#spring.datasource.url=jdbc:mysql://localhost:3306/데이터베이스이름
spring.datasource.username=DB_ID
spring.datasource.password=DB_PW
# SQL Debug
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
JPA의 구성 요소
- Entity
- 테이블의 맵핑되는 객체
- @Entity 어노테이션으로 표기한다.
- Dto가 View 혹은 API에서 사용되는 객체라면 Entity는 도메인 본연의 데이터에 가깝다.
- Repository
- Entity 객체를 이용해 실제 CRUD 연산을 수행해주는 객체
- interface로 선언하면 Spring data jpa가 해당 내용을 수행해준다.
Controller에서 바로 데이터베이스에 접근하여 정보를 얻고 가공해서 가져가는 것은 위험하다.
정보를 직접 CRUD하고 가공하는 과정에서 DB 테이블에 저장된 원본의 정보가 손상될 우려가 크다
따라서 정보 변동의 위험이 큰 로직은 Service에서 진행한다.
추가로 이때 원본의 데이터를 바로 사용하지 않고 추출한 정보의 복사복인 DTO를 만들어서 로직을 조작
Entity 클래스 추가
Domain == Entity
- Entity : table에 매칭
package com.example.myschool.teacher.domain;
import ...
@Getter
@Setter
@Entity
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer teacherId;
@Column(length = 50)
private String name;
@Column(length = 200)
private String description;
// 선생님을 조회했을 때 해당 선생님의 수업 목록을 보기 위해 같이 불러오는 의미
@OneToMany(mappedBy="teacher")
private List<Lecture> lectures;
}
@OneToMany
- Teacher 하나가 여러 개의 수업을 가질 수 있다.
- 1:N 관계
@Getter
@Setter
@Entity
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer lectureId;
@Column(length = 50)
private String title;
@Column(length = 200)
private String description;
//
@ManyToOne
@JoinColumn(name="teacher_id")
private Teacher teacher;
}
@ManyToOne
- 수업 여러개가 선생님 하나에 대응된다.
- N:1 관계
- @JoinColumn을 이용하여 Teacher 클래스가 다대일 관계의 소유측이고 데이터베이스의 teacher_id 열에 매핑됨을 의미
Repository 만들기
- Repository : SQL 매칭
package com.example.myschool.teacher.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.myschool.teacher.domain.Teacher;
public interface TeacherRepository extends JpaRepository<Teacher, Integer> {
}
- JPA를 사용할 때는 Repository를 생성만 해두어도 기본적인 CRUD 함수가 추가됩니다.
- findAll, findById, save 등등
<Teacher, Integer>
Entity class, Primary key class
Repository 기능 추가
- 전체 검색은 자동으로 제공하므로 필요한 기능만 추가한다.
- 이름 검색 - 주어진 이름 포함, 대소문자 무시
- Spring data jpa는 함수 이름을 이용해 SQL문을 생성한다
public interface TeacherRepository extends JpaRepository<Teacher, Integer>{
public List<Teacher> findByNameContainsIgnoreCase(String name);
}
public List findByNameContainsIgnoreCase(String name);
- findBy : 데이터를 찾는다
- Name : DB 열 이름(Snake Case 사용)
- Contains : 일치하는
- IgnoreCase : 대소문자 구분 안함
Service 작성
- Service 레이어가 왜 필요한가?
- DB에서 쿼리된 원본 데이터를 모두 response할 필요는 없다
- 각 Controller에서 Repository 함수를 각각 구현할 경우 Repository에 대한 의존도가 높아지고 비슷한 코드가 반복될 수 있다.
- service에서 Entity - Dto 변환을 수행하면 된다.
- Client 요청에 대한 올바른 정보를 제공하기 위한 처리 → “비즈니스 로직을 수행한다”
package com.example.myschool.lecture.service;
import org.springframework.stereotype.Service;
import com.example.myschool.lecture.domain.Lecture;
import com.example.myschool.lecture.dto.LectureDto;
import com.example.myschool.lecture.repository.LectureRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // 필수 필드가 있는 생성자를 자동으로 생성
@Service // Spring 에서 Service 클래스이라는 걸 명시하는 어노테이션
public class LectureService {
private final LectureRepository lectureRepository; // 생성자 주입을 통해 주입
private LectureDto entityToDto(Lecture lecture) { // Entity 객체를 DTO로 변환하는 메소드
return LectureDto.builder()
.lectureId(lecture.getLectureId())
.descriptiton(lecture.getDescription())
.title(lecture.getTitle())
.teacherId(lecture.getTeacher().getTeacherId())
.build();
}
public List<LectureDto> getLectures(String title) { // 수업목록을 리스트로 가져오는 메소드
ArrayList<LectureDto> lectures = new ArrayList<LectureDto>(); // 수업목록을 담을 리스트 생성
if(title == null) { // 파라미터가 null 일때는
lectureRepository.findAll().forEach(lecture->{ // 수업목록에 대해 모든 결과를 가져온다
lectures.add(entityToDto(lecture)); // Entity를 DTO로 변환하여 수업목록 리스트에 넣는다.
});
} else {
// 파라미터가 있으면 Title Column에 대해 대소문자 구분을 하지 않고 검색결과 값을
lectureRepository.findByTitleContainsIgnoreCase(title).forEach(lecture->{
lectures.add(entityToDto(lecture)); // Entity를 DTO로 변환하여 수업 목록 리스트에 넣는다.
});
}
return lectures; // 수업 목록 리스트 반환
}
public LectureDto getLecture(int id) { // id 값과 동일한 수업목록을 찾는 메소드
Optional<Lecture> result = lectureRepository.findById(id); // 결과가 없거나 하나일 때 Opional 사용
// lecture Entity의 Primary Key에 대해 파라미터 id와 동일한 값 검색
if (result.isPresent()) { // 존재하면
return entityToDto(result.get()); // Entity를 DTO로 변환하여 반환
} else {
return null;
}
}
}
Controller에서 사용
@RequiredArgsConstructor
@RestController
public class LectureController {
private final LectureService lectureService; // Serivce를 사용하기 위해 객체를 만들어줌
@GetMapping("/lectures")
public List<LectureDto> getLectures(
@RequestParam(name="title", required=false) String title) {
return lectureService.getLectures(title); // 메소드 호출
}
@GetMapping("/lectures/{id}")
public LectureDto getLecture(@PathVariable("id") int id) {
return lectureService.getLecture(id); // 메소드 호출
}
}