Spring Boot DB 연동
프로젝트 순서
-
- 크롬 - JSON Formatter 확장하기
- 2. @ResponseBody 로 return
- AppService - read all 부분 수정
// READ ALL
public List<StudentEntity> readStudentAll() {
System.out.println(
this.studentRepository.findAll());
List<StudentEntity> studentEntityList= this.studentRepository.findAll();
return studentEntityList;
}
- appcontroller - read all 마찬가지로 수정
@GetMapping("read-all")
public @ResponseBody List<StudentEntity> readAll() {
this.service.readStudentAll();
// return "done-read-all";
return this.service.readStudentAll();
}
3.실행 → http://localhost:8080/read-all
dto 패키지 생성 → StudentDto클래스 생성
- @Data 어노테이션 생성
package com.example.jpa.dto;
import lombok.Data;
@Data
public class StudentDto {
private Long id; //Entity.id
private String name; //Entity.name
private String email; //Entity.email
}
@ResponseBody로 return
- AppService의 readStudentAlll() 작성
- List<StudentEntity>를 반환하게 해보자.
- Controller도 @ResponseBody List<StudentEntity>로 바꾸자
// READ ALL
//public List<StudentEntity> readStudentAll() {
public List<StudentDto> readStudentAll(){
System.out.println(
this.studentRepository.findAll());
List<StudentEntity> studentEntityList =
this.studentRepository.findAll();
List<StudentDto> studentDtoList = new ArrayList<>();
for (StudentEntity studentEntity :
this.studentRepository.findAll()) {
StudentDto studentDto = new StudentDto();
studentDto.setId(studentEntity.getId());
studentDto.setName(studentEntity.getName());
studentDto.setEmail(studentEntity.getEmail());
studentDtoList.add(studentDto);
}
// return studentEntityList;
return studentDtoList;
}
- app controller 코드 수정
@GetMapping("read-all")
public @ResponseBody List<StudentDto> readAll() {
this.service.readStudentAll();
// return "done-read-all";
return this.service.readStudentAll();
}
실행(json fommater를 다운받았기 때문에 데이터가 구조화되어서 실행된다)
Entity 가리기 - DTO
Entity를 그대로 사용자에게 돌려주게 되면 사용자 정보들이 그대로 노출 되기 때문에 Entity를 가리려고한다.
Data Transder Object
- 일종의 디자인패턴
- View에서 사용하는 데이터와 Model이 관리하는 데이터의 형태를 분리
- Entity가 변화했을 때 DTO를 사용하는 View에 영향이 줄어든다.
- Entity에서 사용자에게 노출되는 정보를 조절할 수 있다.(정보은닉)
-StudentDto 생성(dto 패키지 생성 후 StudentDto 클래스 생성)
- 객체의 생성을 담당하는 클래스 메소드
- 객체 내부의 메소드를 통해 새로운 객체를 만드는 방법이다.
import com.example.jpa.entities.StudentEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentDto {
private Long id; //Entity.id
private String name; //Entity.name
private String email; //Entity.email
//studentDto가 entity정보가 있을 때 새로운 dto가 만들어지게끔 할 수 있음
// 정적 팩토리 메소드
// 유지보수, 재사용성 측면에서 좋음
public static StudentDto
fromEntity(StudentEntity studentEntity){
//내부에서 새로운 dto를 만들어줌
StudentDto dto = new StudentDto();
dto.setId(studentEntity.getId());
dto.setName(studentEntity.getName());
dto.setEmail(studentEntity.getEmail());
return dto;
}
}
- DTO는 윗부분만 적어도 되지만 StudentDto fromEntity()를 추가
- 이런식으로 static을 이용하여 static factory method 구조를 만들 수 있음
- static factory method
- 객체의 생성을 담당하는 클래스 메소드
- 객체 내부의 메소드를 통해 새로운 객체를 만드는 방법
- static factory method
AppService // READ ALL 부분 작성
// READ ALL
public List<StudentDto> readStudentAll() {
List<StudentEntity> studentEntityList = this.studentRepository.findAll();
List<StudentDto> studentDtoList = new ArrayList<>();
for (StudentEntity studentEntity : studentEntityList) {
studentDtoList.add(StudentDto.fromEntity(studentEntity));
}
return studentDtoList;
}
- static factory method 구조를 활용하면
- studentEntity에서 studentDto로 바꾸는 일은 빈번하게 일어날텐데, 매번 new 해서 set하는건 번거로우일
- studentDto가 entity정보가 있을때 새로운 dto가 만들어지게끔 할 수 있음
- Skeleton 프로젝트
- 연동, Entity, Interface
- 연동하기
클론을 생성하기 위해 강사님 깃 주소 https://github.com/edujeeho/likelion-backend-5.git 를 이용하여 깃 배쉬에서 다운 받을 폴더로 이동하여 다음과 같이 한다
-
$ git clone <https://github.com/edujeeho/likelion-backend-5.git>
- 그 폴더에 클론이 생기게 되면 원하는 프로젝트를 인텔리제이로 열어주면 된다.
- Entity
data를 넣어주기 위해 StudentEntity 를 생성해줌
빌드 성공 화면
- Entity : data를 넣어주기 위해 StudentEntity 를 생성해줌
package com.example.student.entity;
/* CREATE TABLE students (
* id INTEGER PRIMARY KEY AUTOINCREMENT,
* name TEXT,
* age INTEGER,
* phone TEXT,
* email TEXT
* */
import jakarta.persistence.*;
import lombok.Data;
//테이블생성
@Data
@Entity
@Table(name = "students")
public class StudentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String phone;
private String email;
}
- database 연결 (sqlite-’db.sqlite’생성)
위에 StudentEntity 에서 생성한 ‘students’ 테이블을 database 에서 db를 불러오면 생성 된 걸 확인할 수 있다.
만약 바로 reload가 되지 않는다면 db → drop한 뒤 새로 만들고 load하면 생성이 된걸 확인할 수 있다.
Interface
- JpaRepository<StudentEntity, Long> 인터페이스를 상속받는 StudentRepository를 구현해줌
- StudentRepository 인터페이스 생성
package com.example.student.repository;
import com.example.student.entity.StudentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentRepository extends JpaRepository<StudentEntity, Long> {
}
Service
- JpaRepository에 대한 의존성 주입을 하여 해당 Service에서 JpaRepository를 사용할 수 있도록 한다.
@Service
public class StudentService {
private final StudentRepository repository;
public StudentService(StudentRepository repository) {
this.repository = repository;
}
- 생성자 주입 시 final을 사용하는 이유는?
- final 키워드를 사용하여 생성자에 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아줄 수 있다.
- 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없고 오직 생성자 주입 방식에만 final 키워드를 사용할 수 있다.
- 또한 final을 사용하게 되면 lombok에 @RequiredArgsConstructor를 사용하여 자동으로 생성자 주입을 사용할 수 있다.
Controller
@Controller
@RequestMapping("/students")
public class StudentController {
private final StudentService service;
public StudentController(StudentService service){
this.service = service;
}
- Class에 @RequestMapping을 사용하는 이유는?
- URL은 계층 구조로 이루어져 있는데 클래스에 RequestMapping을 사용하면 반복되는 코드와 오타의 위험성이 줄어들고 해당 클래스에서 관리하는 URL을 한정시켜 정의한 URL의 하위 계층만 관리하게 할 수 있다.
HTTP 상태코드란?
- TTP Status Code(HTTP 상태 코드)는 클라이언트가 보낸 HTTP 요청에 대한 서버의 응답을 코드로 표현한 것으로 요청에 대한 성공 실패 실패요인 등을 알 수 있다.
- 종류
- 1xx(Informational) : 요청이 수신되어 처리중
- 2xx(Successful) : 요청 정상 처리
- 3xx(Redirection) : 요청을 완료하려면 추가 행동이 필요
- 4xx(Client Error): 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음
- 404(Not Found)
- 요청 리소스를 찾을 수 없음
- 요청 리소스가 서버에 없거나 클라이언트가 권한이 부족한 리소스에 접근할 때 해당 리소스를 숨기고 싶을 때
- 404(Not Found)
- 5xx(Server Error) : 서버오류, 서버가 정상 요청을 처리하지못함
CREATE
- form에서 넘겨받은 데이터의 값을 DTO로 넘겨받아 save()메소드를 통해 DB에 저장한다.
- 이때 저장한 후 redirect로 상세보기 페이지로 리턴해주어야 한다!
- PRG(POST-Redirect-GET)
- POST로 데이터를 전송 후 브라우저를 새로고침 하게되면 서버에 다시 요청되어 중복 요청이 된다.
- 이렇게 되면 중복 된 데이터가 계속 들어가게 된다.
- 이러한 것을 막기 위해 PRG를 사용해서 POST로 데이터 전송 후 GET 메소드로 리다이렉트 하여 데이터가 이중으로 전송되는 것을 막는다.
- URL이 이미 POST→ GET으로 변경, 실수로 새로고침 해도 GET으로 결과 화면만 조회!
// CREATE public StudentDto createStudent(StudentDto dto) { StudentEntity entity = new StudentEntity(); entity.setName(dto.getName()); entity.setAge(dto.getAge()); entity.setPhone(dto.getPhone()); entity.setEmail(dto.getEmail()); return StudentDto.fromEntity(this.repository.save(entity)); // 새로 등록된 DTO를 리턴 }
// Controller // 새로운 StudentEntity 생성 후 상세보기 페이지로 @PostMapping("/create") public String create(StudentDto dto) { System.out.println(dto.toString()); StudentDto newDto = service.createStudent(dto); return "redirect:/students/" + newDto.getId(); }
UPDATE
- update도 save() 메소드를 사용하여 업데이트 한다.
- 이때 update를 하게 될 때 해당 id로 받아온 객체를 회수하여 해당 데이터가 있을 때만 업데이트가 진행 되는 로직을 작성하는것이 좋다.
- 업데이트가 완료되면 업데이트 완료된 객체를 넘겨 해당 업데이트된 객체를 화면에 보여준다.
- PRG를 통해 조회 페이지로 Redirect 해준다.
// UPDATE
public StudentDto updateStudent(Long id, StudentDto dto) {
// id로 해당 객체를 회수하고 save를 하는것이 좋음
Optional<StudentEntity> optionalEntity
= this.repository.findById(id);
if(optionalEntity.isPresent()){
StudentEntity targetEntity = optionalEntity.get();
targetEntity.setName(dto.getName());
targetEntity.setAge(dto.getAge());
targetEntity.setPhone(dto.getPhone());
targetEntity.setEmail(dto.getEmail());
repository.save(targetEntity);
return StudentDto.fromEntity(targetEntity);
} else throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// Controller
// id에 해당하는 StudentEntity 수정 후 상세보기 페이지로
@PostMapping("/{id}/update")
public String update(@PathVariable Long id, StudentDto dto) {
StudentDto updateDto = service.updateStudent(id, dto);
return "redirect:/students/" + updateDto.getId();
}
DELETE
- deleteById() 메소드를 사용하여 인자로 넘겨받은 ID의 값을 삭제한다.
- 이때 existsById() 메소드를 사용하여 인자로 받은 ID가 실제 DB에 존재하는지 확인 한 후 delete를 진행한다.
- delete를 하게되면 해당 데이터가 제거되기 때문에 Home으로 돌아가게 한다.
public void deleteStudent(Long id) {
// existsById
if(this.repository.existsById(id)) {
this.repository.deleteById(id);
} else throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// id에 해당하는 StudentEntity 삭제 후 홈페이지로
@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id) {
service.deleteStudent(id);
return "redirect:/students";
}