애완견 등록하기(2) - Service, Controller, DTO 생성

Jongwon·2023년 3월 20일
1

DMS

목록 보기
14/18

저의 개발 경험 상 Controller를 생성해보아야 Service에 요구할 기능들이 맞춰지기 때문에 Controller부터 만들어보도록 하겠습니다.


펫 등록 로직

먼저 애완견을 등록하는 로직부터 만들어 보겠습니다.

✅PetController - 1

@Tag(name = "애완견 관련", description = "애완견 관련 API")
@RestController
@RequestMapping("/pet")
@RequiredArgsConstructor
public class PetController {

    private final BreedService breedService;
    private final PetService petService;

    @PostMapping("/dog/register")
    public PetDogDTO registerPetDog(@ModelAttribute PetDogRegisterDTO petDogRegisterDTO) {

        return petService.registerPet(petDogRegisterDTO);
    }

}

'http://localhost:8080/pet/dog/register' 에 저장할 애완견 정보를 보내면, 이를 DB에 저장하게 합니다.

Client에서 받아야하는 펫 정보를 PetDog를 보고 파악하여, PetDogRegisterDTO를 만들겠습니다.

✅PetDogRegisterDTO

@Data
@Builder
@AllArgsConstructor
public class PetDogRegisterDTO {

    private Long petId;
    private String name;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
    private LocalDate birth;
    private int gender;
    private long breedId;
    private double weight;
    private MultipartFile petDogImage;
}
  • PetDog를 만들 때 (PetId, Member)쌍을 키로 정했었습니다. @MapsId로 생성하였기 때문에 키값 자동 생성은 불가합니다.
    따라서 애완견을 등록하고자 할 때만 DTO를 통해 메서드를 호출하여 Entity로 매핑하도록 한다면 안전하게 PetId를 만들 수 있다고 생각하였습니다.

  • 견종(Breed)는 클라이언트가 선택할 수 있도록 BreedList를 전달해줄 예정입니다. 선택한 견종의 Breed만 Id만 서버로 전송하여 서버가 직접 ID를 통해 DB에서 검색한다면, 통신에 드는 오버헤드를 줄일 수 있습니다.

PetId값은 "생성일+애완견명"입니다.


Return값인 PetDogDTOPetDog내용을 클라이언트로 보낼 때 필요없는 정보인 회원정보(Member)를 제외하고, Image를 DB에서 가져온 Image로 넣어 전달해줍니다.

✅PetDogDTO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PetDogDTO {

    private Long petId;
    private String name;
    private LocalDate birth;
    private int gender;
    private Breed breed;
    private double weight;
    private int obesity;
    private int calorieGoal;
    private PetImageDTO profileImage;
}


Service계층에서는 펫을 Repository를 통해 DB에 저장할 수 있도록 로직을 설계해야 합니다.

✅PetService - 1

@Service
@RequiredArgsConstructor
@Log4j2
public class PetService {

    private final PetRepository petRepository;
    private final PetOwnRepository petOwnRepository;
    private final FileService fileService;
    private final PetImageRepository imageRepository;
    private final BreedService breedService;
    private final MemberRepository memberRepository;

    @Transactional
    public PetDogDTO registerPet(PetDogRegisterDTO petDogRegisterDTO) {
        String userId = SecurityUtil.getCurrentUsername();
        Member member = memberRepository.findByUserId(userId).get();
        if(petRepository.existsById(petDogRegisterDTO.getPetId())) {
            throw new RuntimeException("이미 등록된 애완견입니다.");
        }

        PetDog petDog = PetDogMapper.INSTANCE.registerDTOToPetDog(petDogRegisterDTO);
        Breed breed = breedService.getBreed(petDogRegisterDTO.getBreedId()).orElseThrow(() -> new RuntimeException("잘못된 매개변수입니다: Breed ID"));
        petDog.setBreed(breed);
        petRepository.save(petDog);

        if(petDogRegisterDTO.getPetDogImage() != null) {
            PetImage petImage = savePetImage(petDogRegisterDTO.getPetDogImage(), petDog);
            petDog.setProfileImage(petImage);
            petRepository.save(petDog);
        }

        PetOwner petOwner = PetOwner.builder()
                .isOwner(true)
                .member(member)
                .petDog(petDog)
                .expireDateTime(null)
                .build();

        petOwnRepository.save(petOwner);

        return PetDogMapper.INSTANCE.petDogToPetDogDTO(petDog);
    }

