Skip to content

[feat] 반려견 dbti 조회 API 및 데이터 구조 최적화 (Breed, DBTI 연관관계 개선)#221

Merged
daeun-han merged 20 commits intodevelopfrom
feat/#212
Jan 29, 2026
Merged

[feat] 반려견 dbti 조회 API 및 데이터 구조 최적화 (Breed, DBTI 연관관계 개선)#221
daeun-han merged 20 commits intodevelopfrom
feat/#212

Conversation

@daeun-han
Copy link
Copy Markdown
Member

@daeun-han daeun-han commented Jan 22, 2026

📌 PR 제목

[feat] 반려견 dbti 조회 API 및 데이터 구조 최적화 (Breed, DBTI 연관관계 개선)


✨ 요약 설명

반려견 DBTI 검사 결과를 조회하는 API를 구현하고, 이 과정에서 발생할 수 있는 성능 저하(N+1 문제)를 방지하기 위해 연관관계를 최적화했습니다. 또한, 최신 develop 브랜치 머지 과정에서 누락되거나 개선이 필요한 재검사 로직, DBTI 타입 반환 연동, 견종(Breed) 엔티티 연결 작업을 통합하여 수행했습니다.


🧾 변경 사항

  1. DBTI 조회 및 재검사 기능 추가
  • 반려견별 DBTI 결과 조회 API 구현
  • 기존 결과가 존재할 경우 새로운 검사 결과로 갱신하는 재검사(Upsert) 로직 추가
  1. 성능 최적화 및 연관관계 재설정
  • Pet - DbtiResult 간 1:1 양방향 연관관계 설정
  • @EntityGraph를 적용하여 펫 프로필 조회 시 DBTI 결과를 한 번의 JOIN으로 가져오도록 개선 (N+1 문제 해결)
  1. 견종(Breed) 관리 방식 변경
  • Pet 엔티티의 견종 정보를 String에서 BreedEntity 외래키 참조 방식으로 변경하여 데이터 정규화 및 무결성 강화
  • 관련 DTO 및 생성 로직 업데이트 (breed명 대신 breedId 사용)
  1. API 응답 스펙 보완
  • 반려견 프로필 조회 시 해당 반려견의 DBTI 타입(예: 'EPR')이 함께 반환되도록 연결

📂 PR 타입

  • 기능 추가
  • 버그 수정
  • 리팩토링
  • 문서 수정
  • 기타 (직접 작성: )

🧪 테스트

  • Postman으로 API 호출 확인
  • 로컬 실행 결과 화면 캡처 포함

✅ 체크리스트

  • 깃 & 코드 컨벤션을 지켰는가?
  • Swagger/문서화는 최신 상태인가?
  • 기능 변경 시 영향 받는 모듈을 확인했는가?

💬 리뷰어에게 전달할 말

  • 이번 작업은 DBTI 조회 기능 구현과 더불어, develop 브랜치 머지 과정에서 도메인 구조의 일관성을 맞추기 위해 Breed 엔티티 연동 및 DBTI 재검사 로직 고도화를 병행하였습니다. 특히 반려견 프로필 조회 시 DBTI 결과가 함께 반환되어야 하는 기획에 맞춰, 발생 가능한 N+1 문제를 방지하고자 Pet과 DbtiResult 간의 양방향 연관관계를 설정하고 @EntityGraph를 통한 Fetch Join 최적화를 적용했으니 이 부분을 중점적으로 검토 부탁드립니다.
  • 해당 PR과 반려견 정보 수정을 다루는 [feat] 유저 및 반려견 정보 수정 API 구현 #201 PR이 모두 머지된 직후, 수정 API에도 breedId를 반영하고 유저당 반려견을 한 마리로 제한하는 시스템적 장치를 추가할 예정이니 참고 부탁드립니다!

🔗 관련 이슈

아래 이슈번호 에 번호를 적으면 풀리퀘스트 머지 완료 시 자동으로 해당 이슈가 닫힙니다.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 반려견의 DBTI 검사 결과를 조회하는 새 API가 추가되어, 반려견 상세에서 검사 결과를 확인할 수 있습니다.
    • DBTI 제출 흐름이 사용자 컨텍스트를 반영하도록 개선되었습니다.
  • Improvements

    • 견종 선택 방식이 ID 기반으로 변경되어 등록/선택이 더 명확해졌습니다.
    • 내 반려견 조회가 단일 프로필 반환으로 단순화되었습니다.
  • Bug Fixes

    • DBTI 결과 조회/처리 관련 오류 응답 및 접근 권한 검증이 개선되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

반려견 DBTI 제출 흐름에 사용자 식별자(userId)를 도입하고, 반려견 DBTI 조회 GET /pets/dbti/{petId} 엔드포인트를 추가했으며, Pet/Breed/Dbti 엔티티와 관련 리포지토리·서비스·DTO를 관계 기반으로 리팩토링했습니다.

