스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - Spring Data JPA

화나·2021년 1월 20일
0
post-thumbnail

JPA

  • 객체지향 프로그래밍 언어와 관계형 데이터베이스의 중간에서 패러다임을 일치시키기 위한 기술
  • 개발자는 객체지향적으로 프로그래밍을 하고 JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행함
  • 더이상 SQL에 종속적인 개발을 하지 않을 수 있음

Spring Data JPA

  • 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요함
  • 구현체는 대표적으로 Hibernate가 있는데 Spring에서 JPA를 사용할 때는 이런 구현체들을 직접 다루진 않고 구현체들을 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 사용한다.
    JPA ← Hibernate ← Spring Data JPA

Spring Data JPA가 등장한 이유

1. 구현체 교체의 용이성

  • 언젠가 Hibernate가 수명을 다해서 새로운 JPA 구현체가 대세로 떠오르면 Spring Data JPA 내부에서 구현체 매핑을 지원해 주기 때문에 쉽게 교체가 가능하다.

2. 저장소 교체의 용이성

  • 관계형 데이터 베이스 외에 다른 저장소로 쉽게 교체하기 위함, 다른 데이터베이스로 의존성만 교체하면 됨

요구사항 분석

  1. 게시판 기능
  • 게시글 조회, 등록, 수정, 삭제
  1. 회원 기능
  • 구글/네이버 로그인, 로그인한 사용자 글 작성 권한, 본인 작성글에 대한 권한 관리

프로젝트에 Spring Data JPA 적용하기

1. build.gradle에 의존성 추가

compile('org.springframework.boot:spring-boot-starter-data-jpa') : 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리 버전들을 관리해줌
compile('com.h2database:h2') : 별도의 설치 필요업이 프로젝트 의존성만으로 관리할수 있음, 메모리에서 실행됨 → 애플리케이션을 재시작할때마다 초기화되기 때문에 테스트 용도로 많이 사용함

2. domain 패키지 추가

  • domain : 소프트웨어에 대한 요구사항 혹은 문제영역

3. Posts 클래스 작성

@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;
    }
}
  • @NoArgsConstructor : 기본 생성자 자동 추가, public Posts() {}와 같은 효과
  • @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동 생성
  • @Builder : 해당 클래스의 빌더 패턴 클래스를 생성, 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함

    Posts 클래스의 한가지 특이점 : Setter 메소드가 없다.
    getter / setter를 무작정 생성하게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확히 구분할 수가 없다.
    그래서 Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다.
    대신, 해당 필드 값을 변경하고 싶다면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가하여 변경하여야 한다.

    그럼 Setter가 없는 이 상황에서 어떻게 값을 채워서 DB에 삽입을 할까?
    1. 기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는것
    2. 변경이 필요할 경우 해당 이벤트에 맞는 메소드를 호출하여 변경하는것을 전제로함

4. Posts 클래스로 DB에 접근하게 해줄 JpaRepository 생성

public interface PostsRepository extends JpaRepository<Posts, Long> {

}
  • DB Layer 접근자, JPA에선 Repository라 부르며 인터페이스로 생성
  • 단순히 인터페이스 생성 후 JpaRepository를 상속받으면 기본적인 CRUD 메소드가 자동으로 생성
  • @Repository 어노테이션을 추가할 필요도 없음, 다만 Entity 클래스와 기본 Repository는 함께 위치해야한다. -> Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없음
    -> 도메인 패키지에서 함께 관리함

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

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

    @After
    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);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
}
  • @After : 단위 테스트가 끝날때마다 수행되는 메소드를 지정, 여러 테스트가 동시에 수행되면 테스트용 데이터가 그대로 남아 있어 다음 테스트가 실패할 수 있기때문에 전부 삭제해준다.
  • save : 테이블 posts에 insert/update 쿼리를 실행한다. id 값이 있다면 update, 없다면 insert 쿼리가 실행된다.
  • findAll : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
  • 별다른 설정없이 @SpringBootTest를 사용할 경우 h2 데이터베이스를 자동으로 실행해줌

6. 등록/수정/조회 API 만들기

6-1. Spring 웹 계층

API를 만들기 위해서는 3개의 클래스가 필요함
1. Request 데이터를 받을 dto
2. API 요청을 받을 Controller
3. 트랜잭션, 도메인 기능간의 순서를 보장하는 Service
여기서 Service는 비지니스 로직을 처리하지 않고 트랜잭션, 도메인 간의 순서보장만 함
기존에 Service로 로직을 처리하던 방식을 트랜잭션 스크립트라고 부르는데, 이 방식을 사용하다보면
서비스 클래스 내부에서 모든 로직이 처리되게 되고 객체는 단순히 데이터 덩어리 역할만 하게 됨

  • Web Layer : 흔히 사용하는 컨트롤러와 뷰 템플릿의 영역, 외부 요청과 응답에 대한 전반적인 처리를 진행함.
  • Service Layer : @Service, @Transactional 어노테이션을 사용하며 일반적으로 Controller와 Dao의 중간 영역에서 사용
  • Repository Layer : 데이터베이스에 접근하는 영역, Dao와 같은 역할
  • Dtos : 계층간의 데이터 교환을 위한 객체(dto)의 영역
  • Domain Model(비지니스 처리를 담당해야 할 곳) : 도메인이라고 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해, 공유할 수 있도록 단순화 시킨것, 택시 앱이라고하면 배차, 탑승, 요금이 도메인이 될 수 있음
    : 도메인 모델 각자가 이벤트 처리를 하며, 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해줌

6-2. 등록/수정/조회 API 코드

PostsApiController

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

PostService

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }

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

@Autowired 어노테이션이 없는 이유
final이 선언된 모든 인자값으로 하는 생성자를 롬복의 어노테이션 @RequiredArgsConstructor가 생성해서 빈을 주입

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

update 메소드에서 데이터베이스에 쿼리를 날리는 부분이 없다.
-> JPA의 영속성 컨텍스트 때문
? 영속성 컨텍스트 : 엔티티를 영구 저장하는 환경, JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다.

JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태

영속성 컨텍스트가 유지된 상태에서 해당 데이터 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경된 사항을 반영함

PostsSaveRequestDto

@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 클래스인 Posts와 거의 유사한 형태임에도 dto 클래스를 새로 추가했음

Entity 클래스Request/Response dto
데이터베이스와 맞닿은 핵심 클래스view를 위한 클래스
Entity 클래스를 기준으로 테이블이 생성, 스키마 변경변경이 잦으며, 결과 값으로 여러 테이블을 조인해서 줘야할 경우가 빈번함

PostResponseDto

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

PostUpdateRequestDto

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content){
        this.title = title;
        this.content = content;
    }
}

6-3. JPA Auditing으로 생성시간/수정시간 자동화하기

보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함한다.(차후 유지보수에 중요한 정보이기 때문에) 그렇다 보니 매번 날짜 데이터를 등록/수정하는 코드가 포함되게 된다.
이런 단순하고 반복적인 코드가 모든 테이블과 서비스에 포함되어야 한다면 매우 지저분해지기 때문에 JPA Auditing을 사용하여 해결하려고 한다.

  • domain 패키지에 BaseTimeEntity 클래스 생성 : 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}
  • @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들도 컬럼으로 인식하도록 함
  • @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
  • @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동저장 됨
  • @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동저장 됨

마지막으로 JPA Auditing에 관련된 모든 어노테이션을 활성화 할 수 있도록 @EnableJpaAuditing 어노테이션을 메인메소드에 추가함

0개의 댓글