본 포스팅은 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 책을 보고 작성하였음

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

API를 만들기 위해 총 3개의 클래스가 필요

  • Request 데이터를 받을 DTO
  • API 요청을 받을 Controller
  • 트랜잭션, 도메일 기능 간의 순서를 보장하는 Service
    * Service에서 비지니스 로직을 처리 하는 것이 아님

1.1 등록

1.1.1 PostsApiController

import com.study.aws.studyspringbootaws.service.posts.PostsService;
import com.study.aws.studyspringbootaws.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

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

1.1.2 PostsService

import com.study.aws.studyspringbootaws.domain.posts.PostsRepository;
import com.study.aws.studyspringbootaws.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;

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

* 스프링 BEAN 주입받는 방식

  • @Autowired
    • 권장하지 않음
  • setter
  • 생성자
    • 가장 권장하는 방식
  • 상기 Controller와 Service에서 사용한 @RequiredArgsContructor?
    • 롬복에서 지원
    • final이 선언된 모든 필드를 인자값으로 하는 생성자를 생성해줌

1.1.3 PostsSaveRequestDto

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import com.study.aws.studyspringbootaws.domain.posts.Posts;

@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 클래스와 거의 유사한 형태임에도 DTO 클래스를 추가로 생성한 이유?

  • 절대로 **Entity 클래스를 Request / Response 클래스로 사용해서는 안됨
  • Entity 클래스는 데이터 베이스와 맞닿은 핵심 클래스임
    • Entity 클래스를 기준으로 테이블이 생성된고, 스키마가 변경됨
  • View Layer와 DB Layer의 역활 분리 필요

1.1.4 테스트

import com.study.aws.studyspringbootaws.domain.posts.PostsRepository;
import com.study.aws.studyspringbootaws.web.dto.PostsSaveRequestDto;
import org.junit.After;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.study.aws.studyspringbootaws.domain.posts.Posts;

import java.util.List;

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

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void savePosts() throws Exception {

        //given
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhot:" + 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 사용하지 않는 이유

  • @WebMvcTest의 경우 JPA 기능이 작동하지 않기 때문
  • Controller와 ControllerAdive 등 외부 연동과 관련된 부분만 활성화됨
  • JPA 기능까지 한번에 테스트할 경우 @SpringBootTest와 TestRestTemplate을 사용하면 됨

1.2 수정 / 조회

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

1.2.2 PostsUpdateRequestDto / PostsResponseDto

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {

    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
import lombok.Getter;
import com.study.aws.studyspringbootaws.domain.posts.Posts;

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

* PostsResponseDto

  • Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣음

1.2.3 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 기능

  • 해당 기능의 경우 DB에 쿼리를 날리는 부분이 없음
    • JPA의 영속성 컨텍스트 때문임
      • 영속성 컨덱트스 란?
        - 엔티티를 영구 저장하는 환경
        - 트랜젹션 안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태
        - 해당 상태에서 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
        - 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다!!! -> 더티체킹!

1.2.4 수정 기능 테스트

    @Test
    public void updatePosts() 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> httpEntity = new HttpEntity<>(requestDto);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, 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);
    }

결과

1.2.5 조회 기능 테스트

  1. 로컬 테스트 H2 DB 직접접근하기 위해 웹 콘솔 활성화
  2. h2 콘솔
  3. h2 데이터 추가
  4. 출력

* PostMan을 이용하여 저장 / 수정 / 조회 기능 사용

  • 저장
  • 수정
  • 조회

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

단순하고 반복적인 코드를 해결

2.1 LocalDate 사용

2.1.1 이유

  • java8이 나오기 전까지 사용되었던 Data와 Calendar 클래스의 문제점
    • 불변(변경이 불가능한) 객체가 아님
  • Calendar는 월 (Month) 값 설계가 잘못되었음
    • 10월을 나타내는 Calendar.OCTOBER의 숫자 값은 '9'임
  • LocalDate, LocalDateTime이 데이터베이스에 제대로 매핑되지 않는 이슈 Hibernate 5.2.10버전에서 해결

2.2 JPA Auditing

2.2.1 BaseTimeEntity 클래스 생성

모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역활

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 ⓐ
@EntityListeners(AuditingEntityListener.class) ⓑ
public abstract class BaseTimeEntity {

    @CreatedDate ⓒ
    private LocalDateTime createdDate;

    @LastModifiedDate ⓓ
    private LocalDateTime modifiedDate;

}
  • ⓐ @MappedSuperclass
    • JPA Enitiy 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createDate, modifiedDate)도 칼럼으로 인식하도록 함
  • ⓑ @EntityListeners(AuditingEntityListener.class)
    • BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
  • ⓒ @CreatedDate
    • Entity가 생성되어 저장될 때 시간이 자동 저장됨
  • ⓓ @LastModifiedDate
    • 조회된 Entity의 값을 변경할 때 시간이 자동 저장됨

2.2.2 JPA Auditing 활성화

  1. Entity에 상속

  2. Application 클래스에 활성화 어노테이션 추가 (@EnableJpaAuditing)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing // JPA Auditing 활성화
@SpringBootApplication
public class StudySpringBootAwsApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudySpringBootAwsApplication.class, args);
    }

}

2.3.3 JPA Auditing 테스트

    @Test
    public void testBaseTimeEntity() {
        //given
        LocalDateTime now = LocalDateTime.of(2021, 11, 11, 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(">>>>>>>>> createDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

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

3. Spring 웹 계층

3.1 Web Layer

  • 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템블릿 영역임
  • 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역

3.2 Service Layer

  • @Service에 사용되는 서비스 영역
  • 일반적으로 Controller와 DAO의 중간 영역에서 사용됨
  • @Transactional이 사용되어야 하는 영역이기도 함

3.3 Repository Layer

  • 데이터 저장소에서 접근하느 영역
  • DAO(Data Access Object) 영역으로 이애하면 쉬움

3.4 DTOs

  • DTO(Data Transger Object)는 계층간에 데이터 교환을 위한 객체이며 DTOs는 이들의 영역을 이야기함
  • ex) 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등을 이야기함

3.5 Domain Model

  • 도메인이라 불리는 개발 대상을 모든 사림이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라함
  • @Enitity가 사용된 영역 역시 도메인 모델이라고 이해하면 됨
  • 다만 무조건 데이터베이스의 데이블과 관계가 있어야만 하는 것은 아님, Becouse, VO처럼 값 객체들도 이 영역에 해당되기 때문
profile
고우고우~

0개의 댓글