Changes

Cohort / File(s) 변경 요약
DBTI 컨트롤러
src/main/java/.../dbti/api/controller/DbtiController.java
GET /pets/dbti/{petId} 엔드포인트 추가; userId 컨텍스트 파라미터 적용 및 Swagger 주석 추가; 제출 경로에서 submit 호출에 userId 전달
DBTI 쿼리/페이사드
src/main/java/.../dbti/application/facade/query/DbtiQueryFacade.java, src/main/java/.../dbti/application/service/DbtiQueryService.java
getPetDbtiResult / getPetDbtiResultDetail 추가; Pet 및 DbtiResult 존재·소유권 검증; DbtiResultRepository·PetRepository 의존성 추가
DBTI 커맨드 서비스·페이사드
src/main/java/.../dbti/application/service/DbtiCommandService.java, src/main/java/.../dbti/application/facade/DbtiCommandFacade.java
calculateSaveAndGetDetail에 userId 파라미터 추가; 소유권 검증 로직 변경; DbtiResult 업서트 로직 도입; submitDbtiTest 시그니처 변경
DBTI 엔티티·리포지토리
src/main/java/.../dbti/infra/persistence/entity/DbtiResultEntity.java, src/main/java/.../dbti/infra/persistence/repository/DbtiResultRepositoryImpl.java, src/main/java/.../dbti/infra/persistence/repository/SpringDataDbtiResultRepository.java
petId:Longpet:PetEntity 일대일 관계로 변경; updateResult/setPet 메서드 추가; 리포지토리 findByPet_PetId로 쿼리 경로 변경; 에러코드 변경(DUPLICATE_DBTI_RESULT → DBTI_RESULT_NOT_FOUND)
Pet 엔티티 관련 DTO/커맨드
src/main/java/.../pet/api/dto/request/PetRequestDto.java, src/main/java/.../pet/application/dto/request/CreatePetCommand.java, src/main/java/.../pet/api/dto/response/PetProfileResponseDto.java
breed 필드 타입 String→Long(breedId) 변환 및 검증 어노테이션 변경; PetProfileResponseDto에서 Breed/DBTI 조회를 null-safe하게 변경
Pet 서비스·리포지토리 구현
src/main/java/.../pet/application/service/PetService.java, src/main/java/.../pet/infra/persistence/repository/PetRepositoryImpl.java, src/main/java/.../pet/infra/persistence/repository/BreedRepositoryImpl.java
BreedRepository.findBreedById 사용으로 견종 조회 추가 및 검증; 사용자당 반려견 중복 등록 체크 추가; 여러 리포지토리 메서드명/반환타입 수정(Optional 도입 등)
SpringData Pet/Breed 리포지토리 인터페이스
src/main/java/.../pet/domain/repository/BreedRepository.java, src/main/java/.../pet/infra/persistence/repository/SpringDataPetRepository.java
findById → findBreedById 네이밍 변경; findAllByUser_UserId -> findByUser_UserId 등 Optional 반환으로 변경, EntityGraph에 dbtiResult 추가; existsByUser_UserId 추가
Pet 엔티티 구조 변경
src/main/java/.../pet/infra/persistence/entity/PetEntity.java
breed를 BreedEntity 다대일 관계로 변경; 기존 dbti String 필드 제거 후 DbtiResultEntity dbtiResult 일대일 관계로 대체; setDbtiResult 연관 설정 추가
User 관련 API/서비스 반환 타입 변경
src/main/java/.../user/api/controller/UserController.java, src/main/java/.../user/application/facade/query/UserPetQueryFacade.java, src/main/java/.../user/application/service/UserPetQueryService.java
사용자 반려견 조회가 리스트 → 단일 PetProfileResponseDto 반환으로 축소(메서드명/시그니처 변경)

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as DbtiController
    participant Facade as DbtiQueryFacade
    participant Service as DbtiQueryService
    participant PetRepo as PetRepository
    participant ResultRepo as DbtiResultRepository
    participant DbtiRepo as DbtiRepository

    Client->>Controller: GET /pets/dbti/{petId}
    activate Controller
    Controller->>Facade: getPetDbtiResult(userId, petId)
    activate Facade
    Facade->>Service: getPetDbtiResultDetail(userId, petId)
    activate Service
    Service->>PetRepo: findById(petId)
    PetRepo-->>Service: Optional<PetEntity>
    alt Pet not found
        Service-->>Facade: throw PetBusinessException(PET_NOT_FOUND)
        Facade-->>Controller: 404
        Controller-->>Client: ApiResponse(404)
    else Pet found
        Service->>Service: verify pet.userId == userId
        alt user mismatch
            Service-->>Facade: throw PetBusinessException(PET_ACCESS_DENIED)
            Facade-->>Controller: 403
            Controller-->>Client: ApiResponse(403)
        else user matches
            Service->>ResultRepo: findByPet_PetId(petId)
            ResultRepo-->>Service: Optional<DbtiResultEntity>
            alt result not found
                Service-->>Facade: throw UserBusinessException(USER_PET_NOT_REGISTERED)
                Facade-->>Controller: 404
                Controller-->>Client: ApiResponse(404)
            else result found
                Service->>DbtiRepo: findByDbtiType(result.dbtiType)
                DbtiRepo-->>Service: DbtiEntity
                Service-->>Facade: DbtiResultDetailVo
                Facade-->>Controller: DbtiResultResponseDto
                Controller-->>Client: ApiResponse(200, body)
            end
        end
    end
    deactivate Service
    deactivate Facade
    deactivate Controller
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bingle625
  • dltnals317
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive PR에는 #212의 DBTI 조회 API 외에도 Breed 엔티티 연동, Pet 엔티티 구조 개선, 유저당 반려견 한 마리 제한 등의 변경이 포함되어 있으며, 이는 PR 설명에 명시되어 있으나 #212 범위를 초과합니다. 추가 변경 사항(Breed, Pet 구조, 유저당 반려견 제한)이 #212와 직접 연관되었는지 또는 별도 이슈와 관련되었는지 명확히 하고, 필요시 관련 이슈 번호를 추가하시기 바랍니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 반려견 DBTI 조회 API 구현과 데이터 구조 최적화(Breed, DBTI 연관관계 개선)라는 주요 변경 사항을 명확하게 요약합니다.
Description check ✅ Passed PR 설명은 요약 설명, 변경 사항, PR 타입, 테스트, 체크리스트, 리뷰어 메모, 관련 이슈 등 필수 섹션을 모두 포함하며 상세하게 작성되어 있습니다.
Linked Issues check ✅ Passed PR은 #212에서 요구한 DBTI 조회 API 구현(조회 Service 로직, 데이터 매핑, 예외 처리)을 모두 충족하며, GET /pets/dbti/{petId} 엔드포인트 추가 및 관련 로직 구현이 확인됩니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@daeun-han
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
`@src/main/java/org/sopt/pawkey/backendapi/domain/dbti/api/controller/DbtiController.java`:
- Around line 75-81: The getPetDbtiResult endpoint is missing the `@UserId`
authentication annotation and owner verification; update the method signature of
getPetDbtiResult to accept the authenticated user id via `@UserId` (matching how
getDbtiQuestions/submitDbti do) and pass/validate that id before calling
dbtiQueryFacade.getPetDbtiResult to ensure the requesting user owns the pet (or
otherwise enforce authorization); if dbtiQueryFacade lacks an overload for owner
checks, add/adjust a service/facade method to perform ownership validation and
return the DbtiResultResponseDto only when authorized.

