스프링 부트와 AWS로 혼자 구현하는 웹 서비스 를 공부하고 정리한 내용입니다.
JPA : 자바 표준 ORM
ORM : 객체를 매핑하는 것
SQL Mapper : 쿼리를 매핑하는 것
현업 프로젝트 대부분이 애플리케이션 코드보다 SQL로 가득하게 된다.
SQL로만 가능하니 각 테이블마다 기본적인 CRUD SQL을 매번 생성해야 한다.
패러다임 불일치 문제
➡️ 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 서로 다른데, 객체를 데이터베이스에 저장하려고 하니 여러 문제가 발생한다. 이를 패러다임 불일치라고 한다.
User, Group 부모-자식 관계
User user = findUser();
Group group = user.getGroup();
여기에 데이터베이스가 추가
User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupId);
➡️ 그러다보니, 웹 애플리케이션 개발은 점점 데이터베이스 모델링에만 집중하게 된다.
➡️ 이로 인해, JPA는 이런 문제점들을 해결하기 위해 등장하게 된다.
✔️ JPA란?
객체 중심으로 개발을 하게 되니 생산성 향상은 물론 유지 보수하기가 정멸 편하다!
✔️ Spring Data JPA
JPA는 인터페이스로서 자바 표준명세서이다.
인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다.
대표적으로 Hibernate, Eclipse Link 등이 있다. 하지만 Spring에서 JPA를 사용할 때는 이 구현체들을 직접 다루지는 않는다.
구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다.
JPA ← Hibernate ← Spring Data JPA
Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없다.
그럼에도 스프링 진영에서는 Spring Data JPA를 개발했고, 이를 권장하고 있다.
✏️ Spring Data JPA가 등장한 이유
- 구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
- Hibernate가 언젠간 수명을 다해서 새로운 JPA 구현체가 대세로 떠오를 때, Spring Data JPA를 쓰는 중이라면 아주 쉽게 교체할 수 있다.
- 저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
- Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같다.
- Spring Data JPA, Spring Data Redis, Spring Data MongoDB 등등 Spring Data의 하위 프로젝트들은
save(), findAll(), findOne()
등을 인터페이스로 갖고 있다.- 그러다 보니, 저장소가 교체되어도 기본적인 기능은 변경할 것이 없다.
➡️ 이와 같은 장점들로 인해 Hibernate를 직접 쓰기보다는 Spring Data 프로젝트를 사용하는 것을 권장한다.
🎁 실무에서 JPA를 사용하지 못하는 이유
- 실무에서 JPA를 사용하지 못하는 가장 큰 이유는 높은 러닝 커브를 이야기한다.
- JPA를 잘 쓰려면 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해해야 한다.
🔑 JPA의 보상
- CRUD 쿼리를 직접 작성할 필요가 없다.
- 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등 객체지향 프로그래밍을 쉽게 할 수 있다.
✔ 요구사항 분석
게시판 기능
- 게시글 조회
- 게시글 등록
- 게시글 수정
- 게시글 삭제
회원 기능
- 구글/네이버 로그인
- 로그인한 사용자 글 작성 권한
- 본인 작성 글에 대한 권한 관리
(1) spring-boot-starter-data-jpa
(2) H2
🔔 domain 패키지
- 도메인을 담을 패키지
- 시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역
- dao 패키지와는 조금 결이 다르다.
- xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이 모두 도메인 클래스에서 해결된다.
Posts
package springbootawsbook.springawsbook.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@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;
}
}
@Entity
: JPA의 어노테이션@Getter
, @NoArgsConstructor
: 롬복의 어노테이션@Entity
를 클래스에 가깝게 두고, 롬복 어노테이션을 그 위로 두었다.👉 JPA를 사용하면 DB데이터에 작업할 경우 실제 쿼리를 날리기보다는, 이와 같은 Entity 클래스의 수정을 통해 작업을 한다.
📝 코드 설명, 어노테이션
@Entity
- 테이블과 링크될 클래스임을 나타낸다.
- 기본 값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(
_
)으로 테이블 이름을 매칭한다.- ex) SalesManager.java ➡ sales_managertable
@Id
- 해당 테이블의 PK 필드를 나타낸다.
@GeneratedValue
- PK의 생성 규칙을 나타낸다.
- 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만
auto_increment
가 된다.- 스프링 부트 2.0 버전과 1.5 버전의 차이는
http://jojoldu.tistory.com/295
를 참고하기
@Column
- 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.
- 사용하는 이유 : 기본 값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
- 문자열의 경우 VARCHAR(255)가 기본 값이지만, 사이즈를 500으로 늘리고 싶거나 (ex: title), 타입을 TEXT로 변경하고 싶거나(ex: content) 등의 경우에 사용된다.
💡 참고
@Entity
의 PK는 Long 타입의 Auto_increment를 추천한다. (MySQL 기준으로 이렇게 하면 bigint 타입이 된다.)
주민등록번호와 같이 비즈니상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 발생한다.
- FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생한다.
- 인덱스에 좋은 영향을 끼치지 못한다.
- 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생한다.
➡ 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하는 것을 추천한다!
📝 코드 설명, 어노테이션
@NoArgsConstructor
- 기본 생성자 자동 추가
public Posts() {}
와 같은 효과
@Getter
- 클래스 내 모든 필드의 Getter 메소드를 자동생성
@Builder
- 해당 클래스의 빌더 패턴 클래스를 생성
- 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함
✔ 그런데, Posts
클래스에는 Setter 메서드가 없다?
getter/setter
를 무작정 생성할 시, 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없다.ex) 주문 취소 메소드를 만든다고 가정할 때
public class Order{
public void setStatus(boolean status){
this.status = status
}
}
public void 주문서비스의_취소이벤트(){
order.setStatus(false);
}
➡
public class Order{
public void cancelOrder(){
this.status = false;
}
}
public void 주문서비스의_취소이벤트(){
order.cancelOrder();
}
✔ Setter가 없는 상황에서는 어떻게 값을 채워 DB에 삽입할까?
💡 참고
책에서는 생성자 대신에@Builder
를 통해 제공되는 빌더 클래스를 사용한다.
생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같다.
✔ 생성자 vs 빌더
public Example(String a, String b){
this.a = a;
this.b = b;
}
Example.builder()
.a(a)
.b(b)
.build();
➡ 앞으로는 모든 예제에서 빌터 패턴을 적극적으로 사용할 것이다.
✔ DB 접근, JpaRepository
PostRepository
package springbootawsbook.springawsbook.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
JpaRepository<Entity 클래스, PK 타입>
를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.@Repository
를 추가할 필요가 없다.
PostRepositoryTest
package springbootawsbook.springawsbook.domain.posts;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@AfterEach
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기() {
// given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("lkc263@gmail.com")
.build());
// when
List<Posts> postsList = postsRepository.findAll();
// then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
📝 코드 설명
@AfterEach
- Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정한다.
- 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침법을 막기 위해 사용한다.
- 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있다.
postsRepository.save
- 테이블 posts에
insert/update
쿼리를 실행한다.- id 값이 있다면
update
, 없다면insert
쿼리가 실행된다.
postRepository.findAll
- 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
💡 참고
JUnit4 ➡ JUnit5
@After
➡@AfterEach
AfterClass
➡@AfterAll
실행 결과
🔑 실제로 실행된 쿼리는 어떤 형태일까?
실행된 쿼리를 로그로 보려면,application.properties, application.yml
등의 파일에서 한 줄의 코드로 설정할 수 있다.
이전에 공부한 내용을 참고하자!
이제 JPA와 H2에 대한 기본적인 기능과 설정을 진행했으니, 본격적으로 API를 만들어보자!