현재 설계한 앱의 방향은 회원가입 및 로그인을 진행한 후, 등록된 애완견이 없다면 애완견 등록 페이지로 자동으로 넘어가도록 하고 있습니다.
따라서 이번 글에서는 애완견 등록 기능을 만들어보도록 하겠습니다.
애완견에 필요한 Entity를 ERD로 그려보았습니다. 애완견은 별도의 ID를 가진 엔티티인데, 애완견과 회원을 M:N관계로 매핑한 후, 둘을 기본키로 가지는 별도의 테이블을 생성하여 연관관계를 설정하려고 합니다.
이렇게 하는 이유는, 애완견을 임시로 맡기거나 할 때, 데이터를 바로 전달할 수 있도록 처리하기 위해서입니다.
추가적으로 견종은 별도로 DB에 저장하고, 클라이언트가 선택한 애완견의 견종 ID를 DTO로 담을 예정이기 때문에 별도의 엔티티로 생성하였습니다.
비만도와 목표 활동량은 추후에 개발할 예정입니다.
애완견 등록에 대한 세부 로직은 다음과 같습니다.
회원이 애완견 등록 Form에 필요한 정보들을 입력합니다.
견종 선택 버튼을 눌러 자신의 견종을 찾습니다. 이는 클라이언트 사이드에서 서버로 견종 List API를 호출하여 견종 엔티티 List를 전해주고, 회원이 선택하면 해당 엔티티에 해당하는 ID를 파악합니다.
애완견 Form + 견종 ID를 DTO에 담아 서버로 보냅니다.
서버에 접근 시에는 Access Token을 헤더에 담아 접근하기 때문에 회원 ID를 토큰에서 꺼낼 수 있습니다.
애완견을 DB에 저장합니다.
회원 ID를 통해 Member객체를 가져오고, 애완견 ID와 함께 PetOwner엔티티에 매핑합니다.
쉽게 접근하기 위해 견종(Breed)관련 엔티티부터 생성하겠습니다.
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Breed {
@Id
private Long id;
private String breedName;
@Builder
public Breed(String breedName) {
this.breedName = breedName;
}
}
애완견 엔티티도 생성해줍니다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@AllArgsConstructor
@ToString(exclude = {"profileImage"})
public class PetDog {
//애완견 등록번호로
@Id
private Long petId;
private String name;
private LocalDate birth;
private int gender;
@OneToOne
private Breed breed;
private double weight;
private int obesity;
private int calorieGoal;
@OneToOne
private PetImage profileImage;
@Builder
public PetDog(Long petId, String name, LocalDate birth, int gender, Breed breed, double weight, int calorieGoal, PetImage profileImage) {
this.petId = petId;
this.name = name;
this.birth = birth;
this.gender = gender;
this.breed = breed;
this.weight = weight;
this.calorieGoal = calorieGoal;
this.profileImage = profileImage;
}
public void setObesity(int rate) {
this.obesity = obesity;
}
public void setBreed(Breed breed){
this.breed = breed;
}
public void setProfileImage(PetImage image) {
this.profileImage = image;
}
}
@OneToOne
매핑을 합니다.@Builder
를 적용하였는데, 등록 시에는 비만도 파악이 불가하므로 obesity는 생성자에서 제외하였습니다.PetOwner 엔티티는 회원:애완견의 M:N관계로 인한 결과물이기 때문에 식별관계로 매핑해주어야 합니다. 이 엔티티는 기본키(PK)값이 (애완견ID, 회원ID)쌍입니다.
JPA에서 식별관계의 ID값을 지정하는 방식은 크게 2가지가 있습니다.
@IdClass
: RDB에 가까운 방식@EmbeddedId
: 객체지향에 가까운 방식둘의 차이점은 크게 없지만, 별도로 만들 Id 인스턴스 자체에 의미를 크게 가져가지 않을 것이기 때문에 @IdClass
로 사용하겠습니다.
🔥식별관계 vs 비식별관계 & @IdClass vs @EmbeddedId
https://www.nowwatersblog.com/jpa/ch7/7-3
위의 링크에서 사용법과 더불어 자세히 설명해주고 있으므로 참고하시길 바랍니다.
다시 프로젝트로 넘어와서, @IdClass
를 사용하려면 별도의 오브젝트를 생성해야합니다. 또한 이 오브젝트는 직렬화할 수 있도록 만들어야합니다.
@EqualsAndHashCode
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PetOwnerId implements Serializable {
private Member member;
private PetDog petDog;
}
다음으로 PetOwnerId를 기본키로 하는 PetOwner엔티티를 생성합니다.
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"member", "petDog"})
@IdClass(PetOwnerId.class)
public class PetOwner {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@NonNull
private Member member;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@NonNull
private PetDog petDog;
private LocalDateTime expireDateTime;
private boolean isOwner;
}
애완견 사진은 인스타그램처럼 여러 사진을 등록할 수 있도록 할 예정입니다. 일대다 연관관계이므로 @ManyToOne
매핑도 진행하겠습니다.
1:N 연관관계설정은 1에 초점을 맞출수도, N에 초점을 맞출수도 있습니다.
NoSQL은 단일값 애트리뷰트일 필요가 없으므로 어느쪽에 초점을 맞춰도 상관없지만, RDB를 사용하는 우리 프로젝트에서는 신중해야합니다.
만약 1에 해당하는 PetDog에@OneToMany
설정을 한다면, 서버에서 이미지를 한번에 가져오긴 편하겠지만, DB에 입장에서 보게되면 다릅니다.
RDB인 MySQL은 단일값 애트리뷰트만 허용하므로 별도로 (PetDog의 ID, 이미지ID)를 PK로 가지는 테이블을 생성할 것입니다. 그리고, 특정 애완견 A에 해당하는 사진들을 호출할 때 역시 이 테이블에서 전체검색을 하기 때문에 성능은 똑같더라도, DB 용량을 더 차지하게됩니다.
반대로 N에 해당하는 이미지에 초점을 맞추면 이미지는 외래키로 PetDog의 ID를 가지고있고, PetImage에서만 애완견 A에 해당하는 이미지를 검색하면 되므로, 용량을 훨씬 적게 가져갈 수 있습니다.
애완견 사진 Entity도 생성합니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
@ToString
public class PetImage extends Image {
@ManyToOne(fetch = FetchType.LAZY)
private PetDog petDog;
}
PetImage 자체는 DB에 저장하지만, 이 자체를 이용하여 다른 DTO에 지정해주면 PetImage 아래에 있는 Member와 Pet정보가 중복되어 전송되기 때문에 이를 방지하기 위해 PetImageDTO를 만들어 두겠습니다.
@Getter
@NoArgsConstructor
public class PetImageDTO {
private Long id;
private String uuid;
private String fileName;
private String fileUrl;
private String petId;
@Builder
public PetImageDTO(Long id, String uuid, String fileName, String fileUrl, String petId) {
this.id = id;
this.uuid = uuid;
this.fileName = fileName;
this.fileUrl = fileUrl;
this.petId = petId;
}
}
견종과 관련한 Repository를 생성하겠습니다.
@Repository
public interface BreedRepository extends JpaRepository<Breed, Long>, PagingAndSortingRepository<Breed, Long> {
List<Breed> findAll();
Optional<Breed> findById(Long breedId);
}
다음으로는 PetImage관련 Repository입니다.
@Repository
public interface PetImageRepository extends ImageRepository {
@Query("select pi from PetImage pi where pi.petDog.id = :petId")
List<PetImage> findAllPetImages(@Param("petId") String petId);
@Query("select pi from PetImage pi where id = :imageId")
PetImage findByImageId(Long imageId);
}
1:N 연관관계로 이미지에 애완견을 매칭했기 때문에, 애완견 하나와 연관된 모든 이미지를 호출하는 메서드를 생성해두었습니다.
다음으로는 PetRepository입니다.
@Repository
public interface PetRepository extends JpaRepository<PetDog, Long> { }
PetOwnerRepository에서 로그인한 회원이 가진 애완견을 모두 호출할 수 있도록 합니다.
public interface PetOwnRepository extends JpaRepository<PetOwner, Long> {
//join fetch 이용하여 petDog fetch
@Query("select distinct p"
+ " from PetOwner po"
+ " inner join PetDog p"
+ " inner join Member m"
+ " on m.userId = :userId")
List<PetDog> findAllByMember(@Param("userId") String userId);
}
견주를 이용하여 엔티티를 호출할 수 있도록 findAllByMember
메서드를 생성하였습니다.
https://yoonbing9.tistory.com/38
https://ojt90902.tistory.com/717
https://deveric.tistory.com/108