In
`@src/main/java/org/sopt/pawkey/backendapi/domain/dbti/infra/persistence/entity/DbtiResultEntity.java`:
- Around line 33-35: The pet relationship on DbtiResultEntity is currently a
nullable=false `@JoinColumn` but lacks a uniqueness constraint allowing multiple
DbtiResultEntity rows to reference the same Pet; update the mapping to enforce
1:1 uniqueness by adding a unique constraint for the pet_id join column (e.g.,
set unique=true on the `@JoinColumn` for the pet field or add a
`@Table`(uniqueConstraints = ...) on DbtiResultEntity) so that
DbtiResultEntity.pet cannot be duplicated and the DB schema enforces one
DbtiResult per Pet.

In
`@src/main/java/org/sopt/pawkey/backendapi/domain/pet/api/dto/response/PetProfileResponseDto.java`:
- Around line 28-29: The map/construction in PetProfileResponseDto calls
pet.getBreed().getName() without null-safety, which can NPE if breed is null;
update the code that builds the response (where pet.getBreed() is accessed) to
null-check pet.getBreed() and use a safe fallback (e.g., empty string or "품종
미등록") similar to how profileImage and dbtiResult are handled — replace direct
pet.getBreed().getName() with a conditional or Optional access that returns a
default when getBreed() is null.

In
`@src/main/java/org/sopt/pawkey/backendapi/domain/pet/infra/persistence/entity/PetEntity.java`:
- Around line 65-66: The PetEntity has a lazy `@OneToOne` dbtiResult causing an
extra SELECT when calling the inherited findById(petId); apply the same eager
fetch via an EntityGraph or add a repository method that loads it only when
needed. Concretely, add `@EntityGraph`(attributePaths =
{"profileImage","dbtiResult"}) to the repository method used for single-item
loads (the findById equivalent) or create a new method such as
findByIdWithDbtiResult(Long petId) that carries that `@EntityGraph`, so accesses
to PetEntity.dbtiResult won't trigger the extra lazy SELECT at runtime.

In
`@src/main/java/org/sopt/pawkey/backendapi/domain/user/application/service/UserPetQueryService.java`:
- Line 19: Remove the unused DbtiResultRepository dependency from
UserPetQueryService: delete the DbtiResultRepository field and its
constructor/injection parameter, and remove any unused import; rely on
SpringDataPetRepository's `@EntityGraph` (attributePaths =
{"profileImage","dbtiResult"}) and the existing PetProfileResponseDto.from()
which accesses pet.getDbtiResult() to provide DBTI data instead.
🧹 Nitpick comments (3)
src/main/java/org/sopt/pawkey/backendapi/domain/pet/api/dto/request/PetRequestDto.java (1)

