코딩하는 문과생
[Spring Boot] JPA, 데이터베이스 연동 본문
※ 해당 글은 튜토리얼이 아닌 헷갈리거나 중요한 개념 위주로 정리한 글입니다.
[JPA]
: 자바 표준 ORM 기술
관계형 DB는 어떻게 데이터를 저장할 지에 초점이 있고, 객체지향프로그래밍은 기능과 속성에 초점이 맞춰져 있다.
즉, 데이터베이스와 스프링이 지향하는 점이 다르다. 이 둘의 패러다임(사상)을 중간에서 일치시켜주는 것이 JPA다.
* MyBatis나 iBatis는 ORM(객체맵핑)이 아닌 SQL Mapper(쿼리맵핑)다.
- JPA 는 인터페이스로서 자바표준명세서다.
- 구현체로 Hibernate, Eclipse Link 등이 있다.
- 그러나 더 추상화시킨 Spring Data JPA를 사용한다. 이는 구현체(Hibernate) 교체와 저장소(MySql) 교체가 용이하기 때문이다. 구현체와 저장소에서는 기본적으로 save(), findOne()등 기본적인 CRUD 인터페이스를 제공하기 때문에 추후 구현체나 저장소를 교체해도 동일한 메소드를 사용할 수 있다.
- 결론: JPA <- Hibernate <- Spring Data JPA
-Entity
- 도메인 패키지에 관리되는 객체
- 한 Entity가 한 테이블을 뜻한다.
- 비즈니스 로직을 작성하기도 한다.
@Getter
@NoArgsConstructor // 기본생성자 자동추가
@Entity //JPA 어노테이션, 테이블과 링크된다.
public class Posts {
@Id //PK
@GeneratedValue(strategy = GenerationType.IDENTITY) //auto increment, PK를 이렇게 설정하는 것이 좋다.
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;
}
}
* @Builder
- 생성자와 역할이 같다.
- 단, @Builder 어노테이션을 사용하면 안전하게 생성시점에 값을 채울 수 있다.
- .build()를 사용해 객체를 생성한다.
- 값을 안전하게 맵핑하기 위해 사용한다.
-JpaRepository
public interface PostsRepository extends JpaRepository<Posts, Long> { //CRUD 자동생성
}
JpaRepository<Entity클래스, PK타입>를 상속받아 PostsRepository 라는 인터페이스를 생성하면 해당 인터페이스에서는 기본적인 CRUD 메소드가 자동으로 생성된다.
* 단, Entity클래스와 Repository는 함께 위치해야 한다. 이 둘은 도메인 패키지에서 주로 관리된다.
- 참고사항
cheese10yun.github.io/spring-builder-pattern/
Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그
Builder 기반으로 객체를 안전하게 생성하는 방법 - Yun Blog | 기술 블로그
cheese10yun.github.io
[스프링의 웹 계층]
Web Layer: 컨트롤러 등 뷰 템플릿 영역, 외부 요청과 응답에 대한 영역이다.(@Filter, @Interceptor)
| Dto: 계층 간 데이터 교환을 위한 객체
Service Layer: 트랜잭션과 도메인 기능간 순서를 보장, 비즈니스 로직 처리하는 부분이 아니다.
| Domain Model: 개발 대상을 단순화 시킨 것. ex.배달앱의 주문, 요금, 배달원배정 등이 있다. 따라서 VO객체도 여기에 포함된다.
Repository Layer: DB에 접근하는 영역, DAO layer와 동일하다.
* 도메인 영역이 바로 비즈니스 로직을 처리하는 부분이다.
[RestController & Service]
스프링에서는 빈객체를 주입받는 방식을 대게 @Autowired를 사용했지만, 사실 빈객체를 주입받는 방식은 setter와 생성자를 통해서도 가능하다. 오히려 생성자를 통해 주입받는 게 안전하다.
-RestContorller
@RequiredArgsConstructor //final or @NonNull 인 필드 대상으로 생성자 생성
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
-Service
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
- Dto
- Dto와 Entity는 서로 목적이 다르다.
- Dto는 뷰 레이어에서 요청과 응답을 담당하지만, Entity Model은 DB에 직접 접근하는 객체다.
- 반드시 분리해서 사용하도록 한다.
@Getter
@NoArgsConstructor //기본 생성자만 생성
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder //Builder 어노테이션을 사용해 객체 생성 시 값을 안전하게 넣을 수 있다.
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
//도메인 모델을 view layer에서 사용하기 위해서는 아래와 같이 객체를 생성해서 사용한다.
//직접 entity객체를 view layer에서 사용하지 않도록 주의하자.
public Posts toEntity() { //Posts 객체를 생성해서 가져온다.
return Posts.builder().title(title).content(content).author(author).build();
}
}
-단위 Test
: Controller에서 Dto를 생성해 DB까지 데이터를 조회하고, 값이 잘 저장되는 지 확인
* 항상 given, when, then을 이용해 코드를 작성하도록 한다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//JPA까지 테스트할 수 있도록 해준다. WebMvcTest는 단지 컨트롤러 테스트할 때 사용
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
//webEnvironment설정시 이용 가능하다. Rest Api를 호출 후 응답을 기다리는 동기 방식
@Autowired
private PostsRepository postsRepository;
@After
public void tearDown() throws Exception {
postsRepository.deleteAll();
}
@Test
public void Posts_Insert() 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); //응답을 Long을 받는다.
//postForObject: post요청에 따른 객체를 응답받는다.
//postForEntity: post요청에 따른 Entity를 응답받는다.
//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);
}
}
[Update & Select]
update 와 select 를 정리하기 전에, 각 객체별 역할과 메소드의 역할을 정리해보았다.
1. PostsController(WEB LAYER) - url과 REST Http Method를 이용해 Service Layer로 자원을 넘긴다.
2. PostsSaveRequestDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 요청)
(@Builder어노테이션은 계층간 이동과 요청 시 안전한 객체 생성을 위해, toEntity() 메소드는 Posts도메인을 이용하기 위해 생성)
3. PostsUpdateRequestDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 요청)
(@Builder 어노테이션만 존재, JPA의 영속성 컨텍스트 개념으로 인해 데이터변경만으로 update가 이루어진다.)
4. PostsResponseDto(WEB LAYER) - Web과 Service계층간 이동을 담당한다.(특히 응답)
(생성자(Posts)만 존재, Posts도메인을 이용하기 위해 생성)
5, PostsService(SERVICE LAYER) - 트랜잭션과 순서 보장
6. Posts(Domain, REPO LAYER) - 테이블과 맵핑되며, 로직을 담당
7. PostsRepository(Domain, REPO LAYER) - DB 접근을 위한 기본적인 CRUD 메소드 생성
* JPA 영속성 컨텍스트:
엔티티를 영구저장하는 공간.
- 자바애플리케이션과 데이터베이스 사이의 가상 데이터베이스 개념이다.
- 매 요청마다 EntityManagerFactory에서 EntityManager를 생성한다.
- 내부적으로 EntityManager가 DB커넥풀을 사용해 DB에 붙는다.
- 참고사항
[JPA] 영속성 컨텍스트와 플러시 이해하기
영속성 컨텍스트 JPA를 공부할 때 가장 중요한게 객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과 영속성 컨텍스트를 이해하는 것 이다. 두가지 개념은 꼭 알고 JPA를 활용하자.
ict-nroo.tistory.com
1. 우선 계층간 이동을 담당할 DTO 생성
- 업데이트할 Dto객체 생성
@Getter
@NoArgsConstructor //기본 생성자만 생성
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content){ //안전한 객체 생성 가능
this.title = title;
this.content = content;
}
}
- 응답 Dto객체 생성
@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();
}
}
2. Service에서 사용할 도메인모델 내 로직 생성
-Posts 도메인 내 메소드 생성
public void update(String title, String content){
this.title = title;
this.content = content;
}
3. Service 와 Controller 작성
-Service
@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);
}
-Controller
@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);
}
4. 단위 test
@Test
public void Posts_Update() throws Exception {
//given
//1. 데이터 저장
Posts savedPosts = postsRepository.save(Posts.builder().title("title").content("content").author("author").build());
//2. id값 얻고,
Long updateId = savedPosts.getId();
//3. 업데이트할 데이터 생성
String expectedTitle = "title2";
String expectedContent = "content2";
//4. 업데이트 객체 생성
PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder().title(expectedTitle).content(expectedContent).build();
//5. 테스트할 url 생성
String url = "http://localhost:"+port+"/api/v1/posts/" + updateId;
//6. 업데이트할 객체를 http객체안에 넣는다.
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
//when
//7. 테스트 시작, url에 post방식으로 http객체를 던지고, 반환값으로 long을 얻는다.
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);
}
[로컬테스트]
내장WAS로 구동시 H2라는 데이터베이스가 메모리에서 실행되기 때문에, 웹 콘솔을 이용해 데이터를 넣고, url을 조회해본다.
1. application.approties 내 옵션 추가
spring.h2.console.enabled=true
2. localhost:8080/h2-console 로그인 후 데이터 삽입
3. url을 이용해 json데이터 확인
[Jpa Auditing]
: Audit는 '감시하다' 라는 뜻으로, Jpa Auditing은 생성일자나 수정일자 등 자동으로 칼럼을 넣어주는 기능을 뜻한다.
* DB 테이블에 값을 넣거나 수정하는 행위는 '누가'. '언제' 하였는지 기록을 잘 남겨 놓아야 한다.
1.BaseTimeEntity 생성
@Getter
@MappedSuperclass //Jpa Entity들이 해당 필드들을 칼럼으로 인식하도록 한다
@EntityListeners(AuditingEntityListener.class) //Auditing기능 포함
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
2. 내장was 구동하는 Application.java에 활성화 어노테이션을 추가
@EnableJpaAuditing //Jpa Auditing 활성화
@SpringBootApplication //스프링 부트의 자동설정, 스프링 빈 읽기 모두 자동으로 설정
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args); //내장 WAS를 실행, 톰캣 설치가 필요X, Jar파일을 실행하면 된다.
}
}
3. 단위 테스트
@Test
public void BaseTimeEntity_Insert(){
//given
LocalDateTime now = LocalDateTime.of(2020,12,3,12,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(">>>>> "+posts.getCreatedDate()+" >>>>>>>> "+posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}
※ 해당 글은 "스프링 부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 저)"를 참고해 작성하였습니다.
'웹 프로그래밍 > Spring Boot' 카테고리의 다른 글
[Spring Boot] AWS 아키텍처(+ HotSpot으로 연결 시 여러 이슈들) (0) | 2020.12.06 |
---|---|
[Spring Boot] Spring Security (0) | 2020.12.06 |
[Spring Boot] Mustache, 화면 구성 (0) | 2020.12.06 |
[Spring Boot] TDD & 단위테스트 (0) | 2020.11.30 |
[Spring Boot] Build.Gradle 설정파일 (0) | 2020.11.29 |