    @Transactional
    private PetImage savePetImage(MultipartFile file, PetDog petDog) {
        String originalName = file.getOriginalFilename();
        Path root = Paths.get(uploadPath, "petDog");

        try {
            ImageDTO imageDTO =  fileService.createImageDTO(originalName, root);
            PetImage petImage = PetImage.builder()
                    .uuid(imageDTO.getUuid())
                    .fileName(imageDTO.getFileName())
                    .fileUrl(imageDTO.getFileUrl())
                    .petDog(petDog)
                    .build();

            file.transferTo(Paths.get(imageDTO.getFileUrl()));

            return imageRepository.save(petImage);
        } catch (IOException e) {
            log.warn("업로드 폴더 생성 실패: " + e.getMessage());
        }

        return null;
    }
}

펫 정보를 DB에 저장할 때는 아래의 과정들이 필요합니다.

  1. 애완견 등록은 견주가 하기 때문에 Member를 조회한 후, 이를 이후에 PetOwner등록 시에 사용합니다.
    -> 이는 JWT를 통해 전달받은 userId값을 이용하여 확인할 수 있습니다.

  2. DB에 같은 애완견 등록번호가 존재하면 안됩니다.
    -> DB조회를 통해 중복여부를 체크해줍니다.

  3. PetDog엔티티를 생성합니다.
    -> 매핑과, Breed 검색 후 주입하는 과정이 필요합니다.

  4. 프로필 이미지도 보냈다면, 이를 처리 및 저장한 후, PetDog에 주입해주어야 합니다.
    -> savePetImage에서 처리합니다.

  5. PetOwner 생성 후 저장합니다.


3번의 견종을 호출하기 위해서는 BreedService에 접근해야 합니다.

✅BreedService - 1

@Service
@RequiredArgsConstructor
@Log4j2
public class BreedService {

    private final BreedRepository breedRepository;

 	Optional<Breed> getBreed(Long id) {
        return breedRepository.findById(id);
    }
}    

Service에서 사용할 Mapping 메서드를 생성하겠습니다.

✅PetDogMapper - 1

@Mapper(componentModel = "spring")
public interface PetDogMapper {

    PetDogMapper INSTANCE = Mappers.getMapper(PetDogMapper.class);

    PetDogDTO petDogToPetDogDTO(PetDog petDog);

    @Mapping(target = "breed", ignore = true)
    PetDog registerDTOToPetDog(PetDogRegisterDTO petDogRegisterDTO);
}



견종 호출 로직

애완견 등록 시 견종 정보를 선택할 때, 서버에서 견종 데이터를 받아와 클라이언트에게 제공할 수 있도록 합니다.

Controller를 만들어 어떤 로직들이 필요한지 확인해보겠습니다.

✅PetController - 2

@Tag(name = "애완견 관련", description = "애완견 관련 API")
@RestController
@RequestMapping("/pet")
@RequiredArgsConstructor
public class PetController {

    private final BreedService breedService;
    private final PetService petService;

    @GetMapping("/getBreedList")
    public List<BreedDTO> getBreedList() {
        List<BreedDTO> breedList = breedService.getBreedList();


        return breedList;
    }
    
    ...생략

견종 리스트는 JWT 토큰 없이도 받을 수 있도록 필터 제외 처리를 하겠습니다.

✅SecurityConfig

public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    private static final String[] URL_TO_PERMIT = {
            "/member/login",
            "/member/signup",
            "/v3/api-docs/**",
            "/swagger-ui/**",
            "/oauth2/**",
            "/api/**",
            "/pet/getBreedList"    //추가
    };
    ...생략

Service 계층에서는 BreedList를 DB에서 가져와 이를 Controller로 전송해주어야 합니다.

이 기능을 포함한 전체코드입니다.

✅BreedService - 2(완)

@Service
@RequiredArgsConstructor
@Log4j2
public class BreedService {

    private final BreedRepository breedRepository;

    public List<BreedDTO> getBreedList() {
        List<Breed> breedList = breedRepository.findAll();

        return breedList.stream().map(breed -> PetDogMapper.INSTANCE.breedToBreedDTO(breed)).collect(Collectors.toList());
    }

    public Optional<Breed> getBreed(Long id) {
        return breedRepository.findById(id);
    }
}

List로 DTO를 전달하기 때문에 DTO를 생성하고, Entity -> DTO매핑을 하는 메서드를 만들어야 합니다.

✅BreedDTO

@Data
@AllArgsConstructor
@Builder
public class BreedDTO {

    private long id;
    private String breedName;
}

✅PetDogMapper - 2(완)

@Mapper(componentModel = "spring")
public interface PetDogMapper {

    PetDogMapper INSTANCE = Mappers.getMapper(PetDogMapper.class);

    BreedDTO breedToBreedDTO(Breed breed);

    PetDogDTO petDogToPetDogDTO(PetDog petDog);

    @Mapping(target = "breed", ignore = true)
    PetDog registerDTOToPetDog(PetDogRegisterDTO petDogRegisterDTO);

