[Spring] CRUD abstraction

HOJUN·2024년 6월 17일

Backend - Spring

목록 보기
30/34

Abstraction

추상화는 객체지향 프로그래밍의 개념으로 기존 클래스의 공통적 요소, 기능을 추출해서 불필요한 부분을 생략하거나 중요한 부분을 중점으로 개략적으로 구성한 것이다.

프로그래밍에서는 인터페이스와 추상 클래스를 통해서 객체 행동을 정의하는 것으로 설명할 수 있겠다.

Abstract Class

추상 클래스는 공통 속성 혹은 메소드를 정의하고, 일부 메소드는 하위 클래스에서 구현할 수 있다.

Interface

인터페이스는 클래스가 구현할 메소드를 선언한다.
실제 구현은 인터페이스를 구현할 클래스에서 모두 구현해야한다.

추상 클래스와 인터페이스는 객체를 생성할 수 없다.
Spring에서는 인스턴스화할 수 없는 클래스는 스프링 컨테이너에 등록할 수 없다.

그렇다면 제목에 나와있는 CRUD는 어떤 방식으로 추상화한다는 것일까

우리는 각 테이블(Board, Post, Reply)의 Controller, db, model, service 레이어의 구현을 진행했었다.

각 테이블은 모두 레이어를 가지고 있고, Controller, Service, Repository의 데이터 교환이 이루어지도록 설계했다.

CRUD의 직접적인 호출은 Controller클래스에서,
CRUD의 내부 구조는 Service클래스에서,
View와 Repository간 데이터 교환 작업은 Converter로 구현했다.

각 테이블 간 공통적인 기능과 구현부를 crud라는 인터페이스로 묶어보자.

public interface CRUDInterface<DTO> {
    DTO create(DTO t);

    Optional<DTO> read(Long id);

    DTO update(DTO t);

    void delete(Long id);

    API<List<DTO>> list(Pageable pageable);
}

그리고 Entity와 DTO간 데이터 조작, 교환 또한 인터페이스로 묶었다.

public interface Converter<DTO, ENTITY> {

    DTO toDTO(ENTITY entity);

    ENTITY toENTITY(DTO dto);
}

Service에서 발생하는 CRUD의 내부 구조는 기본적으로 DTO -> Repository -> Entity, 혹은 그 반대에 있다.

각 테이블의 Entity와 DTO간 CRUD 기능을 추상클래스로 구현할 수 있다.

public abstract class CRUDService<DTO, ENTITY> implements CRUDInterface<DTO> {

    @Autowired(required = false)
    private JpaRepository<ENTITY, Long> jpaRepository;

    @Autowired(required = false)
    private Converter<DTO, ENTITY> converter; //Converter를 상속받은 bean이 있으면 컨테이너에 등록, 없으면 null값

    @Override
    public DTO create(DTO dto) {
        var entity = converter.toENTITY(dto);

        jpaRepository.save(entity);

        var returnDTO = converter.toDTO(entity);

        return returnDTO;
    }

    @Override
    public Optional<DTO> read(Long id) {
        var optionalEntity = jpaRepository.findById(id);

        var dto = optionalEntity.map(
                it -> {
                    return converter.toDTO(it);
                }
        ).orElseGet(() -> null);

        return Optional.ofNullable(dto);
    }

    @Override
    public DTO update(DTO dto) {

        var entity = converter.toENTITY(dto);

        jpaRepository.save(entity);

        var returnDTO = converter.toDTO(entity);

        return returnDTO;
    }

    @Override
    public void delete(Long id) {
        jpaRepository.deleteById(id);
    }

    @Override
    public API<List<DTO>> list(Pageable pageable) {
        var list = jpaRepository.findAll(pageable);

        var pagination = Pagination.builder()
                .page(list.getNumber())
                .size(list.getSize())
                .currentElements(list.getNumberOfElements())
                .totalElements(list.getTotalElements())
                .totalPage(list.getTotalPages())
                .build();

        var dtoList = list.stream()
                .map(it -> {
                    return converter.toDTO(it);
                }).collect(Collectors.toList());

        var response = API.<List<DTO>>builder()
                .body(dtoList)
                .pagination(pagination)
                .build();
        return response;
    }
}

여기서 등장하는 @AutoWired는 위에서 했던 이야기를 이어받는다.
@Autowired는 주입할 빈이 반드시 존재해야 한다.

(required = false)지시자는 빈에 등록된 클래스를 상속 받는 빈이 있다면 스프링 컨테이너에 등록된 빈을 가져오고, 없다면 null을 반환한다.
때문에 추상클래스인 CRUDService는 @Autowired(required = false) 어노테이션을 통해서 빈이 없어도 예외를 발생시키지 않고 null을 필드에 주입한다.

Controller레이어 또한 추상화할 수 있다.

public abstract class CRUDAbstractApiController<DTO, ENTITY> implements CRUDInterface<DTO>{

    @Autowired(required = false)
    private CRUDService<DTO, ENTITY> crudService;

    @PostMapping("")
    @Override
    public DTO create(
            @Valid
            @RequestBody
            DTO t) {
        return crudService.create(t);
    }

    @GetMapping("/id/{id}")
    @Override
    public Optional<DTO> read(
            @PathVariable
            Long id) {
        return crudService.read(id);
    }

    @PutMapping("")
    @Override
    public DTO update(
            @Valid
            @RequestBody
            DTO t) {
        return crudService.update(t);
    }

    @DeleteMapping("")
    @Override
    public void delete(
            @PathVariable
            Long id) {
        crudService.delete(id);
    }


    @GetMapping("/all")
    @Override
    public API<List<DTO>> list(
            @PageableDefault
            Pageable pageable) {
        return crudService.list(pageable);
    }
}

결국 CRUD라는 공통적인 기능을 구현한 하나의 패키지를 재사용해서 테이블마다 필요했던 긴 코드를 작성하지 않게 추상화한 것이다.

포스팅이 길어지므로
다음 포스팅에서 Reply 테이블의 추상화 적용 코드를 보는 것으로 챕터를 마치겠다.

0개의 댓글