20-20: 양수 검증 추가를 고려해 주세요.

@NotNull은 null 체크만 수행합니다. 유효하지 않은 ID 값(예: 0 또는 음수)이 전달될 경우를 방지하려면 @Positive 어노테이션을 추가하는 것을 권장합니다.

💡 선택적 개선 제안
-	`@NotNull`(message = "품종은 필수값입니다.") Long breedId
+	`@NotNull`(message = "품종은 필수값입니다.")
+	`@Positive`(message = "품종 ID는 양수여야 합니다.") Long breedId

jakarta.validation.constraints.Positive import가 필요합니다.

src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiQueryService.java (2)

30-32: 필드 선언 위치가 일반적인 Java 컨벤션과 다릅니다.

resultRepositorypetRepository 필드가 기존 메서드 아래에 선언되어 있습니다. 일반적으로 Java에서는 모든 필드를 클래스 상단에 모아 선언합니다.

♻️ 제안하는 수정
 `@Service`
 `@RequiredArgsConstructor`
 `@Transactional`(readOnly = true)
 public class DbtiQueryService {
 	private final DbtiRepository dbtiRepository;
+	private final DbtiResultRepository resultRepository;
+	private final PetRepository petRepository;

 	public List<DbtiQuestionEntity> getAllQuestions() {
 		return dbtiRepository.findAllQuestionsWithDetails();
 	}
-
-	private final DbtiResultRepository resultRepository;
-	private final PetRepository petRepository;

 	public DbtiResultDetailVo getPetDbtiResultDetail(Long petId) {

34-47: 불필요한 DB 호출 및 중복 코드 개선 가능

  1. petRepository.existsById(petId) 체크는 resultRepository.findByPetId(petId)가 빈 결과를 반환하면 자연스럽게 처리되므로, 별도의 존재 확인 쿼리가 중복될 수 있습니다. 단, Pet이 존재하지만 DBTI 결과가 없는 경우와 Pet 자체가 없는 경우를 구분해야 한다면 현재 로직이 맞습니다.

  2. Line 46에서 DbtiResultDetailVo가 이미 import 되어 있으므로 FQN(fully qualified name) 사용이 불필요합니다.

♻️ 제안하는 수정
-		return new org.sopt.pawkey.backendapi.domain.dbti.application.dto.DbtiResultDetailVo(result, dbtiInfo);
+		return new DbtiResultDetailVo(result, dbtiInfo);

@daeun-han
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiCommandService.java`:
- Around line 42-44: The ownership check in DbtiCommandService currently risks
NPE and uses the wrong error code; update the check to first null-check
pet.getUser() (avoid calling pet.getUser().getUserId() when pet.getUser() is
null) and, if the owner is null or owner.getUserId() does not equal userId,
throw a permission-related UserBusinessException (e.g., use
UserErrorCode.FORBIDDEN or add a new UserErrorCode.USER_FORBIDDEN constant and
throw that) instead of USER_NOT_FOUND; ensure you update any enum/constants if
you add USER_FORBIDDEN and use that new constant in the throw site
(DbtiCommandService).

In
`@src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiQueryService.java`:
- Around line 42-44: In DbtiQueryService replace the direct call to
pet.getUser().getUserId() with a null-safe check: if pet.getUser() is null,
throw UserBusinessException(UserErrorCode.USER_NOT_FOUND); otherwise, when the
owner's userId does not equal the provided userId, throw an
authorization-specific error (e.g.,
UserBusinessException(UserErrorCode.FORBIDDEN) or USER_NOT_AUTHORIZED) instead
of USER_NOT_FOUND to reflect lack of permission; update the throw site that
currently uses UserErrorCode.USER_NOT_FOUND to use the authorization error and
ensure you reference the existing pet.getUser() and userId variables.
♻️ Duplicate comments (1)
src/main/java/org/sopt/pawkey/backendapi/domain/pet/infra/persistence/entity/PetEntity.java (1)

65-66: findById() 메서드에서 dbtiResult 지연 로딩 최적화 필요

@OneToOne(mappedBy="pet", fetch=FetchType.LAZY) 설정이 되어 있지만, Hibernate에서 inverse side의 @OneToOne LAZY 로딩은 실제로 프록시를 생성하지 못하고 추가 SELECT가 발생합니다. findAllByUser_UserId()에는 @EntityGraph가 적용되어 있으나, findById()에는 적용되지 않아 개별 Pet 조회 시 추가 쿼리가 발생할 수 있습니다.

🧹 Nitpick comments (5)
src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiCommandService.java (1)

62-75: 양방향 연관관계 동기화 고려가 필요합니다.

새로운 DbtiResultEntity 생성 시 pet.setDbtiResult(result)를 호출하지 않아, 같은 트랜잭션 내에서 pet.getDbtiResult()null을 반환할 수 있습니다. DbtiResultEntity에 이미 setPet() 메서드가 양방향 동기화를 처리하도록 구현되어 있으므로 활용하는 것이 좋습니다.

♻️ 수정 제안
 		DbtiResultEntity result = resultRepository.findByPetId(petId)
 			.map(existingResult -> {
 				existingResult.updateResult(dbtiType, eiScore, psScore, rfScore);
 				return existingResult;
 			})
 			.orElseGet(() -> {
-				return resultRepository.save(DbtiResultEntity.builder()
+				DbtiResultEntity newResult = DbtiResultEntity.builder()
 					.pet(pet)
 					.dbtiType(dbtiType)
 					.eiScore(eiScore)
 					.psScore(psScore)
 					.rfScore(rfScore)
-					.build());
+					.build();
+				pet.setDbtiResult(newResult);
+				return resultRepository.save(newResult);
 			});
src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiQueryService.java (2)

33-35: 필드 선언 위치가 일반적인 컨벤션을 따르지 않습니다.

resultRepositorypetRepository 필드가 메서드 정의 이후에 선언되어 있습니다. Java에서는 일반적으로 모든 필드를 클래스 상단에 선언하는 것이 가독성과 유지보수에 좋습니다.

♻️ 수정 제안
 `@Service`
 `@RequiredArgsConstructor`
 `@Transactional`(readOnly = true)
 public class DbtiQueryService {
 	private final DbtiRepository dbtiRepository;
+	private final DbtiResultRepository resultRepository;
+	private final PetRepository petRepository;

 	public List<DbtiQuestionEntity> getAllQuestions() {
 		return dbtiRepository.findAllQuestionsWithDetails();
 	}
-
-	private final DbtiResultRepository resultRepository;
-	private final PetRepository petRepository;

 	public DbtiResultDetailVo getPetDbtiResultDetail(Long userId, Long petId) {

52-52: 불필요한 FQN(Fully Qualified Name) 사용

DbtiResultDetailVo는 이미 5번 라인에서 import되어 있으므로 FQN 없이 사용할 수 있습니다.

♻️ 수정 제안
-		return new org.sopt.pawkey.backendapi.domain.dbti.application.dto.DbtiResultDetailVo(result, dbtiInfo);
+		return new DbtiResultDetailVo(result, dbtiInfo);
src/main/java/org/sopt/pawkey/backendapi/domain/pet/infra/persistence/entity/PetEntity.java (1)

93-95: 양방향 연관관계 동기화가 누락되었습니다.

setDbtiResult 메서드가 DbtiResultEntity.setPet()처럼 양방향 동기화를 수행하지 않습니다. 일관성을 위해 양쪽 setter 모두 동기화 로직을 포함하거나, 한쪽에서만 관리하도록 명확히 하는 것이 좋습니다.

♻️ 수정 제안
 	public void setDbtiResult(DbtiResultEntity dbtiResult) {
 		this.dbtiResult = dbtiResult;
+		if (dbtiResult != null && dbtiResult.getPet() != this) {
+			dbtiResult.setPet(this);
+		}
 	}

단, DbtiResultEntity.setPet()에서 이미 역방향 동기화를 수행하므로 무한 루프 방지 조건이 있는지 확인하세요.

src/main/java/org/sopt/pawkey/backendapi/domain/dbti/infra/persistence/entity/DbtiResultEntity.java (1)

15-15: 사용하지 않는 import 문이 있습니다.

@ManyToOne이 import되어 있지만 실제로는 @OneToOne만 사용됩니다.

♻️ 수정 제안
-import jakarta.persistence.ManyToOne;

@daeun-han
Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/sopt/pawkey/backendapi/domain/user/api/controller/UserController.java (1)

111-123: Swagger 설명을 단일 반려견 조회에 맞게 수정하세요.

@Operation 설명이 "반려견 목록을 조회합니다"로 되어 있지만, 실제 API는 단일 PetProfileResponseDto를 반환합니다. 설명을 실제 동작과 일치하도록 수정해야 합니다.

📝 제안된 수정사항
-	`@Operation`(summary = "유저 반려견 프로필 조회", description = "유저가 등록한 반려견 목록을 조회합니다.", tags = {"Users"})
+	`@Operation`(summary = "유저 반려견 프로필 조회", description = "유저가 등록한 반려견 프로필을 조회합니다.", tags = {"Users"})
🤖 Fix all issues with AI agents
In
`@src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiQueryService.java`:
- Around line 46-47: The code currently throws
UserBusinessException(UserErrorCode.USER_PET_NOT_REGISTERED) when
resultRepository.findByPetId(petId) returns empty; change this to throw a
domain-appropriate exception by adding a DBTI_RESULT_NOT_FOUND entry to
DbtiErrorCode (e.g., "해당 반려견의 DBTI 결과가 존재하지 않습니다.") and then replace the throw
with new DbtiBusinessException(DbtiErrorCode.DBTI_RESULT_NOT_FOUND) in the
DbtiQueryService where DbtiResultEntity is looked up
(resultRepository.findByPetId).
🧹 Nitpick comments (5)
src/main/java/org/sopt/pawkey/backendapi/domain/pet/infra/persistence/repository/PetRepositoryImpl.java (1)

3-18: 사용되지 않는 import와 의존성을 제거하세요.

리팩토링 후 다음 항목들이 사용되지 않습니다:

  • Line 3: java.util.List import
  • Line 7: BreedEntity import
  • Line 18: springDataBreedRepository 필드
♻️ 제안된 수정사항
 package org.sopt.pawkey.backendapi.domain.pet.infra.persistence.repository;

-import java.util.List;
 import java.util.Optional;

 import org.sopt.pawkey.backendapi.domain.pet.domain.repository.PetRepository;
-import org.sopt.pawkey.backendapi.domain.pet.infra.persistence.entity.BreedEntity;
 import org.sopt.pawkey.backendapi.domain.pet.infra.persistence.entity.PetEntity;
 import org.springframework.stereotype.Repository;

 import lombok.RequiredArgsConstructor;

 `@Repository`
 `@RequiredArgsConstructor`
 public class PetRepositoryImpl implements PetRepository {

 	private final SpringDataPetRepository springDataPetRepository;
-	private final SpringDataBreedRepository springDataBreedRepository;
src/main/java/org/sopt/pawkey/backendapi/domain/user/application/facade/query/UserPetQueryFacade.java (1)

3-3: 사용되지 않는 import를 제거하세요.

java.util.List import가 단일 객체 반환으로 리팩토링된 후 더 이상 사용되지 않습니다.

♻️ 제안된 수정사항
 package org.sopt.pawkey.backendapi.domain.user.application.facade.query;

-import java.util.List;
-
 import org.sopt.pawkey.backendapi.domain.pet.api.dto.response.PetProfileResponseDto;
src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiCommandService.java (1)

19-20: 사용되지 않는 import 구문이 있습니다.

UserBusinessExceptionUserErrorCode가 import되어 있지만, 접근 권한 검사가 PetBusinessException(PetErrorCode.PET_ACCESS_DENIED)로 변경되면서 더 이상 사용되지 않습니다.

🧹 제거 제안
-import org.sopt.pawkey.backendapi.domain.user.exception.UserBusinessException;
-import org.sopt.pawkey.backendapi.domain.user.exception.UserErrorCode;
src/main/java/org/sopt/pawkey/backendapi/domain/dbti/application/service/DbtiQueryService.java (2)

33-35: 필드 선언 위치가 일관성 없습니다.

resultRepositorypetRepository 필드가 메서드(getAllQuestions) 이후에 선언되어 있습니다. 클래스 상단의 다른 필드(dbtiRepository)와 함께 배치하는 것이 좋습니다.

♻️ 리팩토링 제안
 `@Service`
 `@RequiredArgsConstructor`
 `@Transactional`(readOnly = true)
 public class DbtiQueryService {
 	private final DbtiRepository dbtiRepository;
+	private final DbtiResultRepository resultRepository;
+	private final PetRepository petRepository;

 	public List<DbtiQuestionEntity> getAllQuestions() {
 		return dbtiRepository.findAllQuestionsWithDetails();
 	}
-
-	private final DbtiResultRepository resultRepository;
-	private final PetRepository petRepository;

52-52: 불필요한 전체 패키지 경로 사용

DbtiResultDetailVo는 이미 line 5에서 import되어 있으므로 전체 패키지 경로 없이 사용할 수 있습니다.

🧹 수정 제안
-		return new org.sopt.pawkey.backendapi.domain.dbti.application.dto.DbtiResultDetailVo(result, dbtiInfo);
+		return new DbtiResultDetailVo(result, dbtiInfo);


@Component
@RequiredArgsConstructor
public class DbtiQueryFacade {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1]
이전에 리뷰 드렸던 UpdateUserInfoFacade 사례와 비슷한 맥락의 의견이 있습니다!
모든 기능(Query, Update 등)이 Controller에 도달하기 전 기계적으로 Facade를 거쳐야 한다는 접근보다는, 해당 유즈케이스에서 Facade가 없을 때, Controller가 실제로 어떤 문제가 있을지를 먼저 고민해 보면 좋을 것 같습니다

현재 DbtiQueryFacade는 유즈케이스 흐름을 조율하기보다 조회 결과를 변환하는 전달하는 역할에 집중되어 있어, 결과적으로 계층의 복잡도만 높이는 상황인 것 같아요(사실 Facade는 비즈니스 로직을 조율하는 곳이지, 단순 변환 로직을 담당하기에는 역할이 다소 모호하다고 생각합니다)

물론 저희가 도메인별로 Facade와 Service를 기본 패키지 구조로 가져가고는 있지만, 모든 기능을 1:1로 Facade로 쪼개는 것이 최선인지는 논의가 필요해 보입니다! 제가 제안드리는 방향은 이렇습니다

  • 여러 도메인을 조합할 필요가 없는 단순 유즈케이스는 Facade 없이 Application Service에서 담백하게 처리 (단순 전달자 역할의 Facade를 지양해서, Facade의 역할을 인지하면서 개발)

  • 여러 도메인(User, Image 등)을 엮거나, 정책적인 흐름 제어가 필요한 시점에 도입

  • 단위 재정의: 기능을 하나하나 Facade 클래스로 쪼개기보다, 의미 있는 큰 유즈케이스 단위(ex: DbtiFacade)로 응집도를 높여서 관리

앞으로도 Facade가 단순 전달자 역할만 하게 될 때, 이 방식을 관성적으로 지키는 것이 생산성과 유지보수에 도움이 될 지 같이 이야기해봐요!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 주신 Facade 레이어의 관성적 사용에 대한 우려에 공감하는 부분입니다. 제 코드에서도 단순 전달 역할만 수행하는 부분이 계층 복잡도를 높였다는 점을 인지했습니다.

그렇지만, 현재 저희 프로젝트는 CQRS 원칙에 따라 Command와 Query의 책임을 패키지 수준에서 분리하고 있습니다. 또한 특정 유즈케이스에서만 Facade를 생략할 경우, 전체적인 아키텍처의 일관성이 깨진다는 우려가 있습니다. 그리고 단순 조회일지라도 향후 권한 체크, 로깅, 혹은 타 도메인과의 조율 로직이 추가될 때 Facade 레이어가 이미 존재한다면 코드 수정이 훨씬 안정적일 것이라는 생각도 드는 바입니다.

따라서 이번 PR에서는 구조적 통일성을 위해 Facade 레이어는 유지하되, 언급해 주신 '단순 변환 로직' 문제는 DTO 내부에 정적 팩토리 메서드(from)를 구현하여 Facade의 코드를 최소화하는 방향으로 개선해 보았습니다. 이렇게 함으로써 Facade가 순수하게 유즈케이스 흐름만 제어하는 역할에 집중하도록 교정했습니다.

마지막으로 현재 모든 도메인에 동일한 아키텍처가 적용되어 있는 상태이기 때문에, 말씀해주신것처럼 추후에 함께 아키텍처 표준을 논의하고 확정된 안을 별도 이슈로 생성하여 전사적으로 적용하는 방향은 어떨까요? 의견 부탁드립니다!

private static final int EXPECTED_OPTION_COUNT = 9;

public DbtiResultDetailVo calculateSaveAndGetDetail(Long petId, DbtiSubmitRequestDto request) {
public DbtiResultDetailVo calculateSaveAndGetDetail(Long userId, Long petId, DbtiSubmitRequestDto request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1]
DbtiCommandService와 DbtiQueryService 모두에서 DbtiResultDetailVo를 반환하고 있는데, 현재 이 객체는 두 엔티티(DbtiResultEntity, DbtiEntity)를 함께 묶어 전달하기 위한 중간 전달 객체로 사용되고 있는 것으로 보입니다. 혹시 의도를 알 수 있을까요?! 개인적으로 이 부분에 대해 우려되는 지점이 있습니다

앱잼 초기에 Entity와 Domain 객체를 분리하는 DDD구조로 방향을 잡은 후, 개발 과정에서 복잡도와 N+1 문제 등을 고려하여, JPA Entity를 도메인 모델로 직접 사용하기로 합의한 상태로 알고 있습니다
현재 VO 사용에 대한 합의가 없는 상황에서, 식별자를 가진 엔티티들을 단순히 묶어주는 객체에 Vo라는 네이밍을 사용하는 것은 역할 해석이나 향후 리팩터링 과정에서 혼란을 줄 수 있을 것 같다는 생각이 들었습니다!!

(다은님은 평소에 VO 계층을 어떤 용도로 정의하고 사용하시는지 궁금합니다! 이번 기회에 이 부분에 대한 의도를 들어보고, 오프라인 회의에서 Facade/Service 반환 컨벤션과 VO 도입 여부를 함께 고민해보면 좋을 것 같아요~!)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 감사합니다! DbtiResultDetailVo는 말씀하신 대로 복잡한 의도가 있었다기보다는, 두 엔티티를 한꺼번에 반환해야 하는 상황에서 단순히 데이터를 묶어 전달(Wrapper)하기 위한 용도로 사용했습니다. 평소 저는 VO를 '불변성을 가진 값의 집합' 정도로 생각하고 사용해 왔는데, 우리 프로젝트의 컨벤션(Entity를 도메인 모델로 직접 사용)을 고려했을 때 Vo라는 명칭이 오해를 살 수 있다는 점을 알게 되었습니다. 따라서, 이번 기회에 제안해 주신 대로 단순 전달 객체의 네이밍 컨벤션을 명확히 정하면 좋을 것 같습니다!

우선은 혼란을 방지하기 위해 명칭을 Dto 혹은 목적에 맞는 이름으로 수정해 보려 했으나, 이전 리뷰에서 제안해 주신 'Facade의 역할 재정의'와 연결 지어 고민해 본 결과, 서비스에서 임시 객체(Vo)로 묶어 반환하기보다 Facade에서 각 서비스를 호출하여 결과를 조합(Orchestration)하는 방식이 더 적절하다고 판단하여 해당 방식으로 리팩터링했습니다.

이를 통해 서비스는 각자의 도메인 로직에 더 집중할 수 있게 되었고, Facade는 여러 서비스를 조율하는 본연의 역할을 수행하게 되었습니다. 네이밍 혼란을 야기했던 중간 전달 객체도 제거하여 구조가 한결 담백해지도록 구성했습니다-!

Comment on lines 27 to 28
public class DbtiQueryService {
private final DbtiRepository dbtiRepository;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1]
질문 조회(getAllQuestions)는 엔티티 리스트를, 결과 조회(getPetDbtiResultDetail)는 중간 객체를 반환하고 있는데 의도된 건가요?~

제 생각으로는
엔티티 반환보다는 ResponseDto 반환으로 역할을 통일하는 것이 구조적으로 더 명확할 것 같습니다!(queryService의 성격이 api명세이 dto에 맞는 데이터를 서빙하는 역할이니까요)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 감사합니다! 말씀해 주신 내용을 바탕으로 서비스에서 DTO를 반환할 때의 실용적인 이점들에 대해 찾아보고 고민해보게 되었습니다.

그럼에도 이번 PR에서 모든 반환 타입을 엔티티로 통일한 이유는 현재 우리 프로젝트가 Facade 계층을 적극적으로 활용하고 있기 때문입니다. 파사드 레이어는 여러 서비스의 도메인 데이터를 조합하여 하나의 유즈케이스를 완성하는 오케스트레이션 역할을 수행한다고 인지했기에, 서비스가 특정 API 스펙인 DTO에 종속되기보다 순수한 엔티티를 제공하는 것이 로직의 재사용성과 유연한 조합 측면에서 더 유리하다고 판단했습니다!

또한 이러한 구조는 프론트엔드의 요구사항 변경이 비즈니스 로직이 담긴 서비스 레이어까지 전파되는 것을 차단하여 레이어 간의 결합도를 낮추는 효과가 있고, 엔티티 반환 시에 우려될 수 있는 지연 로딩 문제는 레포지토리 단에서 Fetch Join 등을 활용해 안정적으로 관리하고 있기 때문에, 이를 통해 성능과 설계의 일관성을 동시에 확보할 수 있다고 생각했습니당.

다만 제안해 주신 방식이 주는 개발 생산성과 구조적 명확성 역시 큰 장점인 만큼, 어떤 방향이 우리 팀의 표준 아키텍처로서 더 적합할지에 대해서는 오프라인 회의를 통해 함께 논의해 보고 싶습니다!

@dltnals317
Copy link
Copy Markdown
Contributor

고생하셨어요를레이히~

@daeun-han
Copy link
Copy Markdown
Member Author

리뷰해주신 내용을 바탕으로 반려견 검증 로직 공통화 및 레이어 책임 분리 리팩터링을 완료하여 푸시했습니다. 제안해주신 덕분에 중복을 제거하고 도메인 간 결합도를 낮출 수 있었습니다.

서비스 반환 타입과 같은 아키텍처 방향성에 대해서는 이번 리팩터링 구조를 바탕으로 추후 오프라인에서 함께 논의해보고 싶습니다. 시간 되실 때 다시 한번 확인 부탁드립니다. 감사합니다!

@daeun-han daeun-han merged commit eec1c3e into develop Jan 29, 2026
1 check passed
@daeun-han daeun-han deleted the feat/#212 branch February 5, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 반려견 dbti 조회 API

2 participants