Skip to content

Conversation

@1117mg
Copy link
Member

@1117mg 1117mg commented Jul 20, 2025

📌 PR 제목

  • feat: 1차 도메인 설계

📋 작업 내용

  • 공통 BaseEntity 추가 (createdAt, updatedAt)
  • 서비스 관련 엔티티 설계

✅ 체크리스트

  • 테스트를 완료했나요?
  • 코드 컨벤션을 지켰나요?
  • 관련 문서를 업데이트 했나요? -> ERD - 팀 노션

🚨 주의사항

  • 티어별로 세부 이미지가 여러 개 생길 가능성도 있는데 별도 엔티티로 관리하는게 좋을지 고민입니다..

@1117mg 1117mg requested a review from eric-jy-park July 20, 2025 10:37
@1117mg 1117mg self-assigned this Jul 20, 2025
@1117mg 1117mg added the enhancement New feature or request label Jul 20, 2025
@claude
Copy link

claude bot commented Jul 20, 2025

Claude finished @1117mg's task —— View job


1차 도메인 설계 코드 리뷰 완료 ✅

안녕하세요! 1차 도메인 설계에 대한 종합적인 코드 리뷰를 완료했습니다.

📋 리뷰 체크리스트

  • BaseEntity 공통 엔티티 분석
  • User 엔티티 검토
  • ClimbingGym 엔티티 검토
  • Tier 엔티티 검토
  • Sector 엔티티 검토
  • RouteMission 엔티티 검토
  • UserSession 엔티티 검토
  • UserMissionAttempt 엔티티 검토
  • SRHistory 엔티티 검토
  • ChallengeRecommendation 엔티티 검토
  • 전체 도메인 설계 및 관계 분석
  • 성능 및 보안 고려사항 검토
  • 최종 리뷰 작성

🌟 잘 구현된 부분들

  1. BaseEntity 활용: createdAt, updatedAt 필드를 공통으로 관리하는 것은 훌륭한 접근입니다.
  2. JPA 어노테이션 활용: 대부분의 엔티티에서 적절한 JPA 어노테이션을 사용했습니다.
  3. Lombok 활용: @Getter, @Setter, 보호된 생성자 등을 적절히 활용했습니다.
  4. 지연 로딩: @ManyToOne에서 FetchType.LAZY를 사용한 것은 성능상 좋은 선택입니다.
  5. 관계형 설계: 클라이밍 도메인의 복잡한 관계를 잘 표현했습니다.

❗ 반드시 수정해야 할 부분들

1. BaseEntity 상속 일관성 문제

문제: 일부 엔티티가 BaseEntity를 상속받지 않고 있습니다.

해당 엔티티:

  • Sector (src/main/java/com/climbup/climbup/sector/entity/Sector.java:18)
  • UserMissionAttempt (src/main/java/com/climbup/climbup/attempt/entity/UserMissionAttempt.java:19)
  • ChallengeRecommendation (src/main/java/com/climbup/climbup/recommendation/entity/ChallengeRecommendation.java:16)

해결방법: 모든 엔티티가 BaseEntity를 상속받도록 수정

2. ID 타입 일관성 문제

문제: ID 필드의 타입이 일관되지 않습니다.

  • Long: User, Sector, RouteMission, UserSession, UserMissionAttempt, SRHistory, ChallengeRecommendation
  • Integer: ClimbingGym, Tier

해결방법: 모든 엔티티의 ID를 Long으로 통일 (Integer는 21억 제한으로 확장성 이슈)

3. 시간 어노테이션 혼용 문제

문제: SRHistory에서 @CreationTimestamp, BaseEntity에서 @CreatedDate 혼용

해결방법: Spring Data JPA의 @CreatedDate로 통일

4. Foreign Key Nullable 문제

문제: ChallengeRecommendation의 관계 필드들이 nullable로 설정

// 현재 (문제)
@JoinColumn(name = "session_id")  // nullable = false 누락
@JoinColumn(name = "mission_id")  // nullable = false 누락

// 수정 필요
@JoinColumn(name = "session_id", nullable = false)
@JoinColumn(name = "mission_id", nullable = false)

⚠️ 개선 권장 사항들

5. 유효성 검증 추가

현재 상태: Bean Validation 어노테이션 누락

권장 개선:

// User 엔티티 예시
@NotBlank(message = "닉네임은 필수입니다")
@Size(min = 2, max = 50, message = "닉네임은 2-50자 사이여야 합니다")
@Column(name = "nickname", nullable = false, unique = true, length = 50)
private String nickname;

@Min(value = 0, message = "SR은 0 이상이어야 합니다")
@Max(value = 9999, message = "SR은 9999 이하여야 합니다")
@Column(name = "sr", nullable = false, columnDefinition = "INT DEFAULT 600")
private Integer sr = 600;

6. 데이터 중복 이슈

