춤추는 개발자

[Spring boot] API의 CRUD 구현과 JPA Auditing 활용하기 본문

Developer's_til/스프링 프레임워크

[Spring boot] API의 CRUD 구현과 JPA Auditing 활용하기

Heon_9u 2021. 7. 14. 17:52
728x90
반응형

 지난 시간에 이어 API의 기본 CRUD를 구현하며 포스트이 생성시간/수정시간을 자동화하는 JPA Auditing을 활용합니다. 해당 포스트에서는 API를 개발하는 과정에서 특정 Annotation의 역할, Entity 대신 Dto를 생성/사용하는 이유 등 여러 상황들을 이해하고 넘어가는 것이 중요합니다.

 

 API를 만들기 위한 3개의 Class

 

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

✅ Spring 웹 계층

API를 구현하기 전, Spring 웹 계층에 간단히 알아보고 넘어가겠습니다.

 

Spring 웹 계층

 

  • Web Layer
    1. 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemaker 등의 뷰 템플릿 영역
    2. 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역
  • Service Layer
    1. @Service에 사용되는 서비스 영역
    2. 일반적으로 Controller와 Dao의 중간 영역에서 사용
    3. @Transactional이 사용되어야 하는 영역
  • Repository Layer
    1. Database와 같이 데이터 저장소에 접근하는 영역
    2. 기존에는 Dao(Data Access Object)영역으로 이해할 수 있다.
  • Dtos
    1. Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 이야기 하며 Dtos는 이들의 영역을 이야기합니다.
    2. 예를 들어 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기합니다.
  • Domain Model
    1. 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 합니다.
    2. 택시 App을 예로 든다면, 배차, 탑승, 요금 등이 모두 도메인이 될 수 있습니다.
    3. @Entity를 사용해보신 분들은 @Entity가 사용된 영역 역시 도메인 모델이라고 이해하면 됩니다.
    4. 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아닙니다.
    5. VO처럼 값 객체들도 이 영역에 해당되기 때문입니다.

 

📢주의하세요!
 가끔 오해하는 부분이, Service에서 비즈니스 로직을 처리해야 한다는 것입니다. 저 또한, 처음 Spring boot를 다룰 때, Service에서 로직을 처리했었습니다😂
 Service는 트랜잭션, 도메인 간 순서 보장의 역할만 합니다.
그렇다면, 실제로 비즈니스 로직을 처리하는 곳은 어디일까요?

 바로 Domain입니다.

 주문 취소 로직을 예로 든다면 Domain 모델에 포함되는 Order, Billing, Delivery라는 클래스(또는 인터페이스)에서 비즈니스 로직을 구현합니다. 
그럼 Service는 트랜잭션과 도메인 간의 메서드를 순서에 맞게 호출하는 역할을 수행하는 것입니다. 자세한 사항은 실제 CRUD 코드를 통해 확인하겠습니다.

 

 

✅ CREATE API

 가장 먼저, Controller와 Service 코드를 살펴보겠습니다.

 

<Controller>

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

 

 여기서는 스프링에서 Bean을 주입받는 방식을 가장 권장하는 방식인 생성자로 주입받는 방식을 사용했습니다. (@AutoWired는 권장하지 않습니다.)

 해당 방식은 @RequiredArgsConstructor로 구현했습니다. final이 선언된 모든 필드를 인자값으로 하는 생성자를 Lombok의 @RequiredArgsConstructor가 대신 생성해주는 것으로 @AutoWired와 동일한 효과를 볼 수 있습니다.

 

 @AutoWired 대신 생성자로 Bean을 주입하는 이유는 간단합니다!

 

 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위한 것입니다. 즉, A 클래스 내부에서 B라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 수정하고 싶은 경우, A 클래스까지 수정해야하는 상황이 발생합니다.

 이러한 경우를 A, B간의 강한 결합이라고 합니다.

 

 

 이제 Controller와 Servcie에서 사용할 Dto 클래스를 생성하겠습니다.

 

@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 클래스는 데이터베이스와 맞닿은 핵심 클래스이기 때문입니다.

  •  Entity 클래스를 기준으로 테이블이 생성되고 스키마가 변경됩니다.
  • 화면 변경을 사소한 문제지만, 테이블과 연결된 Entity 클래스 변경은 고려해야할 점이 많습니다.
  • Entity 클래스를 기준으로 동작하는 Service나 로직들에 영향을 끼치며 잦은 변경이 발생합니다.
  • 결국, View와 DB Layer의 역할이 모호해지며 클래스간의 결합도가 증가합니다.

 

 결론적으로 View Layer와 DB Layer의 역할을 철저하게 분리해야 합니다. 반드시 Entity 클래스와 Controller에서 사용할 Dto를 분리해서 사용해야 합니다.

 

 

