JPA란? / Domain, Repository, Service / CRUD실습
용어의 정의는 이러하다.
JPA(Java Persistence API) - 자바 ORM기술에 대한 API 표준 명세
ORM(Object-Relational Mapping) - 객체가 DB의 테이블이 되도록 매핑시켜주는 프레임워크 (객체와 RDB 사이에 존재한느 개념과 접근을 객체지향적으로 다루기 위한 기술)
쉽게 말하자면 SQL을 쓰지 않고도 데이터를 DB에 Create, Read, Update, Delete (CRDU) 할 수 있도록 해주는 번역기라고 말할 수 있다.
JPA는 ORM을 쓰기 위한 방식을 정의한 인터페이스의 모음이다.
따라서 이를 상속한 ORM프레임워크를 사용해야 한다.
JPA에서 테이블에 해당하는 것은 "Domain", SQL에 해당하는 것은 "Repository"이다.
수업 정보에 대한 테이블을 CRUD하는 간단한 실습을 해보았다.
Spring의 구조는 세 가지 영역으로 나눌 수 있다.
1. Controller : 가장 바깥 부분, 요청/응답을 처리
2. Service : 중간 부분, 실제 중요한 작동이 많이 일어나는 부분
3. Repository : 가장 안쪽 부분 (DB layer 와 맞닿아 있다.)
Spring Service Layer에 대한 고찰
Spring Service Layer에 대한 고찰
velog.io
각 요소의 의미가 크게 와닿지 않았는데.. 위 블로그를 보고 어느정도 이해가 되었다!
당연히 Controller에 다 끌어와서 구현할 순 있겠지만, 그러면 하나의 API가 너무 방대해지고, 유지보수도 어려울 것이다.
따라서 Layer를 나누어서 Controller에서는 요청을 받아들여서 적합한 Service에게 "전달"해주는 역할을 한다.. 정도로 이해하면 적합할 것 같다.
먼저, Course 테이블이 필요할 것이다.
따라서 Course.java 파일을 작성한다. 즉, 이는 도메인 클래스가 된다.
@Entity
public class Course extends Timestamped {
@Id (Primary key)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String title;
@Column(length=100)
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;
}
public Long getId() {
return id;
}
public void update(Course course){
this.title = course.title;
this.tutor = course.tutor;
}
}
@Entity : 해당 클래스가 테이블(도메인, 엔티티)임을 나타낸다.
ORM이 해당 어노테이션이 붙은 클래스를 찾아 작업을 수행하게 된다.
(참고) 도메인(엔티티) 클래스에서는 컬럼에 대해 절대 Setter메소드를 만들지 않는다!
이 때, 생성일과 수정일 컬럼을 추가해주기 위해
아래와 같이 작성한 Timestamped 인터페이스를 상속하였다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate // 생성일자
private LocalDateTime createdAt;
@LastModifiedDate // 마지막 수정일자
private LocalDateTime modifiedAt;
}
@MappedSuperclass : 해당 클래스를 상속했을 때, 멤버변수를 컬럼으로 인식하게 된다.
@EntityListeners(AuditingEntityListener.class) : 생성/수정 시간을 자동으로 반영하도록 설정함
이렇게 컬럼이 update될 때 시간을 자동으로 매핑해주는 기능을 JPA Auditing이라고 부른다.
이를 활성화시키기 위해 메인 어플리케이션 클래스에 @EnableJpaAuditing 어노테이션을 추가한다.
생성일과 수정일은 매우 중요한 데이터로서,
이 인터페이스를 모든 도메인 클래스에 상속시켜 모든 테이블에 생성일자, 수정일자가 기록되도록 한다.
이렇게 도메인 클래스 작성을 마무리하고,
Course클래스로 DB에 접근할 Repository를 생성해야한다.
JpaRepository를 상속하여 도메인에 대한 리포지토리를 만든다.
public interface CourseRepository extends JpaRepository<Course, Long> {
}
JpaRepository<클래스, pk 타입> 을 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.
(참고) Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다.
다음으로 Service를 만든다.
먼저 Course클래스에 update 메소드를 추가한다.
public void update(Course course){
this.title = course.title;
this.tutor = course.tutor;
}
service패키지를 만들고 그 하위에 CourseService 클래스를 생성한다.
@Service
public class CourseService {
private final CourseRepository courseRepository;
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@Transactional
public Long update(Long id, Course course) { // id와 정보 필요
Course course1 = courseRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 아이디가 존재하지 않습니다.")
);
course1.update(course);
return course1.getId();
} // update하고 해당 column의 id 전달
}
@Service : 서비스 클래스 임을 나타낸다.
(사실상 @Repository와 기능적인 차이는 없으나 의미 구별을 위해 구분해서 사용한다.)
@Transacitonal : 이 메소드에 대해 트랜잭션을 수행한다. 성공하면 Commit, 실패하면 Roll Back
작성한 코드를 테스트 해보기 위해 임시로 메인 어플리케이션에 코드를 작성해보겠다.
(단지 이해를 위해 작성된 코드이기 때문에 세세히 신경쓰지 않아도 된다.)
● Create & Read
@Bean
public CommandLineRunner demo(CourseRepository repository) {
return (args) -> {
Course course1 = new Course("title_1", "tutor_1");
repository.save(course1);
List<Course> courseList = repository.findAll();
for(int i=0;i<courseList.size();i++){
Course c = courseList.get(i);
System.out.println(c.getTitle());
System.out.println(c.getTutor());
}
System.out.println("=========");
Course course = repository.findById(1L).orElseThrow(
() -> new IllegalArgumentException("유효하지 않은 Id")
);
System.out.println(course.getTitle());
System.out.println(course.getTutor());
};
}
repository.save()를 통해 데이터를 저장하고, repository.findAll()을 통해 조회한다.
findById.orElseThrow() 안에는 만약 해당 id에 해당하는 컬럼이 없을 때 예외처리할 내용을 넣는다.
로그에 작성한 코드에 해당하는 SQL문과 해당 쿼리 결과가 나오는 것을 볼 수 있다.
● Update
@Bean
public CommandLineRunner demo(CourseRepository courseRepository, CourseService courseService) {
return (args) -> {
courseRepository.save(new Course("course1", "tutor1"));
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());
}
Course new_course = new Course("course1", "tutor2");
courseService.update(1L, new_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());
}
};
}
새로운 Course객체에 값을 입력하고 CourseService.update메소드에 id와 객체를 전달한다.
해당 메소드에서는 Id값으로 Course객체(컬럼)을 찾고, update한다.
(실제로 update를 수행할 때는 Course를 직접 사용하는 것이 아니라 Dto를 사용해야 한다. 이는 다른 포스팅에서 다루도록 하겠다.)
● Delete
@Bean
public CommandLineRunner demo(CourseRepository courseRepository, CourseService courseService) {
return (args) -> {
courseRepository.save(new Course("course1", "tutor1"));
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());
}
courseRepository.deleteById(1L);
Course new_course = new Course("course1", "tutor2");
courseService.update(1L, new_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());
}
courseRepository.deleteAll();
};
}
deleteById(), deleteAll() 등을 이용해 데이터를 삭제할 수 있다.
기존에 Node.js나 php로 서버 개발을 했을 때
Controller, Dao을 오가며 열심히 SQL문과 코드를 수정한 기억이 있는데
스프링+JPA 조합으로 이렇게 쓸 수 있다니 신기하당
실제 실무에서 많이 쓰이고 있는 추세라고 한다.