문제 1: Difficulty 필드 중복

  • RouteMission.difficulty vs ChallengeRecommendation.difficulty

문제 2: Score 필드 중복 (주석으로도 언급됨)

  • Sector.score vs RouteMission.score

권장사항: 도메인 로직 검토 후 중복 제거 또는 명확한 역할 구분

7. 인덱스 추가 권장

@Table(name = "users", indexes = {
    @Index(name = "idx_user_nickname", columnList = "nickname"),
    @Index(name = "idx_user_tier", columnList = "tier_id")
})

@Table(name = "user_sessions", indexes = {
    @Index(name = "idx_session_user_date", columnList = "user_id, session_date")
})

@Table(name = "user_mission_attempts", indexes = {
    @Index(name = "idx_attempt_user_session", columnList = "user_id, session_id"),
    @Index(name = "idx_attempt_mission", columnList = "mission_id")
})

8. Enum 활용 검토

현재: String으로 difficulty 관리
권장: Enum으로 타입 안정성 확보

public enum DifficultyLevel {
    BEGINNER("초급"),
    INTERMEDIATE("중급"), 
    ADVANCED("고급"),
    EXPERT("전문가");
    
    private final String description;
    // 생성자, getter 등
}

// 엔티티에서 사용
@Enumerated(EnumType.STRING)
@Column(name = "difficulty", nullable = false)
private DifficultyLevel difficulty;

🚀 아키텍처 개선 제안

1. Tier 이미지 관리 (PR 작성자 고민사항)

현재 고민하고 계신 티어별 세부 이미지 문제에 대한 제안:

@Entity
@Table(name = "tier_images")
public class TierImage extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "tier_id", nullable = false)
    private Tier tier;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "image_type", nullable = false)
    private TierImageType type; // MAIN, ACHIEVEMENT, LEVEL_UP, BADGE 등
    
    @NotBlank
    @Column(name = "image_url", nullable = false, columnDefinition = "TEXT")
    private String imageUrl;
    
    @Column(name = "alt_text")
    private String altText;
}

2. 성능 최적화 고려사항

N+1 문제 예방:

// Repository에서 Join Fetch 활용 예시
@Query("SELECT u FROM User u JOIN FETCH u.tier WHERE u.id = :id")
User findByIdWithTier(@Param("id") Long id);

대용량 데이터 대비 페이징:

// UserSession 조회 시 페이징 필수
Page<UserSession> findByUserIdOrderBySessionDateDesc(Long userId, Pageable pageable);

3. 보안 고려사항

민감 정보 보호:

  • 사용자 비디오 URL 접근 권한 검증 로직 필요
  • 이미지 URL 검증 및 XSS 방지

데이터 무결성:

  • SR 점수 변경 시 이력 관리 (SRHistory 잘 설계됨)
  • 세션 종료 시간이 시작 시간보다 빠른 경우 검증

📚 주니어 개발자를 위한 학습 포인트

1. JPA 관계 설정 패턴

  • 양방향 관계: 연관관계 주인(Owner) 설정의 중요성
  • 지연 로딩: 성능 최적화를 위한 필수 설정
  • Cascade: 언제 사용하고 언제 피해야 하는지

2. 데이터베이스 설계 원칙

  • 정규화: 데이터 중복 최소화
  • 인덱스: 조회 성능 최적화
  • 제약조건: 데이터 무결성 보장

3. 확장 가능한 설계

  • ID 타입 선택: Long vs Integer의 차이점
  • Enum 활용: 타입 안정성과 유지보수성
  • 공통 필드 관리: BaseEntity 패턴의 장점

🎯 총평

전반적으로 클라이밍 도메인을 잘 이해하고 합리적인 엔티티 설계를 하셨습니다. 특히 복잡한 도메인 관계를 JPA로 잘 표현했고, 성능을 고려한 지연 로딩 설정도 좋습니다.

우선순위 1 (필수 수정): BaseEntity 상속, ID 타입 통일, nullable 설정
우선순위 2 (권장): 유효성 검증, 인덱스 추가, 중복 데이터 정리
우선순위 3 (미래 고려): Enum 활용, 이미지 관리 구조 개선

주니어 개발자 단계에서 이 정도 설계를 하신 것은 매우 인상적입니다. 앞으로 실제 서비스 운영하면서 성능 이슈나 확장성 문제를 경험하시면서 더욱 성장하실 것 같습니다! 🚀


Comment on lines 27 to 28
@Column(name = "score", nullable = false)
private Integer score; //todo: RouteMission에 score가 있는데 Sector에도 필요한가?
Copy link
Member

Choose a reason for hiding this comment

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

섹터 스코어에는 뭐가 들어가게되나요?! 스코어는 개별 루트미션에 한정된 값일 것 같아서요!

Copy link
Member Author

Choose a reason for hiding this comment

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

