저의 개발 경험 상 Controller를 생성해보아야 Service에 요구할 기능들이 맞춰지기 때문에 Controller부터 만들어보도록 하겠습니다.
먼저 애완견을 등록하는 로직부터 만들어 보겠습니다.
@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를 만들겠습니다.
@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값인 PetDogDTO는 PetDog내용을 클라이언트로 보낼 때 필요없는 정보인 회원정보(Member)를 제외하고, Image를 DB에서 가져온 Image로 넣어 전달해줍니다.
@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에 저장할 수 있도록 로직을 설계해야 합니다.
@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에 저장할 때는 아래의 과정들이 필요합니다.
애완견 등록은 견주가 하기 때문에 Member를 조회한 후, 이를 이후에 PetOwner등록 시에 사용합니다.
-> 이는 JWT를 통해 전달받은 userId값을 이용하여 확인할 수 있습니다.
DB에 같은 애완견 등록번호가 존재하면 안됩니다.
-> DB조회를 통해 중복여부를 체크해줍니다.
PetDog엔티티를 생성합니다.
-> 매핑과, Breed 검색 후 주입하는 과정이 필요합니다.
프로필 이미지도 보냈다면, 이를 처리 및 저장한 후, PetDog에 주입해주어야 합니다.
-> savePetImage
에서 처리합니다.
PetOwner 생성 후 저장합니다.
3번의 견종을 호출하기 위해서는 BreedService에 접근해야 합니다.
@Service
@RequiredArgsConstructor
@Log4j2
public class BreedService {
private final BreedRepository breedRepository;
Optional<Breed> getBreed(Long id) {
return breedRepository.findById(id);
}
}
Service에서 사용할 Mapping 메서드를 생성하겠습니다.
@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를 만들어 어떤 로직들이 필요한지 확인해보겠습니다.
@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 토큰 없이도 받을 수 있도록 필터 제외 처리를 하겠습니다.
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로 전송해주어야 합니다.
이 기능을 포함한 전체코드입니다.
@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
매핑을 하는 메서드를 만들어야 합니다.
@Data
@AllArgsConstructor
@Builder
public class BreedDTO {
private long id;
private String breedName;
}
@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를 생성하겠습니다.
@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 타입으로 보내, 중복된 정보를 보내지 않도록 하는 것입니다.
@Data
@Builder
@AllArgsConstructor
public class PetListResponse {
private List<PetDogDTO> petList;
}
@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