추상화는 객체지향 프로그래밍의 개념으로 기존 클래스의 공통적 요소, 기능을 추출해서 불필요한 부분을 생략하거나 중요한 부분을 중점으로 개략적으로 구성한 것이다.
프로그래밍에서는 인터페이스와 추상 클래스를 통해서 객체 행동을 정의하는 것으로 설명할 수 있겠다.
추상 클래스는 공통 속성 혹은 메소드를 정의하고, 일부 메소드는 하위 클래스에서 구현할 수 있다.
인터페이스는 클래스가 구현할 메소드를 선언한다.
실제 구현은 인터페이스를 구현할 클래스에서 모두 구현해야한다.
추상 클래스와 인터페이스는 객체를 생성할 수 없다.
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 테이블의 추상화 적용 코드를 보는 것으로 챕터를 마치겠다.