맞아요 저도 그렇게 생각해요! 그럼 섹터에는 순수 구역 정보만 포함하고 점수는 루트미션에만 넣는걸로 할게요!!

@eric-jy-park
Copy link
Member

클로드 코드의 리뷰가 아쉬운 부분들이 좀 있네요 너무 기계적인 느낌

Comment on lines 33 to 34
@Column(name = "difficulty", nullable = false, length = 10)
private String difficulty;
Copy link
Member

Choose a reason for hiding this comment

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

루트미션에 이미 난이도값이 존재하는데, 추천쪽에도 필요할까 싶습니다..!

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 추천 쪽에서 난이도는 빼겠습니다!

@claude
Copy link

claude bot commented Jul 20, 2025

Claude finished @1117mg's task —— View job


클라이밍 서비스 1차 도메인 설계 리뷰 ✅

안녕하세요! 클라이밍 도메인을 정말 잘 이해하고 설계하신 것 같네요. 전체적으로 봤을 때 꽤 탄탄한 엔티티 구조를 만드셨습니다. 코드를 쭉 살펴보면서 느낀 점들을 솔직하게 말씀드릴게요.

🎯 먼저 잘하신 부분들

BaseEntity 패턴 도입이 정말 좋았어요. createdAt, updatedAt을 공통으로 관리하는 건 실무에서도 당연히 해야 할 패턴이고, Spring Data JPA의 @CreatedDate, @LastModifiedDate를 제대로 활용하신 것도 좋습니다.

JPA 관계 설정도 적절해요. 특히 FetchType.LAZY를 기본으로 쓰신 건 성능 측면에서 매우 좋은 선택이에요. N+1 문제를 미리 방지하려는 의식이 보입니다.

도메인 모델링도 클라이밍의 복잡한 관계를 잘 표현했어요. 사용자 → 세션 → 시도 → 루트미션 → 점수 변화까지의 플로우가 논리적으로 잘 연결되어 있습니다.

❗ 하지만 몇 가지 일관성 문제가 있어요

1. BaseEntity 상속이 일관되지 않아요

// UserSession, SRHistory는 BaseEntity를 상속받지 않고 있어요
public class UserSession {  // extends BaseEntity 누락
public class SRHistory {   // extends BaseEntity 누락

이 부분이 좀 아쉬워요. 세션이나 SR 변경 이력도 언제 생성되고 수정되었는지 추적이 필요한 데이터니까, 일관되게 BaseEntity를 상속받는 게 좋을 것 같아요.

2. ID 타입이 섞여있어요

private Long id;     // 대부분의 엔티티
private Integer id;  // ClimbingGym, Tier만 Integer

Integer는 21억까지밖에 안 되거든요. 클라이밍장이나 티어가 그렇게 많지는 않겠지만, 확장성을 생각하면 모두 Long으로 통일하는 게 안전해요.

3. SRHistory의 시간 관리가 특이해요

다른 엔티티들은 BaseEntity의 @CreatedDate를 쓰는데, SRHistory만 직접 @CreatedDate를 필드에 붙였네요. 통일성을 위해 BaseEntity를 상속받고 필요하면 changedAt 필드를 별도로 두는 게 어떨까요?

💡 개선하면 좋을 부분들

1. 리뷰 댓글에서 말씀하신 중복 문제들

eric-jy-park님이 지적해주신 부분들이 정말 좋은 포인트였어요:

  • 섹터에 score 필드 → 이미 제거하셨네요! 👍
  • 추천에서 difficulty 중복 → 이것도 getDifficulty() 메서드로 깔끔하게 처리하셨어요!

이런 식으로 팀원들과 코드리뷰하면서 중복을 제거해나가는 문화가 정말 좋은 것 같아요.

2. Tier 이미지 고민사항에 대한 생각

PR에서 고민하고 계신 "티어별 세부 이미지"는 정말 실무에서 자주 마주치는 문제예요. 제 생각에는:

// 당장은 현재 구조로 시작하되, 나중에 필요하면
@Entity
public class TierImage {
    @ManyToOne
    private Tier tier;
    
    @Enumerated(EnumType.STRING)
    private TierImageType type; // MAIN, ACHIEVEMENT, LEVEL_UP 등
    
