[Study-2주차] PetClinicProject 초기 설정 + 클래스 생성 + 코드 설명
[Study-2주차] PetClinicProject 초기 설정 + 클래스 생성 + 코드 설명
[Study-1주차] PetClinicProject 기능 명세 + 개념 정리 + 2주차 과제 [Study-1주차] PetClinicProject 기능 명세 + 개념 정리 + 2주차 과제 [Study-0주차] Study 진행 방식 [Study-0주차] Study 진행 방식 https://github.com/sprin
soohykeee.tistory.com
본격적으로 CRUD 개발에 앞서, 우선적으로 수정사항이 생겼다.
기존에 우리가 설계했던 pet-clinic project에서 vets(수의사)와 specialties(학위) 테이블에서 학위 클래스를 enum 클래스로 생성해서 사용하려 했지만, 학위테이블을 생성해주고 학위와 수의사를 다대다로 연결시켜주기로 했다.
다대다이기에 JPA때 공부했던 방식을 적용해서 연관 테이블인 vets_specialties 을 생성해준 후 해당 클래스와 일대다로 연결시켜 주기로 했다. 코드를 간략하게 푼다면 다음과 같다.
수의사 테이블{
@OneToMany(mappedby = "연관_ID")
List<연관_Table> table = new ArrayList<>();
}
학위 테이블{
@OneToMany(mappedby = "연관_ID")
List<연관_Table> table = new ArrayList<>();
}
연관 테이블{
//다 쪽이 주인
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "...")
수의사_Table table
//다 쪽이 주인
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "...")
학위_Table table
}
또한 Controller에서의 URI 정책은 다음과 같이 설정해주기로 약속했다.
GET | /api/v1/owners |
POST | /api/v1/owners |
PUT | /api/v1/owners |
DELETE | /api/v1/owners |
추가적으로 기본적인 CRUD를 해줄것이기에 예외처리는 해주지 않고, 기본적인 CRUD기능 로직만 구현하기로 했다.
본격적으로 CRUD 개발에 앞서 스터디 그룹원과의 소통을 통해 들은 몇가지 팁들이 있었다.
CRUD 개발의 시작은 기능을 만들었다고 가정하고 Controller부터 순차적으로 작성해주는 것이 낫다고 배웠다.
또한 패키지 구조에서 Dto의 경우는 persistence와 같은 영속적으로 존재하는 것이 아니기에 service의 model 디렉토리를 따로 생성해준 후, 거기서 Dto 클래스들을 관리해주는 것이 좋다고 배웠다.
추가적으로, 아래의 CRUD 코드들을 보게되면 Dto를 entity당 RequestDto, ResponseDto를 구분하여 생성해준 후, 거기서 static으로 CREATE, UPDATE, READ 등등과 같은 클래스를 선언해준 후 사용을 했다. 물론 실제 실무에서나 규모가 큰 프로젝트에서 정적으로 클래스를 생성해주면 메모리 낭비를 초래할 수 있지만, 현재 프로젝트에는 미미할 정도의 영향을 줄 뿐만 아니라, 가독성이 높은 코드를 위해 이처럼 작성을 해주었다.
Owners
OwnersController
package kr.co.jshpetclinicstudy.controller;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/owners")
public class OwnersController {
private final OwnersService ownersService;
public void createOwner(@RequestBody @Valid OwnersRequestDto.CREATE create){
ownersService.createOwner(create);
}
public List<OwnersResponseDto.READ> getOwnerList() {
return ownersService.getOwnerList();
}
public OwnersResponseDto.DETAIL_READ getOwner(Long id) {
OwnersResponseDto.DETAIL_READ detailRead = ownersService.getOwner(id);
return detailRead;
}
public void updateOwner(@RequestBody @Valid OwnersRequestDto.UPDATE update) {
ownersService.updateOwner(update);
}
public void deleteOwner(Long id) {
ownersService.deleteOwner(id);
}
}
@RestController
- 해당 어노테이션은 @Controller + @ResponseBody가 결합된 것으로, 해당 어노테이션을 붙이게 되면 해당 하위 메서드에 @ResponseBody 어노테이션을 붙이지 않아도 문자열과 JSON등을 전송할 수 있다. 즉, 주용도는 JSON 형태로 객체 데이터를 반환하기에 사용해준다.
@Valid
- valid는 유효성을 검사하기 위한 어노테이션으로, 객체 안에서 들어오는 값에 대한 검증이 가능하다. 다른 클래스들에서 사용해준 @NotNull, @NotBlank, @NotEmpty, @Positive, @AssertTrue, @AssertFalse, @Size 등 과 같은 어노테이션들이 true로 되는지 확인해주는 것이다.
- 해당 어노테이션을 사용해주기 위해서는 gradle 파일에 validation 의존성을 추가해줘야 한다.
- 그렇다면 @Valid와 @Validated의 차이는 무엇일까?
- @Valid는 JSR-330 자바 표준 스펙이고, @Validated는 자바 표준 스펙이 아닌 스프링 프레임워크가 제공하는 기능이다.
- @Valid는 controller 메소드의 유효성 검증만 가능하고, @Validated는 AOP를 기반으로 스프링 빈의 유효성 검증을 위해 사용되며, 클래스에서는 @Validated를 메소드에서는 @Valid를 붙여줘야 한다.
- @Validated는 내부에 groups 기능을 포함하고 있다.
-> 즉, @Validated를 사용하는 경우는 groups 기능이 필요하면 사용하는 것이 좋고, 그렇지 않은 경우에는 둘 중에 더욱 선호하는 것으로 사용해도 된다. 뭐가 낫다라고 명확하게 정해진것이 없기에 프로젝트를 진행하는 팀에서 합의를 통해 결정하는 것을 추천한다. (by. 김영한)
앞서 말했듯 OwnersRequestDto, OwnserResponseDto를 나누어서 요청에는 CREATE, UPDATE를 응답에는 READ, DETAIL_READ를 넣어줄 것이다.
메소드명을 읽으면 어떠한 동작을 하는지 알 수 있듯, owner를 생성해주는 createOwner, 모든 owner를 조회하는 getOwnerList, 특정 owner를 조회하는 getOwner, owner의 정보를 수정해주는 updateOwner, owner를 삭제해주는 deleteOwner가 있다.
또한 우리는 프론트단을 구현하지 않을 것이기에 @RestController를 사용하여 JSON 형태로 데이터를 반환받아줄 것이고, owner를 생성하거나 수정할 때는 @Valid 어노테이션을 통해 유효성을 검증해줄 것이다. 또한 @Valid와 @Validated 중에 무엇을 사용할지 고민했지만, 딱히 groups를 통해 구분지어 유효성 검사해주지 않을것 같았기에 @Valid를 사용해주었다.
Owners DTO
package kr.co.jshpetclinicstudy.service.model;
public class OwnersRequestDto {
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class CREATE {
private String firstName;
private String lastName;
private String address;
private String city;
private String telephone;
}
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class UPDATE {
private Long ownerId;
private String firstName;
private String lastName;
private String address;
private String city;
private String telephone;
}
}
package kr.co.jshpetclinicstudy.service.model;
public class OwnersResponseDto {
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class READ {
private Long ownerId;
private String firstName;
private String lastName;
private String address;
private String city;
private String telephone;
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class DETAIL_READ {
private Long ownerId;
private String firstName;
private String lastName;
private String address;
private String city;
private String telephone;
}
}
앞서 말했듯 RequestDto, ResponseDto를 나누어서 요청시에는 CREATE, UPDATE 응답시에는 READ, DETAIL_READ를 정적으로 생성해주었다.
CRATE시에는 id 값은 우리가 설정한 전략에 의해 DB에서 자동 생성해주기에 받아오는 값으로 넣어주지 않았고, firstName, lastName, address, city, telephone을 넣어주었다.
UPDATE시에는 id 값을 통해 값을 알아야하기에 CREATE와 달리 id 값을 넣어주었다.
READ와 DETAIL_READ도 동일하다.
Owners Entity
package kr.co.jshpetclinicstudy.persistence.entity;
@Entity
@AttributeOverride(name = "id", column = @Column(name = "owner_id", length = 4))
@Getter
@NoArgsConstructor
@Table(name="tbl_owners")
public class Owners extends BaseEntity {
// unique 추가
@Column(name = "telephone", length = 20, unique = true)
private String telephone;
// ... 생략
public static Owners dtoToEntity(OwnersRequestDto.CREATE create){
return Owners.builder()
.firstName(create.getFirstName())
.lastName(create.getLastName())
.address(create.getAddress())
.city(create.getCity())
.telephone(create.getTelephone())
.build();
}
public static OwnersResponseDto.READ entityToDto(Owners owners){
return OwnersResponseDto.READ.builder()
.ownerId(owners.getId())
.firstName(owners.getFirstName())
.lastName(owners.getLastName())
.address(owners.getAddress())
.city(owners.getCity())
.telephone(owners.getTelephone())
.build();
}
public static OwnersResponseDto.DETAIL_READ entityToDetailDto(Owners owners){
return OwnersResponseDto.DETAIL_READ.builder()
.ownerId(owners.getId())
.firstName(owners.getFirstName())
.lastName(owners.getLastName())
.address(owners.getAddress())
.city(owners.getCity())
.telephone(owners.getTelephone())
.build();
}
public void changeOwnerAddress(String changeAddress) {
this.address = changeAddress;
}
public void changeOwnerCity(String changeCity) {
this.city = changeCity;
}
public void changeOwnerFirstName(String changeFirstName) {
this.firstName = changeFirstName;
}
public void changeOwnerLastName(String changeLastName) {
this.lastName = changeLastName;
}
public void changeOwnerTelephone(String changeOwnerTelephone) {
this.telephone = changeOwnerTelephone;
}
}
요청과 응답시에 entity, dto로 변환해주어 넘겨주어야 하기에, entity클래스에서 builder를 사용하여 값을 변환하도록 해주었다. 또한 추가적으로 change~() 메서드를 작성해주어서, update시에 수정될 수 있는 정보들을 작성해주었다.
OwnersRepository
package kr.co.jshpetclinicstudy.persistence.repository;
@Repository
public interface OwnersRepository extends JpaRepository<Owners, Long> {
@Query(value = "select o.owner_id, o.address, o.city, o.first_name, o.last_name, o.telephone " +
"from tbl_owners o " +
"where o.telephone=:telephone", nativeQuery = true)
Optional<Owners> findOwnerByTelephone(String telephone);
}
앞서 Owners Entity에서 telephone을 unique 속성으로 선언해주었다. owner를 id 뿐아니라, 구분하여 찾을 수 있는 수단으로 사용해주기 위해서 unique하게 해주었다.
findOwnerByTelephone, 핸드폰 번호를 사용하여 owner 정보를 끌어오기 위해서 JPQL query 문을 사용하여 값을 가져올 수 있도록 해주었다.
OwnersService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class OwnersService {
private final OwnersRepository ownersRepository;
public void createOwner(OwnersRequestDto.CREATE create) {
final Owners owners = Owners.dtoToEntity(create);
ownersRepository.save(owners);
}
public List<OwnersResponseDto.READ> getOwnerList() {
return ownersRepository.findAll().stream()
.map(Owners::entityToDto).collect(Collectors.toList());
}
public OwnersResponseDto.DETAIL_READ getOwner(Long id) {
return Owners.entityToDetailDto(ownersRepository.findById(id).get());
}
public void updateOwner(OwnersRequestDto.UPDATE update) {
final Optional<Owners> owners = ownersRepository.findById(update.getOwnerId());
isOwners(owners);
owners.get().changeOwnerCity(update.getCity());
owners.get().changeOwnerAddress(update.getAddress());
owners.get().changeOwnerTelephone(update.getTelephone());
owners.get().changeOwnerFirstName(update.getFirstName());
owners.get().changeOwnerLastName(update.getLastName());
ownersRepository.save(owners.get());
}
public void deleteOwner(Long id) {
final Optional<Owners> owners = ownersRepository.findById(id);
isOwners(owners);
ownersRepository.deleteById(id);
}
private void isOwners(Optional<Owners> owners) {
if (owners.isEmpty()) {
throw new RuntimeException("This Owner is Not Exist");
}
}
}
서비스 코드는 예외처리 없이, 간단한 구현만을 위한 코드로 작성해주었다. 코드를 보면 이해할 수 있기에 별도의 설명은 하지 않겠다.
OwnersService Test Code
package kr.co.jshpetclinicstudy.service;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class OwnersServiceTest {
@Autowired
private OwnersService ownersService;
@Autowired
private OwnersRepository ownersRepository;
@Test
void createOwner() {
OwnersRequestDto.CREATE create = OwnersRequestDto.CREATE.builder()
.firstName("수혁")
.lastName("장")
.address("도봉구 창동")
.city("서울시")
.telephone("01064564655")
.build();
ownersService.createOwner(create);
Optional<Owners> owners = ownersRepository.findOwnerByTelephone("01064564655");
assertThat(owners.get().getFirstName()).isEqualTo("수혁");
}
@Test
void getOwnerList() {
List<OwnersResponseDto.READ> readList = ownersService.getOwnerList();
assertThat(readList.get(0).getFirstName()).isEqualTo("수혁");
assertThat(ownersRepository.findAll().size()).isEqualTo(readList.size());
}
@Test
void getOwner() {
OwnersResponseDto.DETAIL_READ detailRead = ownersService.getOwner(2L);
assertThat(detailRead.getTelephone()).isEqualTo("01064564655");
}
@Test
void updateOwner() {
OwnersRequestDto.UPDATE update = OwnersRequestDto.UPDATE.builder()
.ownerId(2L)
.firstName("철수")
.lastName("김")
.address("성북구 정릉동")
.city("서울시")
.telephone("01064564655")
.build();
ownersService.updateOwner(update);
assertThat(ownersRepository.findById(2L).get().getFirstName()).isEqualTo("철수");
}
@Test
void deleteOwner() {
ownersService.deleteOwner(2L);
assertThat(ownersRepository.findById(2L)).isEmpty();
}
}
테스트 코드 또한 간단하기에 설명하지 않고 넘어가겠다.
Types
test 코드를 위해 Types enum class에 아래와 같이 추가해주었다.
package kr.co.jshpetclinicstudy.persistence.entity;
public enum Types {
푸들, 비숑, 시바, 포메라니안
}
Pets
pet의 경우도 위의 owners와 별다른 차이점은 없다.
PetsController
package kr.co.jshpetclinicstudy.controller;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/pets/")
public class PetsController {
private final PetsService petsService;
public void createPet(@RequestBody @Valid PetsRequestDto.CREATE create) {
petsService.createPet(create);
}
public List<PetsResponseDto.READ> getPetList() {
return petsService.getPetList();
}
public PetsResponseDto.DETAIL_READ getPet(Long id) {
PetsResponseDto.DETAIL_READ detailRead = petsService.getPet(id);
return detailRead;
}
public List<PetsResponseDto.READ> getPetListOfOwner(Long ownerId) {
return petsService.getPetListOfOwner(ownerId);
}
public void updatePet(@RequestBody @Valid PetsRequestDto.UPDATE update) {
petsService.updatePet(update);
}
public void deletePet(Long id) {
petsService.deletePet(id);
}
}
메소드명을 읽으면 어떠한 동작을 하는지 알 수 있듯, pet을 생성해주는 createPet, 모든 pet을 조회하는 getPetList, 특정 pet을 조회하는 getPet, owner가 소유하고 있는 pet의 정보들을 조회하는 getPetListOfOwner, pet의 정보를 수정해주는 updatePet, pet을 삭제해주는 deletePet이 있다.
Pets DTO
package kr.co.jshpetclinicstudy.service.model;
public class PetsRequestDto {
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class CREATE {
private String name;
private LocalDate birthDate;
private Owners owners;
private String type;
}
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class UPDATE {
private Long petId;
private String name;
private LocalDate birthDate;
private Owners owners;
private String type;
}
}
package kr.co.jshpetclinicstudy.service.model;
public class PetsResponseDto {
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class READ {
private Long petId;
private String name;
private LocalDate birthDate;
private String ownerFirstName;
private String ownerTelephone;
private String type;
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class DETAIL_READ {
private Long petId;
private String name;
private LocalDate birthDate;
private String ownerFirstName;
private String ownerTelephone;
private String type;
}
}
pets의 정보를 response, 응답으로 데이터를 보여줄 때, 해당 pet의 owner의 정보도 보여주면 좋을것같다고 생각하여 ownerFirstName, ownerTelephone을 String으로 받을 수 있게 해주었다.
Pets Entity
package kr.co.jshpetclinicstudy.persistence.entity;
@Entity
@AttributeOverride(name = "id", column = @Column(name = "pet_id", length = 4))
@NoArgsConstructor
@Getter
@Table(name="tbl_pets")
public class Pets extends BaseEntity{
// ... 생략
public static Pets dtoToEntity(PetsRequestDto.CREATE create) {
return Pets.builder()
.name(create.getName())
.birthDate(create.getBirthDate())
.owners(create.getOwners())
.types(Types.valueOf(create.getType()))
.build();
}
public static PetsResponseDto.READ entityToDto(Pets pets) {
return PetsResponseDto.READ.builder()
.petId(pets.getId())
.name(pets.getName())
.birthDate(pets.getBirthDate())
.ownerTelephone(pets.getOwners().getTelephone())
.ownerFirstName(pets.getOwners().getFirstName())
.type(pets.getTypes().toString())
.build();
}
public static PetsResponseDto.DETAIL_READ entityToDetailDto(Pets pets) {
return PetsResponseDto.DETAIL_READ.builder()
.petId(pets.getId())
.name(pets.getName())
.birthDate(pets.getBirthDate())
.ownerTelephone(pets.getOwners().getTelephone())
.ownerFirstName(pets.getOwners().getFirstName())
.type(pets.getTypes().toString())
.build();
}
public void changePetName(String changeName) {
this.name = changeName;
}
public void changePetBirtDate(LocalDate changeBirthDate) {
this.birthDate = changeBirthDate;
}
public void changePetType(Types changeTypes) {
this.types = changeTypes;
}
}
앞서 PetsResponseDto에서 READ, DETAIL_READ에서 추가해주었던 ownerFirstName, ownerTelephone에 값을 넣어주기 위해서 pets에서의 getOwners를 해준 후, 거기서 firstName, telephone 을 get메서드로 꺼내온 후 값을 넣어주었다.
PetsRepository
package kr.co.jshpetclinicstudy.persistence.repository;
@Repository
public interface PetsRepository extends JpaRepository<Pets, Long> {
@Query(value = "select p.pet_id, p.name, p.birth_date, p.pet_type, p.owner_id " +
"from tbl_pets p " +
"where p.owner_id=:ownerId", nativeQuery = true)
List<Pets> findPetListByOwnerId(Long ownerId);
}
앞서, controller에서 owner가 소유하고 있는 pet의 정보를 끌어오기 위해서 Repository에서 JPQL을 사용하여 ownerId를 통해 해당 pet의 정보들을 끌어오도록 해주었다. owner와 pet의 관계를 1:N으로 설계해주었기에 List<>로 받아주었다.
PetsService
package kr.co.jshpetclinicstudy.service;
@Service
@RequiredArgsConstructor
public class PetsService {
private final PetsRepository petsRepository;
public void createPet(PetsRequestDto.CREATE create) {
final Pets pets = Pets.dtoToEntity(create);
petsRepository.save(pets);
}
@Transactional
public List<PetsResponseDto.READ> getPetList() {
return petsRepository.findAll().stream()
.map(Pets::entityToDto).collect(Collectors.toList());
}
@Transactional
public PetsResponseDto.DETAIL_READ getPet(Long id) {
return Pets.entityToDetailDto(petsRepository.findById(id).get());
}
@Transactional
public List<PetsResponseDto.READ> getPetListOfOwner(Long ownerId) {
return petsRepository.findPetListByOwnerId(ownerId).stream()
.map(Pets::entityToDto).collect(Collectors.toList());
}
public void updatePet(PetsRequestDto.UPDATE update) {
final Optional<Pets> pets = petsRepository.findById(update.getPetId());
isPets(pets);
pets.get().changePetName(update.getName());
pets.get().changePetBirtDate(update.getBirthDate());
pets.get().changePetType(Types.valueOf(update.getType()));
petsRepository.save(pets.get());
}
public void deletePet(Long id) {
final Optional<Pets> pets = petsRepository.findById(id);
isPets(pets);
petsRepository.deleteById(id);
}
private void isPets(Optional<Pets> pets) {
if (pets.isEmpty()) {
throw new RuntimeException("This Pet is Not Exist");
}
}
}
PetsService Test Code
package kr.co.jshpetclinicstudy.service;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class PetsServiceTest {
@Autowired
private PetsService petsService;
@Autowired
private PetsRepository petsRepository;
@Autowired
private OwnersRepository ownersRepository;
@Test
void createPet() {
Optional<Owners> owners = ownersRepository.findOwnerByTelephone("01064564655");
PetsRequestDto.CREATE create = PetsRequestDto.CREATE.builder()
.name("멍멍이")
.birthDate(LocalDate.of(2021, 1, 9))
.owners(owners.get())
.type("푸들")
.build();
petsService.createPet(create);
Optional<Pets> pets = petsRepository.findById(1L);
assertThat(pets.get().getName()).isEqualTo("멍멍이");
}
@Test
void getPetList() {
List<PetsResponseDto.READ> readList = petsService.getPetList();
assertThat(readList.get(0).getName()).isEqualTo("멍멍이");
assertThat(petsRepository.findAll().size()).isEqualTo(readList.size());
}
@Test
void getPet() {
PetsResponseDto.DETAIL_READ detailRead = petsService.getPet(2L);
assertThat(detailRead.getName()).isEqualTo("뭉이");
assertThat(detailRead.getOwnerFirstName()).isEqualTo("수혁");
}
@Test
void getPetListOfOwner() {
List<PetsResponseDto.READ> petListOfOwner = petsService.getPetListOfOwner(1L);
assertThat(petListOfOwner.get(2).getName()).isEqualTo("초코");
assertThat(petListOfOwner.size()).isEqualTo(3);
}
@Test
void updatePet() {
PetsRequestDto.UPDATE update = PetsRequestDto.UPDATE.builder()
.petId(3L)
.name("딸기")
.birthDate(LocalDate.of(2011, 01, 01))
.type("포메라니안")
.build();
petsService.updatePet(update);
assertThat(petsRepository.findById(3L).get().getName()).isEqualTo("딸기");
}
@Test
void deletePet() {
petsService.deletePet(3L);
assertThat(petsRepository.findById(3L)).isEmpty();
}
}