[스프링 부트와 AWS로 혼자 구현하는 웹 서비스]무작정 따라하기 3일차

민지킴·2021년 3월 29일
0
post-thumbnail

*모든 내용은 책에 있는 내용을 기반으로 작성하였습니다.

3장 ----

기존의 MyBatis같은 SQL 매퍼를 이용해서 데이터베이스의 쿼리를 작성한다.
하지만 이렇게 되면 SQL을 다루는 시간이 개발 시간보다 늘어나서 객체지향 프로그래밍과 어긋나게 되는 문제점이 생겼다.
이 문제의 해결책으로 JPA라는 자바 표준 ORM(object relational mapping) 기술을 만나게 된다.
둘의 차이점은 ORM은 객체를 매핑하는것이고, SQL Mapper는 쿼리를 매핑한다.

3.1 JPA 소개

JPA는 서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어, 관계형 데이터베이스)을 중간에서 패더라임 일치를 시켜주기 위한 기술이다.
즉 개발자는 객체지향적으로 프로그래밍을 하고 JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다. 개발자는 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 된다.

3.1.1 Spring Data JPA

JPA는 인터페이스로서 자바 표준명세서이다. 인터페이스인 JPA를 사용하기 위해서는 구현체라 필요하며
대표적으로 Hibernate, Eclipse Link등이 있다. 하지만 Spring 에서는 이 구현체들을 좀 더 쉽게 사용하고자 추상화 시킨 Spring Data JPA라는 모듈을 사용해서 JPA기술을 다룬다.

JPA <- Hibernate <- Spring Data JPA

Hibernate와 Spring Data JPA는 큰 차이는 없지만 아래와 같은 이유로 등장했다고 볼수 있다.

  • 구현체 교체의 용이성
  • 저장소 교체의 용이성

1) 구현체 교체의 용이성

Hibernate 외에 다른 구현체로 쉽게 교체하기 위함이다. Hibernate가 언젠간 수명을 다해서 새로운 JPA 구현체가 대세로 떠오를때 Spring Data JPA를 쓰고 있다면 매우 쉽게 교체 가능
-> Spring Data JPA 내부에서 구현체 매핑을 지원해 주기 때문이다.

2) 저장소 교체의 용이성

Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문에
Spring Data JPA, Spring Data Redis, Spring Data MongoDB등으로 교체되어도 기본적인 기능은 변경할 것이 없다.

3.2 프로젝트에 Spring Data Jpa 적용하기

3.2.1 Build.gralde

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok:')
    compile('org.springframework.boot:spring-boot-starter-data-jpa') //(1)
    compile('com.h2database:h2') //(2)
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

3.2.1.1 spring-boot-starter-data-jpa

  • 스프링 부트용 Spring Data Jpa 추상화 라이브러이다.
  • 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전을 관리해준다.

3.2.1.2 h2

  • 인메모리 관계형 데이터베이스이다.
  • 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할때마다 초기화되므로 테스트, 로컬환경에서 사용한다.

3.2.2 domain 패키지

도메인이란 게시글, 댓글, 회원, 정선, 결제 등 소프트웨어에 대한 요구사항 혹은 문제영역이라고 생각하면 된다. mybatis의 dao 패키지와는 결이 살짝 다르다.

3.2.3 Posts.java

package com.jojoldu.book.springboot.domain.posts;

import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter //(6)
@NoArgsConstructor //(5)
@Entity //(1)
public class Posts extends BaseTimeEntity {
   
    @Id //(2)
    @GeneratedValue(strategy = GenerationType.IDENTITY) //(3)
    private long id;

    @Column(length = 500, nullable = false) //(4)
    private String title;

    @Column(columnDefinition = "TEXT" , nullable = false)
    private String content;

    private String author;

    @Builder //(7)
    public Posts(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public void update(String title, String content){
        this.title = title;
        this.content=content;
    }


}

실제 DB와 매핑될 클래스에 대해서는 @Column을 사용한다. 이것을 Entity 클래스라고도 한다.
JPA를 사용한다면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는 Entity 클래스의 수정을 통해 작업한다.

3.2.3.1 @Entity