    private String imageUrl;
}

이런 식으로 확장하는 게 좋을 것 같아요. 지금 당장 over-engineering 할 필요는 없으니까요.

3. UserSession의 endedAt 필드

@Column(name = "ended_at", nullable = false)
private LocalDateTime endedAt;

세션이 진행 중일 때는 endedAt이 null일 수 있지 않나요? nullable = true로 하고, 세션 종료 시에 값을 넣는 게 자연스러울 것 같아요.

🚀 주니어 개발자로서 성장 포인트

1. 일관성을 중시하세요
지금처럼 BaseEntity 패턴을 도입했으면, 모든 엔티티에 일관되게 적용하는 습관을 기르세요. 나중에 운영하다 보면 "언제 생성됐는지" 정보가 없어서 디버깅에 애먹는 경우가 정말 많아요.

2. 팀 코드리뷰의 힘
eric-jy-park님의 리뷰 댓글들이 정말 좋았어요. 이런 식으로 서로 코드를 봐주고 중복을 찾아내는 문화가 있으면 코드 품질이 확실히 올라갑니다.

3. 확장성 vs 단순함의 균형
티어 이미지 문제처럼, "나중에 복잡해질 수 있다"고 해서 처음부터 모든 걸 다 고려할 필요는 없어요. 현재 요구사항에 맞게 단순하게 시작하고, 필요할 때 리팩토링하는 게 좋습니다.

🎯 총평

솔직히 주니어 개발자 분이 이 정도로 도메인을 이해하고 JPA 관계를 설정하신 건 정말 인상적이에요. 특히 지연 로딩이나 Cascade 설정까지 신경 쓰신 걸 보면, 성능에 대한 고민도 하고 계신 것 같아서 좋습니다.

몇 가지 일관성 문제들만 정리하시면 정말 탄탄한 도메인 모델이 될 것 같아요. 실제 서비스 운영하면서 더 많은 경험을 쌓으시면 더욱 성장하실 거예요! 🔥


@claude
Copy link

claude bot commented Jul 20, 2025

Claude finished @1117mg's task —— View job


클라이밍 서비스 1차 도메인 설계 리뷰 ✅

안녕하세요! 클라이밍 도메인을 정말 잘 이해하고 설계하신 것 같네요. 전체적으로 봤을 때 꽤 탄탄한 엔티티 구조를 만드셨습니다. 코드를 쭉 살펴보면서 느낀 점들을 솔직하게 말씀드릴게요.

🎯 먼저 잘하신 부분들

BaseEntity 패턴 도입이 정말 좋았어요. createdAt, updatedAt을 공통으로 관리하는 건 실무에서도 당연히 해야 할 패턴이고, Spring Data JPA의 @CreatedDate, @LastModifiedDate를 제대로 활용하신 것도 좋습니다.

JPA 관계 설정도 적절해요. 특히 FetchType.LAZY를 기본으로 쓰신 건 성능 측면에서 매우 좋은 선택이에요. N+1 문제를 미리 방지하려는 의식이 보입니다.

도메인 모델링도 클라이밍의 복잡한 관계를 잘 표현했어요. 사용자 → 세션 → 시도 → 루트미션 → 점수 변화까지의 플로우가 논리적으로 잘 연결되어 있습니다.

❗ 하지만 몇 가지 일관성 문제가 있어요

1. BaseEntity 상속이 일관되지 않아요

// UserSession은 BaseEntity를 상속받지 않고 있어요
public class UserSession {  // extends BaseEntity 누락

UserSession이 BaseEntity를 상속받지 않는 게 좀 아쉬워요. 세션도 언제 생성되고 수정되었는지 추적이 필요한 데이터니까, 일관되게 BaseEntity를 상속받는 게 좋을 것 같아요.

2. UserSession의 endedAt 필드 문제

@Column(name = "ended_at")  // nullable = true로 되어있는데
private LocalDateTime endedAt;

@Column(name = "total_duration", nullable = false)  // 이건 nullable = false
private Integer totalDuration;

세션이 진행 중일 때는 endedAt이나 totalDuration이 null일 수 있지 않나요? 로직상 세션 종료 시에 값을 넣는 게 자연스러울 것 같은데, totalDuration도 nullable로 고려해보시는 게 어떨까요?

3. ChallengeRecommendation 생성자 이슈

// 생성자에서 difficulty 파라미터를 받지만 사용하지 않아요
public ChallengeRecommendation(UserSession session, RouteMission mission, Integer recommendedOrder, String difficulty) {
    this.session = session;
    this.mission = mission;
    this.recommendedOrder = recommendedOrder;
    // difficulty는 사용되지 않음
}

getDifficulty() 메서드로 mission에서 가져오는 구조로 바꾸셨는데, 생성자에서 difficulty 파라미터는 제거하는 게 좋을 것 같아요.

💡 팀 리뷰를 통한 개선사항들

eric-jy-park님이 지적해주신 부분들이 정말 좋은 포인트였어요:

  • 섹터에 score 필드 중복 → 이미 제거하셨네요! 👍
  • 추천에서 difficulty 중복 → 이것도 getDifficulty() 메서드로 깔끔하게 처리하셨어요!

이런 식으로 팀원들과 코드리뷰하면서 중복을 제거해나가는 문화가 정말 좋은 것 같아요.

🚀 추가 개선 제안들

1. Tier 이미지 고민사항에 대한 생각

PR에서 고민하고 계신 "티어별 세부 이미지"는 정말 실무에서 자주 마주치는 문제예요. 제 생각에는:

// 당장은 현재 구조로 시작하되, 나중에 필요하면
@Entity
public class TierImage {
    @ManyToOne
    private Tier tier;
    
