💡 학습내용
1. RDBMS의 기초 지식을 습득한다.
2. Spring Data JPA의 사용법을 습득한다.
3. REST API를 만드는 방법을 익힌다.
RDBMS RDB는 관계형 데이터베이스, 정보 저장소
H2 RDBMS의 한 종류, 서버가 켜져있는 동안에만 작동하는 RDB
SQL 데이터를 읽고, 저장하고, 변경하고, 삭제하는 구체적인 문법
JPA 스프링은 Java로 작동, Database는 SQL로 작동, Java를 위한 번역기 JPA(Spring Data JPA)는 Java로 코드를 작성하면 SQL로 번역해줄 뿐만 아니라, 기본적인 기능이 거의 완벽하게 들어있음.
Repository Repository는 JPA를 작동시키는 매개체
API서버 간의 약속인 API와 데이터를 주고받는 구체적인 방법(GET, POST, PUT, DELETE)
Lombok코드를 절약할 수 있음
DTO데이터를 주고받을 때 DTO를 반드시 이용함
RDBMS(Relational DataBase Management System)은 컴퓨터에 정보를 저장하고 관리하는 기술
종류
MySQL (현업에서 가장 많이 쓰인다.)
PostgreSQL
Oracle Database
H2 => In-memory DB, 인메모리 DB란 서버가 작동하는 동안에만 내용을 저장하고, 서버가 작동을 멈추면 데이터가 모두 삭제되는 데이터베이스
src > main > resources > application.properties 파일 안에 아래 설정하기
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
설정 후 서버 run 상태에서 아래 주소로 들어가서 Connect 누르기
http://localhost:8080/h2-console
CREATE TABLE IF NOT EXISTS courses (
id bigint(5) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
tutor varchar(255) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO courses (title, tutor) VALUES
('Spring', 'James'), ('React', 'Jane');
SELECT * FROM courses;
=> 우리가 명령을 내린 Java 명령어를 SQL로 번역해주는 것이 Spring Data JPA
SQL을 쓰지 않고 데이터를 생성, 조회, 수정, 삭제할 수 있도록 해주는 번역기
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
명령 Java로 만들 수 있음.
repository.save(new Customer("Jack", "Harper"));
Spring - MySQL
Domain - Table
Repository - SQL
DB를 사용하는데 핵심이었던 테이블, SQL과 동일한 개념의 자바 용어가 Domain과 Repository
파일 위치
package com.sparta.week02.domain;
Course.java => Class
@Entity
엔티티, 하나의 테이블임을 말함.
@Id
ID값, Primary Key로 사용하겠다는 뜻.
@GeneratedValue(strategy = GenerationType.AUTO)
자동 증가 명령
@Column(nullable = false)
컬럼 값이고, 반드시 값이 존재해야함.(nullable = false)
package com.sparta.week02.domain;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@NoArgsConstructor // 기본생성자를 대신 생성해줍니다.
@Entity // 테이블임을 나타냅니다.
public class Course {
@Id // ID 값, Primary Key로 사용하겠다는 뜻입니다.
@GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가 명령입니다.
private Long id;
@Column(nullable = false) // 컬럼 값이고 반드시 값이 존재해야 함을 나타냅니다.
private String title;
@Column(nullable = false)
private String tutor;
public String getTitle() {
return this.title;
}
public String getTutor() {
return this.tutor;
}
public Course(String title, String tutor) {
this.title = title;
this.tutor = tutor;
}
}
CouesrRepository => Interface
CourseRepository
QL 역할을 대신해주는 것, Course 에 관한 Repository.
extends
른 기능을 내가 가져와서 이용한다. JpaRepository 에 있는 기능을 Course Repository 에 내가 가져와서 쓸꺼야. 라는 뜻
JPA 는 SQL 쿼리 날리는 것, 그러면 어떤 걸 대상으로 어떤 것을 식별할 수 있는지 써줘야 함.
<Course, Long>
Course 라는 것을 사용하고 id의 형태가 Long 이다.
JPA 는 인터페이스를 통해서만 사용할 수 있다.
인터페이스란
클래스에서 멤버가 빠진 메소드 모음집.
JpaRepository
는 인터페이스라는 뜻, 멤버 변수 이런 건 없고 메소드가 많은 녀석
CourseRepository
또한 메소드만 잔쯕 모여있는 것. 그 메소드는 내가 작성하는 것이 아니고 JPA 에서 미리 작성된 것을 가져다 쓰는 것
package com.sparta.week02.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CourseRepository extends JpaRepository<Course, Long> {
}
SQL이 보이도록 application.properties 세팅
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
spring.jpa.show-sql=true
추가하기
Spring이 JPA로 작동을 할 때, SQL을 보여달라는 뜻.
Week02Application.java
package com.sparta.week02;
import com.sparta.week02.domain.CourseRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Week02Application {
public static void main(String[] args) {
SpringApplication.run(Week02Application.class, args);
}
// Week02Application.java 의 main 함수 아래에 붙여주세요.
@Bean
public CommandLineRunner demo(CourseRepository repository) {
return (args) -> {
};
}
}
SQL 연습을 위한 코드, 실제로 프로젝트를 실행할 때는 사용하지 않음.
@Bean
public CommandLineRunner demo(CourseRepository repository) {
return (args) -> {
Course course1 = new Course("Django", "Tom");
repository.save(course1);
List<Course> courseList = repository.findAll();
for (int i = 0; i < courseList.size(); i++) {
System.out.println(courseList.get(i));
}
};
}
result
Hibernate: call next value for hibernate_sequence Hibernate: insert into course (title, tutor, id) values (?, ?, ?) Hibernate: select course0_.id as id1_0_, course0_.title as title2_0_, course0_.tutor as tutor3_0_ from course course0_ com.sparta.week02.domain.Course@179ee36b
for (int i = 0; i < courseList.size(); i++) {
Course c = courseList.get(i);
System.out.println(c.getTitle());
System.out.println(c.getTutor());
}
result
Hibernate: call next value for hibernate_sequence Hibernate: insert into course (title, tutor, id) values (?, ?, ?) Hibernate: select course0_.id as id1_0_, course0_.title as title2_0_, course0_.tutor as tutor3_0_ from course course0_ Django Tom
클래스의 상속: 이미 만들어진 것 가져다가 사용하자!
class Person {
private String name;
private String getName() {
return this.name;
}
}
class Tutor extends Person {
private String address;
// Person 클래스를 상속했기 때문에,
// name 멤버변수와 getName() 메소드 사용 가능,
}
DB의 기본 중의 기본은 '생성일자', '수정일자'를 필드로 가지는 것.
Timestamped.java
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass // 상속했을 때, 컬럼으로 인식하게 합니다.
@EntityListeners(AuditingEntityListener.class) // 생성/수정 시간을 자동으로 반영하도록 설정, 생성, 수정이 일어날 때 자동으로 반영해줘!
public abstract class Timestamped {
// abstract: 추상, 직접 구현 안된다. 상속으로만 구현할 수 있다. => new Timestamped 하면 오류남.
// 무조건 상속으로, extends Timestamped 로 사용해야 한다.
@CreatedDate // 생성일자임을 나타냅니다.
private LocalDateTime createdAt;
@LastModifiedDate // 마지막 수정일자임을 나타냅니다. 수정이 될 때마다 시간이 계속 업데이트 됨.
private LocalDateTime modifiedAt;
}
Week02Application.java
...
@EnableJpaAuditing // 수정일자 반영됨.
@SpringBootApplication
public class Week02Application {
...
}
Course.java
public class Course extends Timestamped {
...
}
정보관리의 기본 기능
생성(Create), 조회(Read), 변경(Update), 삭제(Delete)
Repository의 save와 findAll 등을 이용함.
public CommandLineRunner demo(CourseRepository repository) {
return (args) -> {
// Create
// 데이터 저장하기
repository.save(new Course("React", "Sophie"));
// Read
// 데이터 전부 조회하기
List<Course> courseList = repository.findAll();
for (int i = 0; i < courseList.size(); i++) {
Course course = courseList.get(i);
System.out.println(course.getId());
System.out.println(course.getTitle());
System.out.println(course.getTutor());
}
// 데이터 하나 조회하기, 특정 값이 있는지 없는지 알 수 없음. .orElseThrow 를 통해 에러 처리
// Long 이라서 1L을 붙여줌
Course course = repository.findById(2L).orElseThrow(
() -> new NullPointerException("해당 아이디가 존재하지 않습니다.") // 가르키는 id 값이 없을 때의 예외
);
};
}
Update, Delete로 넘어가기 전에 Service 개념을 알아야 한다.
Spring의 영역
1) Controller: 가장 바깥 부분, 요청/응답을 처리함
2) Service: 중간 부분, 실제 중요한 작동이 많이 일어나는 부분.
3) Repo: 가장 안쪽 부분, DB와 맞닿아 있음(Repository, Entity)
Update는 Service 부분에 작성
package com.sparta.week02.service
CourseService.java
package com.sparta.week02.service;
import com.sparta.week02.domain.Course;
import com.sparta.week02.domain.CourseRepository;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service // 스프링에게 이 클래스는 서비스임을 명시, update 하는 작업이 있을 수 있으니깐 알아두라는 뜻.
public class CourseService {
// final: 서비스에게 꼭 필요한 녀석임을 명시
// 검색이나 업데이트 하는 경우 필요함. SQL 을 날리는 역할
// final => 이거 클래스에 꼭 필요한 거라고 표시해 주는 것. 한번 값이 부여되면 변형될 수 없음.
private final CourseRepository courseRepository;
// 생성자를 통해, Service 클래스를 만들 때 꼭 Repository 를 넣어주도록
// 스프링에게 알려줌
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
// => 생성자
// CourseRepository 를 내가 언제든지 쓸 수 있게 스프링에서 생성해서 넘겨준다.
// Update => 어떤 데이터가 업데이트 되었는지 id를 알려주는 것, 전달 받는 것은 2개의 재료가 필요함
// 1. 어떤 데이터를 업데이트 할 것인지? id 2. 업데이트할 정보를 가져오는 값
@Transactional // SQL 쿼리가 일어나야 함을 스프링에게 알려줌, 자동으로 Database 에 적용됨.
public Long update(Long id, Course course) {
// 1. id를 우선 찾고
Course course1 = courseRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
);
course1.update(course); // course 데이터의 update 매서드 활용, 내 정보를 바꿔줌.
return course1.getId();
}
}
Course.java
Course 클래스에 update 매서드 추가하기
...
public Course(String title, String tutor) {
this.title = title;
this.tutor = tutor;
}
public void update(Course course) {
this.title = course.title;
this.tutor = course.tutor;
}
...
Web02Application.java
package com.sparta.week02;
import com.sparta.week02.domain.Course;
import com.sparta.week02.domain.CourseRepository;
import com.sparta.week02.service.CourseService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import java.util.List;
@EnableJpaAuditing // 수정일자 반영됨.
@SpringBootApplication
public class Week02Application {
public static void main(String[] args) {
SpringApplication.run(Week02Application.class, args);
}
// Week02Application.java 의 main 함수 아래에 붙여주세요.
@Bean
public CommandLineRunner demo(CourseRepository courseRepository, CourseService courseService) {
return (args) -> {
// 1) Create
courseRepository.save(new Course("Vue.js", "Chloe"));
// 2) Read
System.out.println("데이터 인쇄");
List<Course> courseList = courseRepository.findAll();
for (int i=0; i<courseList.size(); i++) {
Course course = courseList.get(i);
System.out.println(course.getId());
System.out.println(course.getTitle());
System.out.println(course.getTutor());
}
// 3) Update, change title
Course new_course = new Course("React", "Chloe"); // 기존 데이터 변경하는 용도
courseService.update(1L, new_course);
// Read updated data
courseList = courseRepository.findAll();
for (int i=0; i<courseList.size(); i++) {
Course course = courseList.get(i);
System.out.println(course.getId());
System.out.println(course.getTitle());
System.out.println(course.getTutor());
}
// 4) Delete
courseRepository.deleteAll(); // delete 는 courseRepository 사용
courseRepository.deleteById(1L);
};
}
}
Update의 경우 Service를 이용한다!
왜 update만 Service에서 관리하는지?
- Repository 에 update 메소드가 없기 때문입니다.
- @Transactional 이라는 어노테이션을 통해, 찾은 객체의 데이터를 변경할 경우 DB에 반영되도록 설정한 것입니다.
delete의 경우 Repository 사용
courseRepository.deleteAll(); // delete 는 courseRepository 사용
courseRepository.deleteById(1L);
코드를 절약하기 위한 것
Java project는 굉장히 반복적으로 쓰이는 코드들이 있다.
getter, setter, 생성자 관련해서 자주 사용하는 것들이 많다.
이런 것들을 자동생성해줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리
Lombok(이하 롬복)은, 자바 프로젝트를 진행하는데 거의 필수적으로 필요한 메소드/생성자 등을 자동생성해줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리입니다.
annotation을 더 잘 작성할 수 있도록 인텔리J에서 도와달라는 뜻.
사용
package com.sparta.week02.domain;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter // Lombok 의 대상
@NoArgsConstructor // 기본생성자를 대신 생성해줍니다. Lombok 의 대상
@Entity // 테이블임을 나타냅니다.
public class Course extends Timestamped{
@Id // ID 값, Primary Key로 사용하겠다는 뜻입니다.
@GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가 명령입니다.
private Long id;
@Column(nullable = false) // 컬럼 값이고 반드시 값이 존재해야 함을 나타냅니다.
private String title;
@Column(nullable = false)
private String tutor;
public Course(String title, String tutor) {
this.title = title;
this.tutor = tutor;
}
public void update(Course course) {
this.title = course.title;
this.tutor = course.tutor;
}
}
package com.sparta.week02.service;
import com.sparta.week02.domain.Course;
import com.sparta.week02.domain.CourseRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@RequiredArgsConstructor // Lombok, 아무 매서드에서나 변수룰 사용할 수 있게 된다.
@Service // 스프링에게 이 클래스는 서비스임을 명시, update 하는 작업이 있을 수 있으니깐 알아두라는 뜻.
public class CourseService {
// final: 서비스에게 꼭 필요한 녀석임을 명시, 꼭 필요한 변수는 final 로 선언을 해주고
private final CourseRepository courseRepository;
@Transactional // SQL 쿼리가 일어나야 함을 스프링에게 알려줌, 자동으로 Database 에 적용됨.
public Long update(Long id, Course course) {
Course course1 = courseRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
);
course1.update(course);
return course1.getId();
}
}
Data Transfer Object
데이터를 전달하고 주고 받을 때는 기존에 있는 클래스 사용하지 말자.
Course
클래스를 막 이용해도 될까요? 전달하는 용도로?
우리가 코스 클래스를 이용할 때는 직접 저장할 때나, 찾아서 프론트 클라이언트에 넘겨줄때나 이럴 때 활용하려고 하는 것.
변경을 하려는데 변경을 할 데이터를 가지고 다니는 매개체로 Course
클래스를 사용하면 안좋다.
Why?
내가 아닌 다른 사람이 변경을 실수로 하게 되면? 시스템이 오류가 날 가능성이 커짐
코스 클래스를 건드리는 것은 DB가 변경될 가능성이 커짐
DB에 연결된 클래스는 그대로 두고 우리가 정보를 물고 다니는 매개체를 따로 만들 필요성이 부각됨.
=> DTO
Update의 경우
코스에 정보를 변경해 달라고 요청을 하는 것
CourseRequestDto
Course Request 정보를 들고 다닐 매개체
CourseRequestDto.java
src > main > java > com.sparta.week02 > domain 에 CourseRequestDto 파일 생성
package com.sparta.week02.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@RequiredArgsConstructor
public class CourseRequestDto {
private final String title;
private final String tutor;
}
CourseService.java
package com.sparta.week02.service;
...
@RequiredArgsConstructor // Lombok, 아무 매서드에서나 변수룰 사용할 수 있게 된다.
@Service // 스프링에게 이 클래스는 서비스임을 명시, update 하는 작업이 있을 수 있으니깐 알아두라는 뜻.
public class CourseService {
// final: 서비스에게 꼭 필요한 녀석임을 명시, 꼭 필요한 변수는 final 로 선언을 해주고
private final CourseRepository courseRepository;
@Transactional
public Long update(Long id, CourseRequestDto requestDto) {
Course course1 = courseRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
);
course1.update(requestDto);
return course1.getId();
}
}
Course.java
package com.sparta.week02.domain;
...
@Getter // Lombok 의 대상
@NoArgsConstructor // 기본생성자를 대신 생성해줍니다. Lombok 의 대상
@Entity // 테이블임을 나타냅니다.
public class Course extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String tutor;
public Course(String title, String tutor) {
this.title = title;
this.tutor = tutor;
}
public void update(CourseRequestDto requestDto) {
this.title = requestDto.getTitle();
this.tutor = requestDto.getTutor();
}
}
Week02Application.java
...
// 3) Update, change title
// Course new_course = new Course("React", "Chloe"); // 기존 데이터 변경하는 용도
// 데이터를 들고 다닐 용도이기 때문에 클래스를 활용해서는 안된다.
// Dto 활용
CourseRequestDto requestDto = new CourseRequestDto("React", "Chloe");
courseService.update(1L,requestDto);
// Read updated data
courseList = courseRepository.findAll();
for (int i=0; i<courseList.size(); i++) {
Course course = courseList.get(i);
System.out.println(course.getId());
System.out.println(course.getTitle());
System.out.println(course.getTutor());
}
...
클라이언트와 서버 간의 약속.
클라이언트가 정한대로 서버에게 요청(Request)
서버가 요구사항을 처리하여 응답(Response)
REST란, 주소에 명사, 요청 방식에 동사를 사용함으로써 의도를 명확히 드러냄.
POST, GET, PUT, DELETE 요청을 하는 것.
주소에 들어가는 명사들은 복수형, /courses
주소에 동사는 가급적 사용하지 않는다.
ex)
CourseController.java
package com.sparta.week02.controller;
import com.sparta.week02.domain.Course;
import com.sparta.week02.domain.CourseRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequiredArgsConstructor // final 이 필요하면 반드시 사용할 때 넣어주라는 말 Lombok
@RestController // JSON 으로 응답을 해야하니까
public class CourseController {
private final CourseRepository courseRepository; // 꼭 필요한 변수의 경우 final
@GetMapping("/api/courses")
public List<Course> getCourses() {
return courseRepository.findAll();
}
}
ARC(Advanced REST Client) 툴 사용
생성 요청
...
@RequiredArgsConstructor // final 이 필요하면 반드시 사용할 때 넣어주라는 말 Lombok
@RestController // JSON 으로 응답을 해야하니까
public class CourseController {
private final CourseRepository courseRepository; // 꼭 필요한 변수의 경우 final
private final CourseService courseService;
// PostMapping을 통해서, 같은 주소라도 방식이 다름을 구분합니다.
@PostMapping("/api/courses")
public Course createCourse(@RequestBody CourseRequestDto requestDto) {
// requestDto 는, 생성 요청을 의미합니다.
// 강의 정보를 만들기 위해서는 강의 제목과 튜터 이름이 필요하잖아요?
// 그 정보를 가져오는 녀석입니다.
// 저장하는 것은 Dto가 아니라 Course이니, Dto의 정보를 course에 담아야 합니다.
// 잠시 뒤 새로운 생성자를 만듭니다.
Course course = new Course(requestDto);
// JPA를 이용하여 DB에 저장하고, 그 결과를 반환합니다.
return courseRepository.save(course);
}
@GetMapping("/api/courses")
public List<Course> getCourses() {
return courseRepository.findAll();
}
}
JSON 형식이 데이터를 주고 받는 대표적인 형식
수정 요청
@RequiredArgsConstructor
@RestController
public class CourseController {
private final CourseRepository courseRepository;
private final CourseService courseService;
@PostMapping("/api/courses")
public Course createCourse(@RequestBody CourseRequestDto requestDto) {
Course course = new Course(requestDto);
return courseRepository.save(course);
}
@GetMapping("/api/courses")
public List<Course> getCourses() {
return courseRepository.findAll();
}
@PutMapping("/api/courses/{id}")
public Long updateCourse(@PathVariable Long id, @RequestBody CourseRequestDto requestDto) {
return courseService.update(id, requestDto);
}
}
/api/courses/{id}
id
path를 통해 전달한 값
requestDto
body값
변경 내용 조회
@DeleteMapping("/api/courses/{id}")
public Long deleteCourse(@PathVariable Long id) {
courseRepository.deleteById(id);
return id;
}
한줄 정리
Controller: 자동 응답기
Service: Update할 때 쓰는 것
Repository: 직접 쿼리를 날리는 것