  • 테이블과 링크될 클래스임을 나타낸다.
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
    ex) SalesManager.java -> sales_manager table

3.2.3.2 @Id

  • 해당 테이블의 PK 필드를 나타낸다.

3.2.3.3 @GenerateValue

  • PK의 생성 규칙을 나타낸다.
  • 스프링 부트 2부터는 GenerationType.IDENTITY 가 있어야지 auto increment가 된다.

3.2.3.4 @Column

  • 테이블의 칼럼을 나타내며 굳이 선언하지 않아도 해당 클래스의 필드는 모두 칼럼이 된다.
  • 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
  • 문자열의 기본인 varchar(255) 를 500으로 늘리거나, 타입을 변경하고 싶은경우에 사용한다.

왠만하면 Entity의 PK는 Long 타입의 Auto increment를 추천하며, 주민등록번호 같이 비즈니스상 유니크키나, 여러 키를 조합한 복합키로 PK를 사용하게 되면 난감한 상황이 올수도 있다.

3.2.3.5 -> Lombok

  • NoArgsConstructor : 기본생성자 자동추가
  • Getter : 클래스 내 모든 필드에 Getter 추가
  • Builder : 해당 클래스의 빌더 패턴 클래스를 생성

Entity 클래스에는 절대 Setter Method를 만들지 않는다.
만든다 하더라도 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야한다.
값을 추가할때는 생성자를 통해 최종값을 채운후 DB에 삽입한다.

3.2.4 PostsRepository.interface

보통 Mybatis에서는 Dao라고 불리면 JPA에서는 Repository라고 불리며 인터페이스로 생성한다.
JpaRepository<Entity 클래스, PK 타입>을 성속하면 기본적인 CRUD 메소드가 자동으로 생성된다.
@Repository를 추가할 필요도 없다.

Entity 클래스와 기본 Entity Repository는 함께 위치 하는 점이다.
항상 도메인 패키지에서 함께 관리한다.

3.3 Spring Data JPA 테스트 코드 작성하기

3.3.1 PostsRepositoryTest.java

package com.jojoldu.book.springboot.domain.posts;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDateTime;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After // (1)
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test 
    public void 게시글저장_불러오기() {
        //given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder() //(2)
                .title(title)
                .content(content)
                .author("jojoldu@gmail.com")
                .build());

        //when
        List<Posts> postsList = postsRepository.findAll(); //(3)

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntity_등록(){
        //given
        LocalDateTime now = LocalDateTime.of(2019,6,4,0,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(">>>>>>> createData="+posts.getCreateDate()+", modifiedDate"+posts.getModifiedDate());

        assertThat(posts.getCreateDate().isAfter(now));
        assertThat(posts.getModifiedDate().isAfter(now));
    }
}

3.3.1.1 @After

  • Junit에서 단위 테스트가 끝날때마다 수행되는 메소드를 지정
  • 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기위해 사용한다.
  • 여러 테스트가 동시에 수행되면 테스트용 db인 h2에 데이터가 그대로 남아 있어 다음 테스트가 실패할 수 있다.

3.3.1.2 postsRepository.save

  • 테이블 posts에 insert/update 쿼리를 실행한다.
  • id 값이 있다면 update가, 없다면 insert 쿼리가 실행된다.

3.3.1.3 postRepository.findAll

  • 테이블 posts에 있는모든 데이터를 조회하는 메소드이다.

3.3.2 application.properties

쿼리를 콘솔에서 확인해보고 싶으면
spring.jpa.show_sql=true

쿼리 로그를 Mysql 버전으로 변경
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

3.4 등록/수정/조회 API 만들기

API를 만들기 위해서는 총 3개의 클래스가 필요하다.

  • Request 데이터를 받을 Dto
  • API 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

Service에서 비즈니스 로직을 처리해야 하는건 오해이다.
Service는 트랜잭션, 도메인간 순서 보장의 역할만 한다.

