웹서비스를 개발하고 운영하다 보면 피할 수 없는 문제 -> DB를 다루는 일.
MyBatis 같은 sql mapper를 이용하면 개발보다 sql을 다루는 시간이 더 많음.
객체 모델링 보다 테이블 모델링에만 집중하는 일이 일어남.
-> JPA 라는 자바 표준 ORM(object Relational Mapping) 기술을 만나게 됨.
JPA 소개
관계형 DB가 웹 애플리케이션의 빠질 수 없는 요소로 되어 감.
애플리케이션 코드보다 SQL 쿼리가 가득하게 됨.
개발자가 아무리 아름답게 자바코드를 설계해도 sql을 통해 DB를 다룰 수 있음.
실제 현업에서는 수십, 수백개의 테이블이 있고, 이테이블의 몇 배나 되는 sql을 만들고 유지 보수 해야함.
단순반복작업을 수백번 해야하는 스트레스.
또 다른 문제 패러다임 불일치 문제.
관계형 DB -> 어떻게 데이터를 저장할지
객체지향프로그래밍 언어 -> 메시지를 기반으로 기능과 속성을 한곳에서 관리하는 기술
관계형 DB로 객체지향(다형성, 추상화 등)을 표현할 수 있나??
애초에 패러다임이 다르기 때문에 쉽지 않음.
따라서 웹 애플리케이션 개발은 DB 모델링에만 집중하게 되었고 이를 해결하기 위해 JPA가 등장.
JPA 는 서로 지향하는 바가 다른 2개영역을 중간에서 패러다임 일치 시켜주기 위한 기술.
Spring Data JPA
- JPA는 인터페이스로서 자바 표준 명세서.
- JPA 이용할라면 구현체가 필요함. ex) hibernate, eclipse link
- 하지만 spring에서 jpa를 사용할 때는 이 구현체를 직접 다루지 않음.
- 구현체들을 좀 더 쉽게 사용하고자 추상화 시킨 Spring Data JPA라는 모듈을 이용해 JPA기술을 다룸.
관계를 보면 다음 과 같다.
JPA <- Hobernate <- spring data JPA
이렇게 한단계더 감싸놓은 spring data JPA가 등장한 이유
- 구현체 교체의 용이성
- 저장소 교체의 용이성
구현체 교체의 용이성
- Hibernate 외에 다른 구현체로 쉽게 교체하기 위함.
- Hibernate가 언젠가 수명을 다해 새로운 JPA구현체가 대세로 떠오르면 Spring Data JPA를 쓰면 쉽게 교체 가능.
저장소 교체의 용이성.
- 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함.
- 서비스 운영하다보면 관계형 DB로는 벅찰때가 있음. 이 때 MongoDB로 교체가 필요하다면 개발자는 Spring Data JPA에서 Spring MongoDB로 의존성만 교체하면 됨.
실무에서 JPA
실무에서 JPA를 사용하지 못하는 가장 큰 이유는 높은 러닝 커브를 이야기함.
-> JPA를 잘쓰기 위해서는 객체지향 프로그래밍과 관계형 DB를 둘다 잘 이해해야 함.
속도 이슈가 없을까 하는 걱정.
잘 활용하면 네이티브 쿼리만큼의 퍼포먼스를 낼 수 있다고 책에서 그럼.
이제 게시판을 3장에서 6장까지 만들어봄! 요구사항 분석을 해보자.
게시판기능
- 게시글 조회
- 게시글 등록
- 게시글 수정
- 게시글 삭제
회원 기능
- 구글/네이버 로그인
- 로그인한 사용자 글 작성 권한
- 본인 작성 글에 대한 권한 관리
domain 패키지
도메인이란?
게시글, 댓글, 회원, 정산, 결제 등 S/W에 대한 요구사항 혹은 문제영역.
@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의 primary key는 Long 타입의 auto_increament를 추천(MySql 기준 bigint 타입이됨)
주민등록번호와 같이 비즈니스상 유니크 키나 여러키를 조합한 복합키로 PK를 잡으면 난감한상황이 발생.
1. foreign key를 맺을때 다른 테이블이 복합키 전부를 갖고있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생.
2. 인덱스에 좋은 영향을 끼치지 못함.
3. 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는일이 발생.
따라서 주민등록번호, 복합키 등은 유니크키로 별도로추가하는 것을 추천.
어노테이션 순서 팁
롬복은 필수 어노테이션은 아님!
Entity는 JPA의 어노테이션 -> 이것을 클래스에 좀더 가까이 둠.
나중에 Kotlin같이 바뀌게 되면 롬복이 필요없게 되는데 가볍게 위에것을 지우기만 하면됨.
@Entity
- 테이블과 링크될 클래스임을 나타냄.
- 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매치
- SalesManager.java -> sales_manager table
@Id
해당 테이블의 PK 필드를 나타냄.
@generatedValue
- PK의 생성 규칙을 나타냄
- 스프링부트 2.0에서는 GenerationType.IDENTITU 옵션을 추가해야만 auto_increment가 됨.
@Column
- 테이블의 칼럼을 나타내며 선언을 하지 않더라도 해당클래스의 필드는 모두 칼럼이 됨.
- 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용함.
- ex) 기본값인 경우 varchar(255)가 기본값인데 사이즈를 500으로 늘리고 싶거나, 타입을 TEXT로 변경하고 싶거나.
@NoArgsConstructor
- 기본생성자 자동 추가
- pubilc Posts(){}와 같은 효과
@Builder
- 해당 클래스의 빌더 패턴 클래스를 생성
- 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함.
이러한 Posts클래스에는 setter 메서드가 없다.
- setter를 무작정 생성하면 해당 클래스의 객체 값들이 언제 어디서 변하는지 코드상 명확한 구분을 할 수 없어 차후 기능 변경시 복잡해지는 문제가 생김.
- 따라서 Entity클래스에서는 절대 setter메서드를 만들지 않음.
- 해당 필드 값 변경이 필요하면 그 목적과 의도를 나타낼 수 있는 메서드를 추가해야함.''
그렇다면 setter가 없는데 어떻게 값을 채워서 DB에 Insert함???
- 기본적으로 생성자를 통해 최종값을 채운후 DB삽입
- 값변경이 필요하면 해당 이벤트에 맞는 public 메서드를 호출하여 값을 변경
- 요 책에서는 생성자 대신 @Builder에서 제공하는 빌더클래스를 사용
- 생성자나 Builder나 생성시점에 값을 채워주는 역할은 똑같다.
- but 생성자의 경우는 지금 채워야할 필드가 무엇인지 명확히 지정할 수 없음. Builder는 어느 필드에 어떤값을 채워야할지 명확하게 인지 가능.
public interface PostsRepository extends JpaRepository<Posts,Long> {
}
Mybatis등에서 Dao라고 불리는 DB Layer 접근자.
JPA에서는 Repostiory라고 부르며 인터페이스로 생성한다.
인터페이스 생성후 JpaRepository<Entity 클래스, pk 타입> 을 상속하면 기본적인 CRUD 메서드가 자동생성.
@Repostiory 추가할 필요도 없음.
주의 할 점!
Entity 클래스와 기본 Entity Repository는 함께 위치하여야함.
Entity클래스는 기본 repositiory없이는 제대로 역할을 할 수가 없으므로 도메인 패키지에서 함께 관리한다.
@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("jojoldu@gmail.com")
.build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
Assertions.assertThat(posts.getTitle()).isEqualTo(title);
Assertions.assertThat(posts.getContent()).isEqualTo(content);
}
}
RepositoryTest
@AfterEach
테스트한개가 끝날때마다 실행
postsRepository.save()
테이블 posts에 insert, update 쿼리를 실행
id값이 있다면 update, 없다면 insert 쿼리가 실행됨.
postsRepository.findAll()
테이블 posts에 있는 모든 데이터를 조회해오는 메서드.
별다른 설정없이 @SpringbootTest 사용시 H2데이터베이스를 자동으로 실행해줌.
실제로 실행된 쿼리는 어떤 형태일까??
쿼리로그를 ON/OFF할수있는 설정이 있다.
spring.jpa.show_sql=true
application property에서 설정.
등록/수정/조회 API 만들기
API를 만들기 위한 3개의 클래스
Request 데이터를 받을 Dto
API요청을 받을 Controller
트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
많은 사람이 오해하고있는것.
Service에서 비즈니스 로직을 처리해야한다는 것.
Service는 트랜잭션, 도메인 간 순서 보장의 역할만 합니다.
Web layer
흔히 사용하는 컨트롤러(@Controller)와 뷰템플릿(JSP/Freemarker)의 영역.
이 외에도 필터, 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 이야기함.
Service Layer
@Service에 사용되는 서비스 영역.
일반적으로 Controller와 Dao(Data access object)의 중간 영역에서 사용됩니다.
@Transactional이 사용되어야하는 영역이기도 함.
Repository layer
DB와 같이 데이터 저장소에 접근하는 영역
Dao영역으로 이해하며 ㄴ쉬움.
Dtos
Dto(Data Transfer Object) 계층 간에 데이터 교환을 위한 객체를 이야기
Dtos는 이들의 영역을 이야기함.
ex) 뷰템플릿 엔진에서 사용될 객체나 Repostiory layer에서결과로 넘겨준 객체등을 이야기함.
Domain Model
도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고함.
ex) 택시앱, 배차, 탑승, 요금 등이 모두 도메인이 될수 있음.
@Entitty가 사용된 영역 역시 도메인 모델이라고 이해할 수 있음.
그러나 DB의 테이블과 무조건 관계가 있어야하는 것은 아님.
VO처럼 값객체들도 이영역에 해당하기 때문이다.
Web, Service, Repository, Dto, Domain중 비즈니스 로직 처리를 담당해야 할곳은 바로 Domain이다.
기존에 서비스로 처리하던 방식을 트랜잭션 스크립트라고 함.
이방식대로하면 객체에 상태를 꺼내오고
이상태를 기반으로 로직을 처리함.
이렇게 서비스에서 비즈니스 로직을 처리하게 되면 객체는 단순한 데이터 덩어리에 불과함.
따라서 도메인에서 비즈니스 처리를 하며
서비스 메소드는 트랜잭션과 도메인간의 순서만 보장해준다. (책104p코드 참고)
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
Entity 클래스와 거의 유사하지만 원래있던 Entity를 사용하지 않고 Dto 에 클래스를 새로 추가해서 생성.
-> 왜 why??
Entity클래스는 DB와 맞닿은 핵심 클래스.
but view에 사용하는 것은 사소한 기능변경인데 이를위해 테이블과 연결된 Entity 클래스를 변경하는것은 너무 큰 변경이다.
따라서 view layer와 db layer의 역할 분리를 철저하게 하는게 좋음.
실제로 Controller에서 결괏값으로 여러 테이블을 조인해서 줘야할 경우가 빈번하므로 Entity 클래스만으로 표현하기 어려운경우가 많음.
따라서 Entity클래스와 Controller에서 쓸 Dto는 꼭 분리해서 사용해야 함.
등록 구현
java/org/example/awsspring/web/PostsApiController.java
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
java/org/example/awsspring/service/PostsService.java
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).
getId();
}
}
java/org/example/awsspring/web/dto/PostsSaveRequestDto.java
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
Entity 클래스와 거의 유사하지만 원래있던 Entity를 사용하지 않고 Dto 에 클래스를 새로 추가해서 생성.
-> 왜 why??
Entity클래스는 DB와 맞닿은 핵심 클래스.
but view에 사용하는 것은 사소한 기능변경인데 이를위해 테이블과 연결된 Entity 클래스를 변경하는것은 너무 큰 변경이다.
따라서 view layer와 db layer의 역할 분리를 철저하게 하는게 좋음.
실제로 Controller에서 결괏값으로 여러 테이블을 조인해서 줘야할 경우가 빈번하므로 Entity 클래스만으로 표현하기 어려운경우가 많음.
따라서 Entity클래스와 Controller에서 쓸 Dto는 꼭 분리해서 사용해야 함.
기능을 구현 TEST코드
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@AfterEach
public void tearDown() throws Exception{
postsRepository.deleteAll();
}
@Test
public void Posts_등록된다() throws Exception{
//given
String title = "title";
String content = "content";
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author("author")
.build();
String url = "http://localhost:" + port +"/api/v1/posts";
//when
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
}
WebMvcTest -> JPA기능이 작동하지 않기 때문
-> JPA기능까지 한번에 테스트 할 때는 @SpringBootTest와 TestRestTemplate 사용하면 됨.
수정, 조회 기능 추가
java/org/example/awsspring/web/PostsApiController.java
@RequiredArgsConstructor
@RestController
public class PostsApiController {
...
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById(@PathVariable Long id){
return postsService.findById(id);
}
}
@PathVariable 이란?
REST API에서 URI에 변수가 들어가는걸 실무에서 많이 볼 수 있다.
예를 들면, 아래 URI에서 밑줄 친 부분이 @PathVariable로 처리해줄 수 있는 부분이다.
http://localhost:8080/api/user/1234
https://music.bugs.co.kr/album/4062464
java/org/example/awsspring/web/dto/PostsResponseDto.java
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
public PostsResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
}
}
Entity중 일부만 사용하므로 생성자로 Entity를 받아 필드 값에 넣는다. 굳이 모든 필드를 가진 생성자가 필요하지 않으므로 Dto는 Entity를 받아 처리한다.
java/org/example/awsspring/web/dto/PostsUpdateRequestDto.java
@Getter
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content) {
this.title = title;
this.content = content;
}
}
java/org/example/awsspring/domain/posts/Posts.java
@Getter
@NoArgsConstructor
@Entity
public class Posts {
....
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
@RequiredArgsConstructor
@Service
public class PostsService {
...
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto){
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
posts.update(requestDto.getTitle(), requestDto.getContent());
return id;
}
public PostsResponseDto findById(Long id){
Posts entity = postsRepository.findById(id).orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
return new PostsResponseDto(entity);
}
}
update부분을 부면 그냥 Posts클래스의 update만 호출해서 그 객체의 값만 바꿔주고 DB에 쿼리를 날리는 부분이 없음...?
-> JPA의 영속성 컨텍스트 덕분.
영속성 컨텍스트 -> 엔티티를 영구 저장하는 환경
JPA의 핵심내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈림
JPA의 Entity manager가 활성화된 상태로 트랜잭션 안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태.
(DB에서 배운 저장 시점에 관련된 이야기인 것같다. 트랜잭션이 끝날때 데이터를 반영하기 때문에 쿼리를 따로 날리지않아도 데이터변경내용을 자동으로 commit해주는 것으로 보임.)
이상태에서 엔티티의 데이터 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블의 변경 분을 반영.
따라서update쿼리를 날릴 필요가 없다. -> Dirty checking 이라고 함.
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId = savedPosts.getId();
String expectedTitle = "title2";
String expectedContent = "content2";
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
//when
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,requestEntity,Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
test 성공
JPA Auditing 으로 생성시간/수정시간 자동화하기
보통 entity에는 해당 데이터의 생성시간과 수정시간을 포함한다.
언제 만들어졌는지 언제 수정되었는지는 유지보수에 있어서 굉장히 중요한 정보이기 때문이다.
but 날짜 데이터를 등록, 수정하는 코드가 여기저기 들어가게 된다.
이러한 문제를 해결하고자 JPA auditing을 사용한다.
LocalDate사용
자바8부터 localdate와 localdatetime이 등장.
java/org/example/awsspring/domain/BaseTimeEntity.java
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
BaseTimeEntity는 모든 Entity의 상위 클래스가 되어 entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할을 함.
MappedSuperclass
JPA Entity클래스들이 BaseTimeEntity를 상속할 경우 필드들도 칼럼으로 인식하도록 한다.
EntityListeners(AuditingEntityListener.class)
BaseTimeEntity클래스에 Auditing 기능을 포함시킨다
@CreatedDate
Entity가 생성되어 저장될때 시간이 자동저장된다.
@LastModifiedDate
조회한 Entity의 값을 변경할때 시간이 자동저장됨.
Posts클래스가 BaseTimeEntity를 상속 받게 수정.
마지막으로 JPA Auditing 어노테이션들을 모두 활성화 할수있도록 Application클래스에 활성화 어노테이션 하나를 추가.
Test코드
@Test
public void BaseTimeEntity_등록(){
//given
LocalDateTime now = LocalDateTime.of(2019,6,4,0,0,0);
postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
System.out.println("CreatedTime = " + posts.getCreatedDate() + " ModifiedTime = " + posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
이번 장에서 배운것들 정리
JPA/ Hibernate / Spring data Jpa의 관계
Spring Data Jpa를 이용해 관계형 DB를 객체지향적으로 관리하는 방법
JPA의 더티체킹을 이용하면 Update쿼리 없이 테이블 수정이 가능하다.
JPA Auditing을 통해 등록/수정 시간을 자동화하는 방법.
'Spring > 스프링부트와 AWS로 혼자구현하는 웹 서비스' 카테고리의 다른 글
섹션7. AWS RDS (1) | 2023.02.05 |
---|---|
섹션 6. AWS 서버 환경을 만들어보자 - AWS EC2 (0) | 2023.01.30 |
5장. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2023.01.29 |
4장. 머스테치로 화면 구성하기 (0) | 2023.01.25 |
2장. 테스트 코드를 작성해보자 (0) | 2023.01.24 |