이 글은 이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹 서비스를 읽고 정리한 글입니다.
내용보다 중요한 점, 추가적으로 알아본 것 등을 위주로 적었습니다.
http://www.yes24.com/Product/Goods/83849117
03장 스프링 부트에서 JPA로 데이터베이스 다뤄보자
백엔드 개발자가 SQL문을 많이 사용하는 게 맞을까?? 실제 서비스 로직 등에 투자하는 시간보다 SQL에 투자하는 시간이 더 많았던 기형적인 형태를 JPA가 고쳐주었다.
JPA
관계형 데이터베이스와 객체지향 프로그래밍 언어는 시작점부터가 달랐다. 이런 패러다임의 불일치가 문제이다. 데이터베이스의 내용을 객체지향적으로 풀어내려면 복잡해지고 판독성도 떨어진다. 그래서 데이터베이스 모델링에만 집중하게 되는 문제가 생겼다.
JPA는 중간에서 이런 패러다임을 일치시켜주는 기술이다. 즉, 개발자는 객체지향적 프로그래밍을 하고, JPA가 SQL을 대신 생성한다. 더 이상 SQL 종속적인 개발을 하지 않아도 된다.
Spring Data JPA
JPA와 다른 것이다. JPA는 Java Persisitence API, 즉 인터페이스이다. 이것을 구현하는 구현체가 필요하다. 이 구현체는 대표적으로 Hibernate, Eclipse, Link등이 있습니다. 스프링 진영에서는 이 구현체를 직접 사용하는 것보다 이걸 한 번 더 감싼 Spring Data JPA라는 것을 개발했다. 그 이유에는 두 가지가 있다.
- 구현체 교체의 용이성
Hibernate가 현재 표준이지만, 나중에 다른 구현체를 써야 한다고 해도 Spring Data JPA가 내부 매핑을 지원한다.
- 저장소 교체의 용이성
서비스 초기에 관계형 데이터베이스로 기능을 처리했지만, 서비스가 커져 교체가 필요하다면 의존성만 교체하면 된다.
JpaRepository와 명명법
Spring Data JPA는 Jpa Repository라는 인터페이스를 제공한다. 여기서 정해진 규칙대로 메소드의 이름을 입력하면 구현하지 않아도 JpaRepository가 해당 메소드를 구현해준다.
public interface StudentRepository extends JpaRepository<Student, Long> {
List<Student> findAllByMajor(String major);
}
위 코드를 보자. 이건 스프링 빈이 아니라 Interface, 즉 구현되어 있지 않았다. JpaRepository를 implements 하지 않고 extends하고 있다는 것에 주목하자. 심지어 JpaRepository도 @NoRepositoryBean이라는 어노테이션이 달려있다. 즉 구현체가 없다는 것이다. 그런데 어떻게 스프링 빈이 만들어지고 Dependency Injection이 가능할까?
Spring data Jpa는 @ComponentScan에 의해 IoC가 빈으로 등록하는 게 아니라 Spring data JPA가 인터페이스만 보고 알아서 동적으로 필요한 구현 클래스를 생성하고 repository를 사용하는 클래스 (Service 클래스 등)와 연결해준다.
위의 코드를 보면, DB에 major(전공)라는 열을 보고 모두 찾아서 리스트를 반환하는 것이다. 실제로 그렇게 동작하는 코드를 짜진 않고 이름만 지었을 뿐이다. 하지만 Spring data JPA가 이것을 보고 실제로 동작하는 클래스를 생성하고 다른 클래스와 DI해준다. SQL문은 전혀 보이지 않는다.
JpaRepository 에 findById, existsById, deleteById등 기본적으로 @Id로 등록된 키로 동작하는 함수, findAll(), deleteAll() 등 모든 column 대상 메소드, count 메소드 등이 있다. 이는 위의 findAllByMajor()함수처럼 선언해주지 않아도 된다.
그럼 findAllByMajor()처럼 선언만 하면 알아서 구현해주는 규칙, 명명법을 알아보자.
아까와 같이 find, exists delete 등을 쓰고 By를 쓰면 되는데, 그 사이에 선택사항으로 All, First등이 가능하다. ex) findByMajor, findAllByMajor
By 뒤에는 해당 변수명과 똑같이 쓰고 매개 변수에 넣어주면 된다. ex)findAllByMajor(String major)
이 때, 명명법으로 작성하기 힘든 복잡한 쿼리가 분명히 있을 것이다. 그리고 테이블의 데이터를 직접 수정, 생성하는 등의 명명법은 지원하지 않는다. 이 때는 @Query로 직접 DB에 쿼리 명령을 한다.
@Query("명령할JPQL_SQL문")과 함께 아까와 같이 Interface에 선언해주면 마찬가지로 이 함수가 포함된 Repository를 알아서 구현해준다. SQL문은 DB 제품마다의 방언을 제거하기 위해 객체 지향인 JPQL을 활용하는 것이 중요하다.
public interface StudentRepository extends JpaRepository<Student, Long> {
List<Student> findAllByMajor(String major);
@Modifying (clearAutomatically = true)
@Query ("update Member m set m.age=m.age+1")
int ageIncrease();
}
위와 같이 하면 ageIncrease() 함수가 불릴 때마다 Member entity의 age가 1씩 증가될 것이다.
https://dingdingmin-back-end-developer.tistory.com/entry/Spring-data-Jpa-4-Update%EC%99%80-Query
DDL Options
application.properties에 spring.jpa.hibernate.ddl-auto에 5가지 기능을 넣을 수 있다.
- create: 기존의 테이블이 존재하면 drop 후 create
- create-drop: 위의 create처럼 한 후 프로그램 종료 시 drop
- update: JPA에 의해 변경된 부분만 반영. 즉 alter table한다. table이 없으면 create
- validate: entity와 table이 정상 매핑되어 있지 않으면 오류. 즉 column등이 맞지 않으면 에러. 검증만 한다.
- none: 초기화하지 않는다.
또한 spring.jpa.show-sql = true 를 넣고 실행하면 콘솔 창에 실제 쿼리가 출력된다.
게시판 프로젝트
앞으로 하나의 게시판 웹 어플리케이션을 만들 것이다.
게시판 요구사항 분석
- 게시판 기능
- 게시글 조회
- 게시글 등록
- 게시글 수정
- 게시글 삭제
- 회원 기능
- 구글/네이버 로그인
- 로그인한 사용자 글 작성 권한
- 본인 작성 글에 대한 권한 관리
Entity 설계 (Posts)
우선, domain 패키지와 그 안에 posts 패키지를 만들고, 그곳에 Posts 클래스를 만들어 Entity를 설계한다.
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
@ToString
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
여기서 클래스에 붙는 annotation들 중 필수적인, 혹은 주요 어노테이션을 클래스에 가깝게 두어야 리팩터링 시 편하다.
또한, Entity 에는 Setter를 사용하면 안 된다. 인스턴스의 값들이 변하는 경로를 정확히 파악할 수 없어, 값 변경 시 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 한다.
그 후 같은 경로에 JpaRepository<Posts, Long>을 extends 하는 PostsRepository를 생성한다.
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
테스트 코드 작성 (PostsRepositoryTest)
이 부분에서 책은 @SpringBootTest를 이용한 통합테스트를 했다. 하지만, Controller나 Service 등이 개입하지 않는 경우에 이런 annotation은 불필요하다. 다음 게시글을 참고할 수 있다.
@ExtendWith(MockitoExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) //내장 DB로 변경하지 않기
@TestInstance(TestInstance.Lifecycle.PER_CLASS) //for non-static @AfterAll
class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@AfterAll
public void check() {
System.out.println("postsRepository.findAll() = " + postsRepository.findAll());
}
@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("morenow")
.build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
@SpringBootTest를 쓰지 않고 DAO layer만 테스트하는 @DataJpaTest 를 사용했다.
만약 MySQL과 같은 물리적 DB를 연동하고 있을 경우에는 내장 DB로 변경할 수 없다는 에러가 뜬다. 이 경우 연결된 DB를 그냥 쓰겠다고 annotation에 적어주면 된다.
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
책에서는 테스트 종료 후 DB의 모든 데이터를 직접 지우는 코드가 있었다.
하지만 @DataJpaTest에는 @Transactional이 포함되어 있다. 즉, 테스트 완료 시 자동으로 롤백되기 때문에 책과 같은 코드가 필요없다.
확인해보기 위해 @AfterAll로 모든 테스트가 종료된 후 DB에 남아있는 모든 데이터를 출력해보았다.
//실행결과
postsRepository.findAll() = []
DB의 값들이 비워져 있는 것을 확인할 수 있다.
'스프링부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
08장 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - EC2 서버에 프로젝트를 배포해 보자 (0) | 2023.02.10 |
---|---|
06,07장 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - AWS 서버 와 데이터베이스 환경을 만들어보자 - AWS EC2, RDS (0) | 2023.02.03 |
02장 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 스프링 부트에서 테스트 코드를 작성하자 (0) | 2023.01.20 |
01장 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 인텔리제이로 스프링 부트 시작하기 (0) | 2023.01.13 |