Spring 웹 계층

  • Web Layer
    • 흔히 사용하는 컨트롤러와 JSP 등의 뷰 템플릿 영역이다.
    • 이외에도 필터, 인터셉터, 컨트롤러 어드바이스등 외부 요청과 응답에 대한 전반적인 영역을 이야기한다.
  • Service Layer
    • @Service에서 사용된다.
    • 일반적으로 Controller와 Dao의 중간영역에서 사용되며 @Transactional이 사용되어야한다.
  • Repository Layer
    • Database와 같이 데이터 저장소에 접근하는 영역이다.
    • Dao 영역으로 이해하면 편하다
  • Dtos
    • Dto는 계층 간에 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 이야기한다.
    • 다른 Layer에서 결과로 넘겨준 객체 등이 이들을 말한다.
  • Domain Model
    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 잇고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라한다.
    • ex) 택시앱 :배차, 탑승, 요금 등이 모두 도메인이 될수 있다.
    • @Entity가 사용된 영역도 도메인 모델이다.
    • 비즈니스 처리를 담당해야 하는곳

스프링에서는 Bean을 주입받는 방법이 3가지가 있다.
1) @Autowired(권장 x)
2) Setter
3) 생성자

여기서 생성자로 Bean을 받도록 하기 위해서는 @RequiredArgsConstructor 에서 해결을 할수가 있다.
final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor가 만들어준다.

롬복 어노테이션을 사용하는 이유:
해당 클래스의 의존성 관계가 변경될때마다 생성자 코드를 계속 수정해야하는 번거로움을 해결하기 위함이다.

3.4.1 PostsSaveRequestDto

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@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();
    }
}

Dto를 만들때 Entity와 유사할 수는 있다.
하지만 Entity를 Request, Response 클래스로 사용하면 안된다.
Entity 클래스는 데이터베이스와 맞닿은 핵심클래스로, Entity 클래스를 기준으로 테입르이 생성되고 스키마가 변경되기 때문이다.
Response와 Request 용 Dto는 View만을 위한 클래스라 정말 자주 변경해야하는 반면
Entity클래스는 여러 클래스에 영향을 끼치기 때문이다.

따라서 View Layer 와 DB Layer는 철저하게 분리하는것이 좋다.

3.4.2 PostsService.java

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.service.posts.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }

    @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);

    }

}

3.4.2.1 영속성 컨텍스트

update 기능에서 테이브터베이스에 쿼리를 날리는 부분이 없다.
이게 가능한 이유가 JPA의 영속성 컨텍스트 때문이다.

영속성 컨텍스트란?
엔티티를 영구 저장하는 환경으로, 일종의 논리적 개념으로, JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있느냐 아니냐로 갈린다.

JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지되 상태이다.
이 상태에서 해당 데이터 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
즉 Entity 객체의 값만 변경하면 별도로 update 쿼리를 날릴 필요가 없다.
이것을 더티 체킹(ditry checking)이라고 한다.

3.4.3 웹에서 테스트해보기

3.4.3.1 application.properties

spring.h2.console.enabled=true

추가하기

톰캣이 8080포트로 실행되었다는 전제로
http://localhost:8080/h2-console 에 접속

JDBC URL에 : jdbc:h2:mem:testdb
로 설정해준다.

3.5 JPA Auditing으로 생성시간/수정시간 자동화 하기

3.5.1 BaseTimeEntity.java

package com.jojoldu.book.springboot.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass //(1)
@EntityListeners(AuditingEntityListener.class) //(2)
public class BaseTimeEntity {

    @CreatedDate //(3)
    private LocalDateTime createDate;

    @LastModifiedDate //(4)
    private LocalDateTime modifiedDate;
}

BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어 createdTime, modifiedDate를 자동으로 관리한다.

3.5.1.1 MappedSuperClass

  • JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들도 칼럼으로 인식

3.5.1.2 EntityListeners(AuditingEntityListener.class)

  • BaseTimeEntity 클래스에 Auditing 기능을 포함한다.

3.5.1.3

  • CreatedDate : Entity가 생성되어 저장될때 시간이 자동저장된다.
  • LastModifiedDate : 조회한 Entity의 값을 변경할때 시간이 자동저장된다.
profile
하루하루는 성실하게 인생 전체는 되는대로

0개의 댓글