Jpa를 사용하기 이전에, Mysql에 접근하면서 데이터들을 저장하고 조작하는 아키텍쳐와 방법에 대해 작성하고자 한다.
기본적으로 SpringBoot는 MVC이기 때문에, Controller가 RestApi의 역할을 맡게 되는데, 이때 통신한 데이터를 저장하거나 특정 로직을 통해 Servicing을 하는 기능까지 모두 맡게 된다면 유지보수성이 급감하는 결과를 초래한다.
따라서 다음 그림과 같이 Layer를 총 3개로 나눠서 각각 역할을 분담한다.
Controller가 Web Layer역할을 맡고, 데이터를 저장하는 등의 JdbcTemplate은 Repository Layer의 역할로 분리를 하는 것이다.
JdbcTemplate이란, 자바에서 SQL을 이용해 Mysql등의 DB를 제어할 수 있도록 하는 일종의 인터페이스이다. (ORM은 아니다..)
// com/course/courses/domain/Course
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Course {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String name;
private String author;
}
이와같은 도메인을 먼저 만들고, (@Entity는 쉽게 말해서 이 도메인을 DB에 매핑해준다는건데 여기서는 매핑을 해주진 않을거고 Jpa 나중 에서나 해줄것 같다.)
// com/course/courses/Repository/CourseRepositoryInterface
package com.course.courses.Repository;
import com.course.courses.domain.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.jdbc.core.JdbcTemplate;
public interface CourseRepositoryInterface {
void save(Course course);
List<Course> findAll();
Optional<Course> findById(int id);
void deleteById(int id);
void update(Course course);
}
Repository interface를 위와 같이 작성을 해두면
// com/course/courses/Repository/CourseRepository
package com.course.courses.Repository;
import com.course.courses.domain.Course;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
@Repository
@RequiredArgsConstructor // Jdbctemplates di 해주기 위함
public class CourseRepository implements CourseRepositoryInterface{
final private JdbcTemplate jdbcTemplate;
@Override
@Transactional
public void save(Course course) {
String name = course.getName();
String author = course.getAuthor();
String sql = "INSERT INTO course(name, author) VALUES(?,?)";
jdbcTemplate.update(sql,name,author);
}
@Override
@Transactional(readOnly = true)
public List<Course> findAll() {
String sql = "SELECT * FROM course";
return jdbcTemplate.query(sql, ROW_MAPPER());
}
@Override
@Transactional(readOnly = true)
public Optional<Course> findById(int id) {
String sql = "SELECT * FROM course WHERE ID = ?";
boolean isCourseNotExist = jdbcTemplate.query(sql,(rs,rowNum)->0,id).isEmpty();
if(isCourseNotExist){
return Optional.empty();
}else{
List<Course> res = jdbcTemplate.query(sql, ROW_MAPPER(), id);
return res.stream().findAny();
}
}
@Override
@Transactional
public void deleteById(int id) {
String sql = "DELETE FROM course WHERE ID = ?";
String search = "SELECT * FROM course WHERE ID = ?";
boolean isCourseNotExist = jdbcTemplate.query(search, (rs,rowNum)->0, id).isEmpty();
if(!isCourseNotExist){
jdbcTemplate.update(sql, id);
}else{
throw new IllegalArgumentException();
}
}
@Override
@Transactional
public void update(Course course){
Optional<Course> res = this.findById(course.getId());
if(res.isEmpty()){
throw new IllegalArgumentException();
}else{
String sql = "UPDATE course SET name = ?, author = ? WHERE ID = ?";
Course course1 = res.get();
jdbcTemplate.update(sql, course.getName(), course.getAuthor(), course.getId());
}
}
private RowMapper<Course> ROW_MAPPER() {
return (rs, rowNum)->{
int ID = rs.getInt("ID");
String name = rs.getString("name");
String author = rs.getString("author");
return new Course(ID,name,author);
};
}
}
위와같이 JdbcTemplate을 이용해 Repository Class에서 DB 접근을 할 수 있다.
@RequiredArgsConstructor를 눈여겨볼만하다. 엄연히 JdbcTemplate도 Bean이기 때문에, 이를 DI 해주기 위한 어노테이션이다.
여기서 더 볼만한 점은 UPDATE query에 AND 연산자 대신 ,를 사용한 점이다. 만약 AND를 쓰면 다음과 같은 에러가 발생한다.
https://wwwnghks.tistory.com/43
[Mysql] truncated incorrect double value 에러
mysql에서 update문을 사용했는데 truncated incorrect double value 에러가 날 경우 대처법 update test_table set col1 = 'test' and col2 = 'test2' where col3 = '테스트'; 위의 쿼리를 아래와 같이 바꾸면 된다. update test_table
wwwnghks.tistory.com
그다음 Service Layer인데, 아직까진 크게 복잡하진 않다.
// com/course/courses/Service/CourseServiceInterface
package com.course.courses.Service;
import com.course.courses.domain.Course;
import java.util.List;
import java.util.Optional;
public interface CourseServiceInterface {
Optional<Course> findById(int id);
List<Course> findAll();
Course createCourse(Course course);
void deleteCourse(int id);
void updateCourse(Course course);
}
// com/course/courses/Service/CourseService
package com.course.courses.Service;
import com.course.courses.Repository.CourseRepositoryInterface;
import com.course.courses.domain.Course;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class CourseService implements CourseServiceInterface {
private final CourseRepositoryInterface courseRepository;
@Override
public List<Course> findAll(){
return courseRepository.findAll();
}
@Override
public Optional<Course> findById(int id){
Optional<Course> course = courseRepository.findById(id);
return course;
}
@Override
public Course createCourse(Course course){
courseRepository.save(course);
return course;
}
@Override
public void deleteCourse(int id){
Optional<Course> c = courseRepository.findById(id);
if(c.isPresent()){
courseRepository.deleteById(id);
}
return;
}
@Override
public void updateCourse(Course course){
courseRepository.update(course);
return;
}
}
@Service로 컨테이너에 컴포넌트 등록을 해주고, Repository를 DI 해주면 된다.
그외엔 크게 특별한게 없고, 각 함수들에 Repository 함수를 시행해주면 된다.
그 다음은 마지막으로 Controller이다.
package com.course.courses.Controller;
import com.course.courses.Service.CourseServiceInterface;
import com.course.courses.domain.Course;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
@CrossOrigin
//@RequestMapping(value="/api")
@RestController
@RequiredArgsConstructor
public class CoursesController {
@Autowired //해줘야함 이유는 모르겠음..
private CourseServiceInterface courseService;
@GetMapping("/")
public ResponseEntity<Course> main(Course course){
courseService.createCourse(course);
return new ResponseEntity<>(course, HttpStatus.CREATED);
}
@GetMapping("/showCourses")
public List<Course> getAll() {
return courseService.findAll();
}
@DeleteMapping("/delete")
public ResponseEntity<Object> Delete(@RequestParam int id) throws URISyntaxException {
courseService.deleteCourse(id);
URI redirectUri = new URI("http://localhost:3000");
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(redirectUri);
return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER);
}
@PutMapping("/update")
public ResponseEntity<Object> Update(Course course) throws URISyntaxException{
courseService.updateCourse(course);
URI redirectUri = new URI("http://localhost:3000");
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(redirectUri);
return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER);
}
}
여기서 고생을 한게 좀 있는데, @RequiredArgsConstructor를 해줬음에도 Service가 DI가 되지 않았다는 점이다. 그래서 @Autowired를 해줘야 한다. (아직 이유는 모르겠다.)
main함수에서의 return의 경우엔 ResponseEntity를 사용하는데, 생성시킨 course JSON 형태로 STATUS 200 flag와 함께 보내는 것 같다.
getAll의 경우, return 값이 Class의 배열형태인데, 알아서 JSON형식으로 반환된다. (Node.js의 경우에는 JSON으로 감싸주고 parsing을 해줘야 했다..)
GET으로 @RequestParam 형태로 query를 보내지만, 이를 DTO를 이용해서 course형태로 main함수와 같이 받을 수도 있다.
추가적으로 delete을 할때에는 따로 @DeleteMapping을 사용하는데, 프론트쪽에서는 이를 POST로 보내고, Spring에서는 @RequestParam으로, query로 받으면 된다.
프론트 코드는 다음과 같다.
import React from 'react'
const DeleteCourse = () => {
return (
<>
<form action="http://localhost:8080/delete" method="POST">
<input type="hidden" name="_method" value="delete"/>
<input name="id" placeholder="insert the id to delete"></input>
<button type="submit">Delete</button>
</form>
</>
)
}
export default DeleteCourse
비슷하게, @PutMapping (수정하기)를 사용하여 Controller를 만들수도 있는데, 원래는 @RequestBody DTO dto 식으로 작성하면 되는데, 이러면 에러가 발생하였다.
https://irerin07.tistory.com/116
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported 에러
스프링 부트로 jwt를 사용하는 간단한 로그인/ 회원가입 페이지를 만들다가 Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported 라는 에러가 발생했습니다. 구글 검색을 해보니 스프링은 'a
irerin07.tistory.com
보니까 @RequestBody 없이 DTO만 argument에 넣으면 되는것이었다.
이 @DeleteMapping과 @PutMapping을 사용하기위해 다음 코드를 메인 서버 코드에 삽입해줘야 한다.
@SpringBootApplication
public class CoursesApplication {
public static void main(String[] args) {
SpringApplication.run(CoursesApplication.class, args);
}
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
return new HiddenHttpMethodFilter();
}
}
수정을 하기위한 프론트코드는 delete와 거의 다른점이 없다.
import React from 'react'
const UpdateCourse = () => {
return (
<>
<form action="http://localhost:8080/update" method="POST">
<input type="hidden" name="_method" value="put"/>
<input name="id" placeholder="insert the id to update"></input>
<input name="name" placeholder="insert the name to update"></input>
<input name="author" placeholder="insert the author to update"></input>
<button type="submit">Update</button>
</form>
</>
)
}
export default UpdateCourse
마지막으로 redirect이다. URI 인스턴스를 만들고 (redirect 하려는 페이지는 "/" url인데, 포트번호는 프론트쪽인 3000번으로 해줘야 한다는 점이 눈여겨볼만하다.) 그 외에도 HttpHeaders 인스턴스를 만들어서 ResponseEntity에 헤더끼워넣고 반환해주면 된다.
참고한 블로그는 다음과같다.
http://jmlim.github.io/spring/2019/09/30/spring-redirect-to-an-external-url/
[Spring] Spring Controller 에서 외부 URL로 redirect 하기. · 기억하기 위한 개발노트
[Spring] Spring Controller 에서 외부 URL로 redirect 하기. 30 Sep 2019 | Spring Spring boot Redirect External 외부 URL Java Spring 웹 어플리케이션 내의 페이지가 아닌 외부 URL로 redirect 하고 싶을 경우 아래와 같이 다양
jmlim.github.io
다음은 리액트와 연동
Spring Boot + React.js 개발환경 연동하기
Spring Boot와 React.js를 연동해 개발환경을 만들고, 빌드해서 jar 파일로까지 만들어보는 과정입니다.
velog.io
'developing > spring' 카테고리의 다른 글
MockMvc를 이용한 Test (0) | 2023.07.29 |
---|---|
Entity 및 DTO, ResponseDTO 작성 (0) | 2023.07.26 |
JpaRepository 사용법 (0) | 2023.02.27 |
SpringBoot 기본 세팅 및 CRUD (0) | 2023.01.18 |
Spring 입문 - MVC Controller 및 build방법 (0) | 2022.12.28 |
댓글