✅ Read & Update API

 지금까지 데이터를 생성하는 기능으로 Create API를 구현했습니다. 이어서 수정/조회 기능을 위한 Controller와 Dto, Service 코드를 살펴보겠습니다.

 

<Controller>

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

 

<ResponseDto>

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

 

 해당 Dto는 Entity에서 필요한 필드만 받아 처리합니다.

 

<UpdateRequestDto>

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {

    private String title;
    private String content;

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

 

<Posts>

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

 

<Service>

@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 기능에서 데이터베이스에 쿼리를 날리는 부분이 없습니다. 이러한 코드가 가능한 이유는 JPA의 영속성 컨텍스트 때문입니다.

 영속성 컨텍스트란, Entity를 영구 저장하는 환경을 뜻합니다. JPA의 핵심 내용으로써 Entity가 영속성 컨텍스트에 포함되어 있냐 아니냐로 구분하게 됩니다.

 

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

 이 상태에서 트랜잭션이 끝나는 시점에 Entity 객체의 변경된 사항을 테이블에 반영합니다. 즉, Update 쿼리를 날릴 필요가 없는 것으로 이러한 개념을 더티 체킹이라고 합니다.

 

 

 

✅ JPA Auditing으로 생성/수정시간 자동화

 Entity에서 생성/수정시간은 차후 유지보수를 위해 중요한 정보입니다. 하지만, 이를 구현하려면 매번 DB에 날짜 데이터를 등록/수정하는 코드가 들어가게 됩니다.

 이러한 상황을 해결하고자 JPA Auditing을 활용합니다.

 

 LocalData

 Java8부터 날짜 타입으로 LocalDate와 LocalDataTime이 등장합니다. 기존에 사용하던 Data와 Calendar 클래스는 불변 객체가 아니라는 점Calendar의 월(Month) 값 설계가 잘못됐다는 점때문에 사용하지 않습니다.

 

 날짜 구현을 위한 Entity를 하나 생성합니다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

 

 

 JPA Auditing을 적용하기 위해 Posts 클래스에서 BaseTimeEntity를 상속받아 자동으로 Entity의 시간을 관리합니다. 마지막으로 Application 클래스에 @EnableJpaAuditing을 추가합니다.

 

Annotation명 역할
@MappedSuperClass JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우, 필드들(createdDate, modifiedDate)을 칼럼으로 인식하도록 합니다.
@EntityListeners
(AuditingEntityListener.class)
BaseTimeEntity 클래스에 Auditing 기능을 포함시킵니다.
@CreatedDate Entity가 생성시간이 자동 저장됩니다.
@LastModifiedDate 조회한 Entity의 값을 변경할 때, 수정시간이 자동 저장됩니다.

 

구현한 API들의 Test코드는 Github에서 확인할 수 있습니다.

 

 

 

🙋‍♂️느낀점

  API들의 기본 메서드인 CRUD를 개발하면서 코드의 유지보수를 높일 수 있는 기술들을 알 수 있었습니다. 인상깊었던 점은 크게 다음과 같습니다.

 

  1. Service는 트랜잭션과 도메인의 순서 보장의 역할! 실제 비즈니스 로직은 Domain에서 처리한다.
  2. 항상 Bean을 주입할 때마다 @AutoWired를 사용했는데, 이는 권장하는 방식이 아니라는 점, 생성자로 Bean을 주입하는 방식이 클래스간의 결합을 느슨하게 만들며, 의존성 관계에 따라 생성자를 계속 수정하는 번거로움을 없앨 수 있다.
  3. Entity 클래스가 아닌 Request와 Response용 Dto를 따로 만들어야 한다는 점. Entity 클래스는 DB와 맞닿아 있기 때문에 잦은 변경에는 고려해야할 점이 많다.
  4. JPA의 영속성 컨텍스트로 트랜잭션이 끝나는 시점에서 데이터의 변경점이 테이블에 반영된다.
  5. Entity의 생성/수정 시간을 JPA Auditing으로 관리할 수 있다.

 위에서 배운 것들의 주된 목적은 각 클래스의 역할을 철저하게 분리하기, 반복적인 코드 제거 및 간결화라는 것입니다. 지난 포스팅에서도 마찬가지로 Annotation인 @Getter로 getter 메서드를 자동 생성하며 코드를 간결하게 만들었습니다.

 반복적인 코드제거는 단순히 코드의 간결화를 넘어 차후 유지보수를 쉽게 만드는 미래 지향적인 코드입니다. 결국, 이러한 작업은 개발자의 숙명!!! 😁😁

 

 해당 포스팅보다 자세히 설명된 내용은 해당 교재를 구입하셔서 확인하시기 바랍니다! 

 

 

728x90
반응형