    @Mapping(target = "petId", source = "petImage.petDog.petId")
    PetImageDTO petImageToPetImageDTO(PetImage petImage);
}


마지막으로 회원이 로그인했을 때, 본인이 가진 모든 애완견 정보를 가져올 수 있도록 API를 생성하겠습니다.

✅PetController - 3(완)

@Tag(name = "애완견 관련", description = "애완견 관련 API")
@RestController
@RequestMapping("/pet")
@RequiredArgsConstructor
public class PetController {

    private final BreedService breedService;
    private final PetService petService;


    @GetMapping("/getPetList")
    public PetListResponse getPetList() {
        String userId = SecurityUtil.getCurrentUsername();

        return PetListResponse.builder().petList(petService.loadMemberPets(userId)).build();
    }

    @GetMapping("/getBreedList")
    public List<BreedDTO> getBreedList() {
        List<BreedDTO> breedList = breedService.getBreedList();


        return breedList;
    }

    @PostMapping("/dog/register")
    public PetDogDTO registerPetDog(@ModelAttribute PetDogRegisterDTO petDogRegisterDTO) {

        return petService.registerPet(petDogRegisterDTO);
    }
}

PetListResponse는 단순히 PetList를 반환합니다. 핵심은, 사진을 PetImageDTO 타입으로 보내, 중복된 정보를 보내지 않도록 하는 것입니다.

✅PetListResponse

@Data
@Builder
@AllArgsConstructor
public class PetListResponse {

    private List<PetDogDTO> petList;
}

✅PetService - 3(완)

@Service
@RequiredArgsConstructor
@Log4j2
public class PetService {

    private final PetRepository petRepository;
    private final PetOwnRepository petOwnRepository;
    private final FileService fileService;
    private final PetImageRepository imageRepository;
    private final BreedService breedService;
    private final MemberRepository memberRepository;

    @Value("${spring.servlet.multipart.location}")
    private String uploadPath;

    public List<PetDogDTO> loadMemberPets(String userId) {
        List<PetDog> petDogs = petOwnRepository.findAllByMember(userId);

        return petDogs.stream()
                .map(petDog -> PetDogMapper.INSTANCE.petDogToPetDogDTO(petDog))
                .collect(Collectors.toList());
    }

    //여기에 주인인지 여부 체크해야함
    public List<PetImageDTO> loadPetImages(String petId) {
        return imageRepository.findAllPetImages(petId).stream()
                .map(petImage -> PetDogMapper.INSTANCE.petImageToPetImageDTO(petImage))
                .collect(Collectors.toList());
    }


    @Transactional
    public PetDogDTO registerPet(PetDogRegisterDTO petDogRegisterDTO) {
        String userId = SecurityUtil.getCurrentUsername();
        Member member = memberRepository.findByUserId(userId).get();
        if(petRepository.existsById(petDogRegisterDTO.getPetId())) {
            throw new RuntimeException("이미 등록된 애완견입니다.");
        }

        PetDog petDog = PetDogMapper.INSTANCE.registerDTOToPetDog(petDogRegisterDTO);
        Breed breed = breedService.getBreed(petDogRegisterDTO.getBreedId()).orElseThrow(() -> new RuntimeException("잘못된 매개변수입니다: Breed ID"));
        petDog.setBreed(breed);
        petRepository.save(petDog);

        if(petDogRegisterDTO.getPetDogImage() != null) {
            PetImage petImage = savePetImage(petDogRegisterDTO.getPetDogImage(), petDog);
            petDog.setProfileImage(petImage);
            petRepository.save(petDog);
        }

        PetOwner petOwner = PetOwner.builder()
                .isOwner(true)
                .member(member)
                .petDog(petDog)
                .expireDateTime(null)
                .build();

        petOwnRepository.save(petOwner);

        return PetDogMapper.INSTANCE.petDogToPetDogDTO(petDog);
    }

    @Transactional
    private PetImage savePetImage(MultipartFile file, PetDog petDog) {
        String originalName = file.getOriginalFilename();
        Path root = Paths.get(uploadPath, "petDog");

        try {
            ImageDTO imageDTO =  fileService.createImageDTO(originalName, root);
            PetImage petImage = PetImage.builder()
                    .uuid(imageDTO.getUuid())
                    .fileName(imageDTO.getFileName())
                    .fileUrl(imageDTO.getFileUrl())
                    .petDog(petDog)
                    .build();

            file.transferTo(Paths.get(imageDTO.getFileUrl()));

            return imageRepository.save(petImage);
        } catch (IOException e) {
            log.warn("업로드 폴더 생성 실패: " + e.getMessage());
        }

        return null;
    }
}




참고자료
https://charlie-choi.tistory.com/246
https://recordsoflife.tistory.com/501
https://devhan.tistory.com/200

profile
Backend Engineer

0개의 댓글