    @Enumerated(EnumType.STRING)
    private TierImageType type; // MAIN, ACHIEVEMENT, LEVEL_UP 등
    
    private String imageUrl;
}

이런 식으로 확장하는 게 좋을 것 같아요. 지금 당장 over-engineering 할 필요는 없으니까요.

2. 성능 최적화 고려사항

인덱스 추가 권장:

@Table(name = "user_sessions", indexes = {
    @Index(name = "idx_session_user_date", columnList = "user_id, session_date")
})

@Table(name = "user_mission_attempts", indexes = {
    @Index(name = "idx_attempt_user_session", columnList = "user_id, session_id")
})

사용자별 세션 조회나 시도 기록 조회가 많을 텐데, 이런 인덱스들이 성능에 도움이 될 것 같아요.

3. 유효성 검증 추가

// User 엔티티 예시
@NotBlank(message = "닉네임은 필수입니다")
@Size(min = 2, max = 50, message = "닉네임은 2-50자 사이여야 합니다")
@Column(name = "nickname", nullable = false, unique = true, length = 50)
private String nickname;

@Min(value = 0, message = "SR은 0 이상이어야 합니다")
@Column(name = "sr", nullable = false, columnDefinition = "INT DEFAULT 600")
private Integer sr = 600;

Bean Validation을 활용하면 데이터 무결성을 더 강화할 수 있어요.

🎯 주니어 개발자로서 성장 포인트

1. 일관성을 중시하세요
지금처럼 BaseEntity 패턴을 도입했으면, 모든 엔티티에 일관되게 적용하는 습관을 기르세요. 나중에 운영하다 보면 "언제 생성됐는지" 정보가 없어서 디버깅에 애먹는 경우가 정말 많아요.

2. 팀 코드리뷰의 힘
eric-jy-park님의 리뷰 댓글들이 정말 좋았어요. 이런 식으로 서로 코드를 봐주고 중복을 찾아내는 문화가 있으면 코드 품질이 확실히 올라갑니다.

3. 확장성 vs 단순함의 균형
티어 이미지 문제처럼, "나중에 복잡해질 수 있다"고 해서 처음부터 모든 걸 다 고려할 필요는 없어요. 현재 요구사항에 맞게 단순하게 시작하고, 필요할 때 리팩토링하는 게 좋습니다.

4. Nullable vs Non-nullable 신중하게
필드별로 언제 null이 될 수 있는지 비즈니스 로직을 생각해보고 설정하세요. 예를 들어 세션의 endedAt이나 totalDuration처럼 진행 중일 때는 null일 수 있는 필드들이 있어요.

📝 총평

솔직히 주니어 개발자 분이 이 정도로 도메인을 이해하고 JPA 관계를 설정하신 건 정말 인상적이에요. 특히 지연 로딩이나 Cascade 설정까지 신경 쓰신 걸 보면, 성능에 대한 고민도 하고 계신 것 같아서 좋습니다.

몇 가지 일관성 문제들만 정리하시면 정말 탄탄한 도메인 모델이 될 것 같아요. 실제 서비스 운영하면서 더 많은 경험을 쌓으시면 더욱 성장하실 거예요! 🔥


@claude
Copy link

claude bot commented Jul 20, 2025

Claude finished @1117mg's task —— View job


클라이밍 서비스 1차 도메인 설계 리뷰 ✅

안녕하세요! 클라이밍 도메인을 정말 잘 이해하고 설계하신 것 같네요. 코드를 쭉 살펴보면서 느낀 점들을 솔직하게 말씀드릴게요.

🎯 먼저 정말 잘하신 부분들

BaseEntity 패턴 도입이 정말 좋았어요. createdAt, updatedAt을 공통으로 관리하는 건 실무에서도 당연히 해야 할 패턴이고, Spring Data JPA의 @CreatedDate, @LastModifiedDateAuditingEntityListener를 제대로 활용하신 것도 좋습니다.

JPA 관계 설정도 적절해요. 특히 FetchType.LAZY를 기본으로 쓰신 건 성능 측면에서 매우 좋은 선택이에요. N+1 문제를 미리 방지하려는 의식이 보입니다.

도메인 모델링도 클라이밍의 복잡한 관계를 잘 표현했어요. 사용자 → 세션 → 시도 → 루트미션 → 점수 변화까지의 플로우가 논리적으로 잘 연결되어 있습니다.

인덱스 고려도 좋아요. UserSessionUserMissionAttempt에 복합 인덱스를 미리 설정해두신 것은 성능을 생각한 설계입니다.

❗ 하지만 몇 가지 일관성 문제가 있어요

1. BaseEntity 상속이 일관되지 않아요

// UserSession이 BaseEntity를 상속받지 않고 있어요 (line 25)
public class UserSession {  // extends BaseEntity 누락

UserSession도 언제 생성되고 수정되었는지 추적이 필요한 데이터니까, 일관되게 BaseEntity를 상속받는 게 좋을 것 같아요. 특히 세션 시작/종료 시간과 별개로 엔티티 자체의 생성/수정 시간을 추적하는 것도 의미가 있어요.

2. ID 타입 혼재 문제

대부분의 엔티티가 Long id를 사용하는데, ClimbingGym(line 23)과 Tier(line 19)는 Long을 쓰고 있네요. 다행히 일관되게 Long으로 통일되어 있어서 좋습니다!

3. UserSession의 필드 설계

@Column(name = "ended_at")  // nullable 허용 (line 41-42)
private LocalDateTime endedAt;

@Column(name = "total_duration")  // nullable 허용 (line 44-45)  
private Integer totalDuration;

세션이 진행 중일 때는 endedAt이나 totalDuration이 null인 게 자연스러워 보이는데, 이 부분은 잘 설계하신 것 같아요!

💡 팀 리뷰를 통한 훌륭한 개선사항들

eric-jy-park님이 지적해주신 부분들과 그에 대한 대응이 정말 좋았어요:

