[Study-4주차] MapStruct, Exception 적용 + CRUD 수정 + Controller 수정 + ResponseFormat - ①
[Study-3주차] PetClinicProject CRUD - ②
[Study-3주차] PetClinicProject CRUD - ②
[Study-3주차] PetClinicProject CRUD - ① [Study-3주차] PetClinicProject CRUD - ① [Study-2주차] PetClinicProject 초기 설정 + 클래스 생성 + 코드 설명 [Study-2주차] PetClinicProject 초기 설정 + 클래스 생성 + 코드 설명 [Stu
soohykeee.tistory.com
앞서 간단한 CRUD를 구현하였다. Owner, Pet, Visit은 구현을 완료했지만 Vet의 경우는 제대로 구현하지 못했다. 추가적으로 MapStruct와 Exception을 적용하고, Controller도 수정해주었다.
또한 클래스명들을 뒤에 '-s' 붙여 복수로 사용하였는데, 명명 규칙을 깔끔하게 해주기 위해서 모두 단수로 바꿔주었다. 그리고 패키지 구조도 조금의 수정을 해주었다.
https://github.com/soohykeee/jsh-PetClinic-Study
우선 MapStruct 적용부터 해보겠다. 아래의 글을 참고하면 조금 더 수월할 수 있다.
[study] MapStruct 란?
들어가기에 앞서.. Study를 하며 Controller, Service, Repsitory와 같은 여러 계층에서 데이터를 서로 교환할 때 사용하는 DTO와 Entity를 서로 형변환 해주는 과정을 하는데 이를 효율적으로 사용해주기 위
soohykeee.tistory.com
MapStruct 적용
MapStruct를 적용하기 전 build.gradle에 의존성을 추가해주었다.
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// MapStruct
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
해당 MapStruct Support Plugin도 설치해주는 것이 간편하다. 해당 플러그인이 없이 구현을 해보았는데 만약 파라미터가 2개이상을 받게될경우, 자동으로 정보를 끌어오는 것이 조금 서툰편이다. 하지만 해당 플러그인을 사용하게 되면, 파라미터를 여러개 받아오게 되어도 자동화가 잘 되는 것을 확인할 수 있다.
OwnerMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface OwnerMapper {
Owner toEntity(OwnerRequestDto.CREATE create);
@Mapping(target = "ownerId", source = "id")
OwnerResponseDto.READ toReadDto(Owner owner);
}
PetMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface PetMapper {
Pet toEntity(PetRequestDto.CREATE create);
@Mapping(target="petId", source = "id")
@Mapping(target = "ownerFirstName", source = "owner.firstName")
@Mapping(target = "ownerTelephone", source = "owner.telephone")
PetResponseDto.READ toReadDto(Pet pet);
}
SpecialtyMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface SpecialtyMapper {
Specialty toEntity(String specialtyName);
}
VetMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface VetMapper {
Vet toEntity(VetRequestDto.CREATE create, List<VetSpecialty> vetSpecialties);
VetResponseDto.READ toReadDto(Vet vet, List<String> specialtiesName);
}
VetSpecialtyMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface VetSpecialtyMapper {
VetSpecialty toEntity(Specialty specialty, Vet vet);
}
VisitMapper
package kr.co.jshpetclinicstudy.service.model.mapper;
@Mapper(componentModel = "spring")
public interface VisitMapper {
Visit toEntity(VisitRequestDto.CREATE create);
@Mapping(target = "visitId", source = "id")
@Mapping(target = "petName", source = "pet.name")
@Mapping(target = "petType", source = "pet.type")
@Mapping(target = "ownerFirstName", source = "pet.owner.firstName")
VisitResponseDto.READ toReadDto(Visit visit);
}
MapStruct를 의존성을 받아와서 사용해주게 되면, 반환값과 파라미터에서 사용하는 컬럼명이 동일하게 되면 자동으로 정보를 가져와서 Mapping을 해준다. 그렇기에 해당 이름이 다른경우에는 위에서 사용한 것처럼 @Mapping 어노테이션을 사용해서 수동으로 Mapping을 해주면 된다.
이제 위에서 MapStruct를 사용해주었기에, 기존에 각각의 Entity에서 작성해주었던 entityToDto, dtoToEntity와 같은 메서드들을 모두 지워줘도 된다. 추가적으로 해당 MapStruct를 적용해보겟다.
OwnerService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class OwnerService {
private final OwnerRepository ownerRepository;
private final OwnerMapper ownerMapper;
@Transactional
public void createOwner(OwnerRequestDto.CREATE create) {
final Owner owner = ownerMapper.toEntity(create);
ownerRepository.save(owner);
}
public List<OwnerResponseDto.READ> getOwnerList() {
return ownerRepository.findAll().stream()
.map(ownerMapper::toReadDto).collect(Collectors.toList());
}
public OwnerResponseDto.READ getOwner(Long id) {
final Optional<Owner> owner = ownerRepository.findById(id);
isOwner(owner);
return ownerMapper.toReadDto(owner.get());
}
@Transactional
public void updateOwner(OwnerRequestDto.UPDATE update) {
final Optional<Owner> owner = ownerRepository.findById(update.getOwnerId());
isOwner(owner);
owner.get().changeOwnerCity(update.getCity());
owner.get().changeOwnerAddress(update.getAddress());
owner.get().changeOwnerTelephone(update.getTelephone());
owner.get().changeOwnerFirstName(update.getFirstName());
owner.get().changeOwnerLastName(update.getLastName());
ownerRepository.save(owner.get());
}
@Transactional
public void deleteOwner(Long id) {
final Optional<Owner> owner = ownerRepository.findById(id);
isOwner(owner);
ownerRepository.deleteById(id);
}
private void isOwner(Optional<Owner> owner) {
if (owner.isEmpty()) {
throw new RuntimeException("This Owner is Not Exist");
}
}
}
PetService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class PetService {
private final PetRepository petRepository;
private final PetMapper petMapper;
@Transactional
public void createPet(PetRequestDto.CREATE create) {
final Pet pet = petMapper.toEntity(create);
petRepository.save(pet);
}
@Transactional
public List<PetResponseDto.READ> getPetList() {
return petRepository.findAll().stream()
.map(petMapper::toReadDto).collect(Collectors.toList());
}
@Transactional
public PetResponseDto.READ getPet(Long id) {
final Optional<Pet> pet = petRepository.findById(id);
isPet(pet);
return petMapper.toReadDto(pet.get());
}
@Transactional
public List<PetResponseDto.READ> getPetListOfOwner(Long ownerId) {
return petRepository.findPetListByOwnerId(ownerId).stream()
.map(petMapper::toReadDto).collect(Collectors.toList());
}
@Transactional
public void updatePet(PetRequestDto.UPDATE update) {
final Optional<Pet> pet = petRepository.findById(update.getPetId());
isPet(pet);
pet.get().changePetName(update.getName());
pet.get().changePetBirtDate(update.getBirthDate());
pet.get().changePetType(Type.valueOf(update.getType()));
petRepository.save(pet.get());
}
@Transactional
public void deletePet(Long id) {
final Optional<Pet> pet = petRepository.findById(id);
isPet(pet);
petRepository.deleteById(id);
}
private void isPet(Optional<Pet> pet) {
if (pet.isEmpty()) {
throw new RuntimeException("This Pet is Not Exist");
}
}
}
VisitService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class VisitService {
private final VisitRepository visitRepository;
private final VisitMapper visitMapper;
@Transactional
public void createVisit(VisitRequestDto.CREATE create) {
final Visit visit = visitMapper.toEntity(create);
visitRepository.save(visit);
}
@Transactional
public List<VisitResponseDto.READ> getVisitList() {
return visitRepository.findAll().stream()
.map(visitMapper::toReadDto).collect(Collectors.toList());
}
@Transactional
public VisitResponseDto.READ getVisit(Long id) {
final Optional<Visit> visit = visitRepository.findById(id);
isVisit(visit);
return visitMapper.toReadDto(visit.get());
}
@Transactional
public List<VisitResponseDto.READ> getVisitListOfPet(Long petId) {
return visitRepository.findVisitListByPetId(petId).stream()
.map(visitMapper::toReadDto).collect(Collectors.toList());
}
@Transactional
public void updateVisit(VisitRequestDto.UPDATE update) {
final Optional<Visit> visit = visitRepository.findById(update.getVisitId());
isVisit(visit);
visit.get().changeVisitDate(update.getVisitDate());
visit.get().changeVisitDescription(update.getDescription());
visit.get().changeVisitPet(update.getPet());
visitRepository.save(visit.get());
}
@Transactional
public void deleteVisit(Long id) {
final Optional<Visit> visit = visitRepository.findById(id);
isVisit(visit);
visitRepository.deleteById(id);
}
private void isVisit(Optional<Visit> visit) {
if (visit.isEmpty()) {
throw new RuntimeException("This Visit is Not Exist");
}
}
}
VetService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class VetService {
private final VetRepository vetRepository;
private final SpecialtyRepository specialtyRepository;
private final VetMapper vetMapper;
private final SpecialtyMapper specialtyMapper;
private final VetSpecialtyMapper vetSpecialtyMapper;
@Transactional
public void createVet(VetRequestDto.CREATE create) {
//Vet Entity Create
Vet vet = vetMapper.toEntity(create, Collections.emptyList());
//create or get
final List<VetSpecialty> vetSpecialties = getOrCreateVetSpecialties(create.getSpecialtiesName(), vet);
vet.changeVetSpecialties(vetSpecialties);
vetRepository.save(vet);
}
@Transactional
public VetResponseDto.READ getVet(Long id) {
final Optional<Vet> vet = vetRepository.findById(id);
isVet(vet);
final List<String> specialtiesName = getSpecialtiesNameByVet(vet.get());
return vetMapper.toReadDto(vet.get(), specialtiesName);
}
@Transactional
public void updateVet(VetRequestDto.UPDATE update) {
final Optional<Vet> vet = vetRepository.findById(update.getVetId());
isVet(vet);
final List<VetSpecialty> vetSpecialties = getOrCreateVetSpecialties(update.getSpecialtiesName(), vet.get());
vet.get().changeVetSpecialties(vetSpecialties);
vetRepository.save(vet.get());
}
@Transactional
public void deleteVet(Long id) {
final Optional<Vet> vet = vetRepository.findById(id);
isVet(vet);
vetRepository.delete(vet.get());
}
private List<String> getSpecialtiesNameByVet(Vet vet) {
return vet.getVetSpecialties().stream()
.map(VetSpecialty::getSpecialty)
.map(Specialty::getSpecialtyName)
.collect(Collectors.toList());
}
private List<Specialty> getOrCreateVetSpecialtiesByNames(List<String> names) {
//names = "n1, n2, n3"
//entity = n1, n2
List<Specialty> specialties = specialtyRepository.findAllBySpecialtyNameIn(names);
//set = n1.name, n2.name
final Set<String> existNames = specialties.stream()
.map(Specialty::getSpecialtyName)
.collect(Collectors.toSet());
//없는 것들 여기서 필터로 걸러서 생성
//list = n3
final List<Specialty> createSpecialties = names.stream()
.filter(name -> !existNames.contains(name))
.map(specialtyMapper::toEntity)
.collect(Collectors.toList());
//list = n1, n2, n3
specialties.addAll(createSpecialties);
return specialties;
}
private List<VetSpecialty> getOrCreateVetSpecialties(List<String> names, Vet vet) {
final List<Specialty> specialties = getOrCreateVetSpecialtiesByNames(names);
//연관 엔티티 반
return specialties.stream()
.map(specialty -> vetSpecialtyMapper.toEntity(specialty, vet))
.collect(Collectors.toList());
}
private void isVet(Optional<Vet> vet) {
if (vet.isEmpty()) {
throw new RuntimeException("This Vet is Not Exist");
}
}
}
위에서 VetService를 보게되면 기존에 작성한 CRUD와 바뀐것을 확인할 수 있다. 기존에는 Vet과 Specialty를 연관테이블을 Null로 준 후 각각 CRUD가 가능하도록 해준 후, Vet_Specialty를 연결하는 연관테이블에서 연결해주는 방식으로 CRUD를 진행하려고 했다. 하지만 이러한 방식은 객체지향스럽지 않고, 실제 환경으로 생각했을 때 비효율적인 방식이기에 수정을 해주었다.
수정해준 새로운 CRUD 방식은 다음과 같다.
- Vet(수의사)를 Create 해줄 때, Specialty(전문학위)를 List로 입력을 받아온다.
- 작성한 Specialty를 실제 Specialty Table과 비교하여 값이 없는 새로운 Specialty인 경우 해당 테이블에도 Create를 해준다. (만약 해당 테이블에 값이 있는 경우라면 Create 해주지 않는다.)
- 연관테이블에도 이때 값을 넣어주고, 연결해준다.
위와 같은 방식으로 VetService를 구현해주었다. 코드를 하나씩 살펴보겠다.
@Transactional
public void createVet(VetRequestDto.CREATE create) {
//Vet Entity Create
Vet vet = vetMapper.toEntity(create, Collections.emptyList());
//create or get
final List<VetSpecialty> vetSpecialties = getOrCreateVetSpecialties(create.getSpecialtiesName(), vet);
vet.changeVetSpecialties(vetSpecialties);
vetRepository.save(vet);
}
Create의 경우를 보게 되면, 우선 Mapper를 통해 dto로 들어온 값을 entity로 변환해준다. 이때 위에서 말한 것처럼 Specialty가 이미 존재하는 학위인지, 존재하지 않는 학위인지를 알 수 없기에 Collections.emtpyList()로 받아서 빈값을 넣어준다.
getOrCreateVetSpecialties() 메소드를 보게되면, 메소드명을 통해 유추가 가능하겠지만, 만약 Specialty에 값이 있다면 Get으로 가져오기만 하고, 만약 없다면 Create 해주는 메소드이다.
해당 메서드를 통해 Specialty에 값을 생성해주고, 연관테이블에도 값을 연결해준 후, changeVetSpecialties() 메서드를 통해 우리가 empty로 지정해줬던 값을 수정해서 값을 넣어준 후, save 해주는 것이다.
private List<Specialty> getOrCreateVetSpecialtiesByNames(List<String> names) {
//작성해준 값(names)이 n1, n2, n3 이고, 실제 DB에 n1, n2 값만 존재한다고 가정해보자
//아래의 findAllBySpecialtyNameIn를 통해 실제 DB와 비교하여 names와 같은 값이 존재하는
//n1, n2가 반환되어 List로 저장이 된다.
List<Specialty> specialties = specialtyRepository.findAllBySpecialtyNameIn(names);
//set = n1.name, n2.name
//위에서 반환받은, 이미 DB에 있는 값들인 n1, n2가 Set으로 저장이 된다.
final Set<String> existNames = specialties.stream()
.map(Specialty::getSpecialtyName)
.collect(Collectors.toSet());
//list = n3
//names와 비교하여 위에서 존재하는 n1, n2를 제외한 n3 값이 createSpecialties에 들어간다.
final List<Specialty> createSpecialties = names.stream()
.filter(name -> !existNames.contains(name))
.map(specialtyMapper::toEntity)
.collect(Collectors.toList());
//list = n1, n2, n3
specialties.addAll(createSpecialties);
return specialties;
}
private List<VetSpecialty> getOrCreateVetSpecialties(List<String> names, Vet vet) {
final List<Specialty> specialties = getOrCreateVetSpecialtiesByNames(names);
//연관 엔티티 반
return specialties.stream()
.map(specialty -> vetSpecialtyMapper.toEntity(specialty, vet))
.collect(Collectors.toList());
}
주석을 통해 각각의 메서드들이 어떻게 동작하는지 작성해주었다.