  1. 섹터의 score 필드 제거 - "점수는 개별 루트미션에 한정된 값"이라는 지적이 정확했고, 실제로 제거하신 것도 좋습니다.

  2. 추천 엔티티의 difficulty 중복 제거 - ChallengeRecommendation.getDifficulty() 메서드(line 35-37)로 mission에서 가져오도록 한 것이 깔끔해요. 중복 데이터를 제거하면서도 편의성을 유지한 좋은 해결책입니다.

이런 식으로 팀원들과 코드리뷰하면서 중복을 찾아내고 개선해나가는 문화가 정말 멋있어요.

🚀 추가로 고려해볼 점들

1. Tier 이미지 고민사항에 대한 생각

PR에서 고민하고 계신 "티어별 세부 이미지"는 정말 실무에서 자주 마주치는 문제예요. 현재 Tier.imageUrl(line 24-25)에 TODO 주석을 달아두신 것처럼, 당장은 현재 구조로 시작하는 게 좋을 것 같아요.

나중에 정말 필요하면:

@Entity
public class TierImage {
    @ManyToOne
    private Tier tier;
    
    @Enumerated(EnumType.STRING)
    private TierImageType type; // MAIN, ACHIEVEMENT, LEVEL_UP 등
    
    private String imageUrl;
}

이런 식으로 확장할 수 있지만, 지금 당장 over-engineering 할 필요는 없어요.

2. 비즈니스 로직 검증 고려

// User 엔티티에서
@Column(name = "sr", nullable = false, columnDefinition = "INT DEFAULT 600")
private Integer sr = 600; // line 31-32

// Tier 엔티티에서  
@Column(name = "sr_min", nullable = false)
private Integer srMin; // line 27-28

User의 SR이 해당 Tier의 범위에 맞는지 검증하는 로직이나, Bean Validation을 추가하면 더 견고해질 것 같아요.

3. 엔티티 생성자 개선

// RouteMission 생성자 (line 54-59)
public RouteMission(ClimbingGym gym, Sector sector, String difficulty, Integer score) {
    // imageUrl, videoUrl은 어떻게 설정하지?
}

필수 필드인 imageUrl, videoUrl이 생성자에 없는데, 이 부분은 어떻게 설정할 계획인지 궁금해요. Builder 패턴을 고려해보시는 것도 좋을 것 같아요.

🎓 주니어 개발자로서 성장 포인트

1. 일관성을 중시하세요

지금처럼 BaseEntity 패턴을 도입했으면, 모든 엔티티에 일관되게 적용하는 습관을 기르세요. 나중에 운영하다 보면 "언제 생성됐는지" 정보가 없어서 디버깅에 애먹는 경우가 정말 많아요.

2. 팀 코드리뷰의 힘

eric-jy-park님과의 리뷰 과정이 정말 좋았어요. 서로 다른 관점에서 코드를 봐주고 중복을 찾아내는 문화가 있으면 코드 품질이 확실히 올라갑니다. 특히 도메인 지식이 필요한 부분(섹터 vs 루트미션의 점수)에서 팀원의 피드백이 빛났어요.

3. 확장성 vs 단순함의 균형

티어 이미지 문제처럼, "나중에 복잡해질 수 있다"고 해서 처음부터 모든 걸 다 고려할 필요는 없어요. 현재 요구사항에 맞게 단순하게 시작하고, 필요할 때 리팩토링하는 YAGNI(You Aren't Gonna Need It) 원칙이 중요해요.

4. 성능을 생각한 설계

인덱스를 미리 고려하신 것처럼, 실제 쿼리 패턴을 생각해서 설계하는 습관이 좋습니다. 앞으로 "이 화면에서 어떤 쿼리가 날아갈까?" 를 항상 생각해보세요.

📝 총평

솔직히 주니어 개발자 분이 이 정도로 클라이밍 도메인을 이해하고 JPA 관계를 설정하신 건 정말 인상적이에요. 특히:

  • 지연 로딩 설정
  • 복합 인덱스 고려
  • Cascade 관계 설정
  • 팀 피드백을 통한 중복 제거

이런 부분들에서 성능과 유지보수성을 고민하고 계신 게 보여요.

UserSession의 BaseEntity 상속만 추가하시면 정말 탄탄한 도메인 모델이 될 것 같아요. 실제 서비스 운영하면서 더 많은 경험을 쌓으시면 더욱 성장하실 거예요! 🔥

클라이밍이라는 복잡한 도메인을 이렇게 깔끔하게 정리하신 걸 보니, 도메인 주도 설계(DDD)에 대해서도 공부해보시면 재미있을 것 같아요.


@claude
Copy link

claude bot commented Jul 20, 2025

Claude finished @1117mg's task —— View job


클라이밍 서비스 1차 도메인 설계 리뷰 ✅

안녕하세요! 클라이밍 도메인을 정말 잘 이해하고 설계하신 것 같네요. 전체적으로 봤을 때 꽤 탄탄한 엔티티 구조를 만드셨습니다. 코드를 쭉 살펴보면서 느낀 점들을 솔직하게 말씀드릴게요.

🎯 먼저 정말 잘하신 부분들

BaseEntity 패턴 도입이 정말 좋았어요. createdAt, updatedAt을 공통으로 관리하는 건 실무에서도 당연히 해야 할 패턴이고, Spring Data JPA의 @CreatedDate, @LastModifiedDateAuditingEntityListener를 제대로 활용하신 것도 좋습니다.

JPA 관계 설정도 적절해요. 특히 FetchType.LAZY를 기본으로 쓰신 건 성능 측면에서 매우 좋은 선택이에요. N+1 문제를 미리 방지하려는 의식이 보입니다.

도메인 모델링도 클라이밍의 복잡한 관계를 잘 표현했어요. 사용자 → 세션 → 시도 → 루트미션 → 점수 변화까지의 플로우가 논리적으로 잘 연결되어 있습니다.

인덱스 고려도 좋아요. UserSessionUserMissionAttempt에 복합 인덱스를 미리 설정해두신 것은 성능을 생각한 설계입니다.

❗ 하지만 몇 가지 일관성 문제가 있어요

1. ID 타입 일관성이 좋아요

대부분의 엔티티가 Long id를 사용하고 있어서 확장성 측면에서 좋습니다. Integer는 21억 제한이 있어서 나중에 문제가 될 수 있거든요.

2. UserSession의 필드 설계

@Column(name = "ended_at")  // nullable 허용
private LocalDateTime endedAt;

@Column(name = "total_duration")  // nullable 허용  
private Integer totalDuration;

세션이 진행 중일 때는 endedAt이나 totalDuration이 null인 게 자연스러워 보이는데, 이 부분은 잘 설계하신 것 같아요!

3. SR 기본값 설정

@Column(name = "sr", nullable = false, columnDefinition = "INT DEFAULT 600")
private Integer sr = 600; // User 엔티티

데이터베이스와 애플리케이션 둘 다에서 기본값을 설정하신 것도 안전한 선택입니다.

💡 팀 리뷰를 통한 훌륭한 개선사항들

eric-jy-park님이 지적해주신 부분들과 그에 대한 대응이 정말 좋았어요:

  1. 섹터의 score 필드 제거 - "점수는 개별 루트미션에 한정된 값"이라는 지적이 정확했고, 실제로 제거하신 것도 좋습니다.

  2. 추천 엔티티의 difficulty 중복 제거 - ChallengeRecommendation.getDifficulty() 메서드로 mission에서 가져오도록 한 것이 깔끔해요. 중복 데이터를 제거하면서도 편의성을 유지한 좋은 해결책입니다.

이런 식으로 팀원들과 코드리뷰하면서 중복을 찾아내고 개선해나가는 문화가 정말 멋있어요.

🚀 추가로 고려해볼 점들

1. Tier 이미지 고민사항에 대한 생각

PR에서 고민하고 계신 "티어별 세부 이미지"는 정말 실무에서 자주 마주치는 문제예요. 현재 Tier.imageUrl에 TODO 주석을 달아두신 것처럼, 당장은 현재 구조로 시작하는 게 좋을 것 같아요.

나중에 정말 필요하면:

@Entity
public class TierImage {
    @ManyToOne
    private Tier tier;
    
    @Enumerated(EnumType.STRING)
    private TierImageType type; // MAIN, ACHIEVEMENT, LEVEL_UP 등
    
    private String imageUrl;
}

이런 식으로 확장할 수 있지만, 지금 당장 over-engineering 할 필요는 없어요.

2. 비즈니스 로직 검증 고려

// User의 SR이 해당 Tier 범위에 맞는지 검증
@Min(value = 0, message = "SR은 0 이상이어야 합니다")
@Max(value = 9999, message = "SR은 9999 이하여야 합니다")
private Integer sr = 600;

// Tier에서 srMax는 nullable인데, 최고 티어를 고려한 설계인가요?
@Column(name = "sr_max")
private Integer srMax; // 최고 티어는 상한 없음?

Bean Validation을 추가하면 더 견고해질 것 같아요.

3. 성능 최적화 추가 고려사항

현재 인덱스를 잘 설정해두셨는데, 몇 가지 더 고려해보시면:

// 추가 인덱스 후보들
@Index(name = "idx_mission_gym_sector", columnList = "gym_id, sector_id") // RouteMission
@Index(name = "idx_sr_history_user_date", columnList = "user_id, createdAt") // SRHistory

4. Enum 활용 검토

// difficulty를 String 대신 Enum으로
public enum DifficultyLevel {
    VB("VB"), V0("V0"), V1("V1"), V2("V2"), V3("V3"), 
    V4("V4"), V5("V5"), V6("V6"), V7("V7"), V8("V8+");
    
    private final String displayName;
    // 생성자, getter 등
}

타입 안정성을 확보할 수 있고, 나중에 difficulty별 로직 처리할 때도 편해요.

🎓 주니어 개발자로서 성장 포인트

1. 팀 코드리뷰의 힘

eric-jy-park님과의 리뷰 과정이 정말 좋았어요. 서로 다른 관점에서 코드를 봐주고 중복을 찾아내는 문화가 있으면 코드 품질이 확실히 올라갑니다. 특히 도메인 지식이 필요한 부분에서 팀원의 피드백이 빛났어요.

2. 확장성 vs 단순함의 균형

티어 이미지 문제처럼, "나중에 복잡해질 수 있다"고 해서 처음부터 모든 걸 다 고려할 필요는 없어요. 현재 요구사항에 맞게 단순하게 시작하고, 필요할 때 리팩토링하는 YAGNI 원칙이 중요해요.

3. 성능을 생각한 설계

인덱스를 미리 고려하신 것처럼, 실제 쿼리 패턴을 생각해서 설계하는 습관이 좋습니다. 앞으로 "이 화면에서 어떤 쿼리가 날아갈까?"를 항상 생각해보세요.

4. 데이터 일관성 고민

SR 변경 이력을 SRHistory로 따로 관리하는 것처럼, 중요한 데이터의 변경 추적은 필수입니다. 이런 고민을 하고 계신 게 보기 좋아요.

📝 몇 가지 작은 개선 제안

1. 생성자 체인 고려

// RouteMission 등에서 Builder 패턴 외에 필수 필드만 받는 생성자도 고려
public RouteMission(ClimbingGym gym, Sector sector, String difficulty, Integer score) {
    this.gym = gym;
    this.sector = sector;
    this.difficulty = difficulty;
    this.score = score;
}

2. toString() 메서드 주의

// Lombok @ToString 사용 시 순환 참조 주의
@ToString(exclude = {"attempts", "recommendations"}) // OneToMany 필드 제외

3. 비즈니스 메서드 고려

// User 엔티티에서
public boolean canAttemptMission(RouteMission mission) {
    // SR 조건, 티어 조건 등 비즈니스 로직
    return this.sr >= calculateRequiredSr(mission.getDifficulty());
}

// UserSession 엔티티에서
public void completeSession() {
    this.endedAt = LocalDateTime.now();
    this.totalDuration = calculateDuration();
}

🎯 총평

솔직히 주니어 개발자 분이 이 정도로 클라이밍 도메인을 이해하고 JPA 관계를 설정하신 건 정말 인상적이에요. 특히:

  • 지연 로딩 설정으로 성능 고려
  • 복합 인덱스로 조회 성능 최적화
  • Cascade 관계를 통한 데이터 생명주기 관리
  • 팀 피드백을 통한 중복 제거
  • BaseEntity 패턴으로 공통 필드 관리

이런 부분들에서 성숙한 설계 의식이 보여요.

앞으로 실제 서비스를 운영하면서 더 많은 경험을 쌓으시면 분명 훌륭한 백엔드 개발자가 되실 거예요! 🔥

클라이밍이라는 복잡한 도메인을 이렇게 깔끔하게 정리하신 걸 보니, 도메인 주도 설계(DDD)에 대해서도 공부해보시면 재미있을 것 같습니다.


@eric-jy-park
Copy link
Member

안되겠다 얘 꺼야겠어요 말이 너무 많네

@1117mg 1117mg merged commit cf69049 into main Jul 21, 2025
1 check passed
@1117mg 1117mg deleted the feat/create-entity branch July 30, 2025 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants