Skip to content

feat: Sprint Mission 7 - 나은비#150

Open
nano-mm wants to merge 2 commits into
codeit-bootcamp-spring:나은비from
nano-mm:나은비-sprint7

Hidden character warning

The head ref may contain hidden characters: "\ub098\uc740\ube44-sprint7"
Open

feat: Sprint Mission 7 - 나은비#150
nano-mm wants to merge 2 commits into
codeit-bootcamp-spring:나은비from
nano-mm:나은비-sprint7

Conversation

@nano-mm
Copy link
Copy Markdown

@nano-mm nano-mm commented Mar 30, 2026

요구사항

기본

  • 레포지토리 레이어의 슬라이스 테스트 작성

  • @DataJpaTest를 활용한 테스트 구현

  • 테스트 환경 프로파일 구성 (test)

  • application-test.yaml 생성 및 설정

  • H2 인메모리 데이터베이스 적용 (PostgreSQL 모드)

  • H2 의존성 추가

  • 테스트 시작 시 스키마 자동 생성 설정 (ddl-auto: create)

  • SQL 로그 확인을 위한 로깅 레벨 설정

  • 테스트 실행 시 test 프로파일 활성화

  • JPA Auditing 활성화 (@EnableJpaAuditing)

  • 주요 레포지토리(User, Channel, Message) 테스트 작성

    • 성공 케이스
    • 실패 케이스
  • 커스텀 쿼리 메소드 테스트

  • 페이징 및 정렬 테스트

  • 컨트롤러 레이어의 슬라이스 테스트 작성

  • @WebMvcTest를 활용한 테스트 구현

  • 필요한 Bean을 @Import로 추가

  • 주요 컨트롤러(User, Channel, Message) 테스트 작성

    • 성공 케이스
    • 실패 케이스
  • MockMvc를 활용한 컨트롤러 테스트

  • 서비스 레이어 Mock 처리 (@MockBean)

  • JSON 응답 검증 테스트 포함

심화

  • MDC 기반 로깅 인터셉터 구현
  • Spring Boot Admin 적용
  • JaCoCo 커버리지 설정 및 분석

주요 변경사항

  • 레포지토리 슬라이스 테스트 추가

    • UserRepository, ChannelRepository, MessageRepository 테스트 구현
    • 커스텀 쿼리 및 페이징/정렬 검증
  • 테스트 전용 환경 구성

    • application-test.yaml 생성
    • H2 인메모리 DB + PostgreSQL 호환 모드 설정
  • JPA Auditing 테스트 환경 적용

  • 컨트롤러 슬라이스 테스트 추가

    • @WebMvcTest 기반 API 테스트 구현
    • MockMvc를 활용한 HTTP 요청/응답 검증
    • 서비스 레이어 Mock 처리
  • JSON 응답 검증 로직 추가


스크린샷

image


멘토에게

  • 테스트 코드 하다보니 너무 많은데 실무에서는 보통 테스트 코드를 어느 정도까지 작성하는지와 보통 Ai를 사용하여 작성하는지 궁금합니다.

Copy link
Copy Markdown

@joonfluence joonfluence left a comment

Choose a reason for hiding this comment

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

PR 리뷰 요약

Discodeit Sprint Mission 7 구현으로, JPA 엔티티 전환, 계층별 테스트, 커스텀 예외 체계, MDC 로깅 등 대규모 리팩토링이 잘 구성되어 있습니다. 전반적으로 구조가 좋으나 보안 이슈와 일부 로직 버그가 있어 수정이 필요합니다.

  • P1: 1건 (비밀번호 평문 저장/비교)
  • P2: 2건 (유저 수정 중복체크 버그, ReadStatus 예외 불일치)
  • P3: 4건
  • P4: 1건
  • P5: 1건


User user = userRepository.findByUsername(username)
.orElseThrow(
() -> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p1. 비밀번호 평문 비교 — 보안 취약점

비밀번호를 평문으로 저장하고 equals()로 비교하고 있습니다. DB 유출 시 모든 사용자의 비밀번호가 노출되는 심각한 보안 취약점입니다.

BCryptPasswordEncoder 등을 사용하여 해시 저장 + matches() 비교로 변경해야 합니다. Sprint Mission 범위 상 의도된 것이라면 최소한 // TODO: BCrypt 적용 필요 주석을 남겨주세요.


userRepository.save(user);
log.info("사용자 생성 완료 - userId: {}", user.getId());
return userMapper.toDto(user);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p2. 중복 체크 시 자기 자신 제외 누락

사용자 수정 시 이메일/사용자명 중복 체크에서 자기 자신을 제외하지 않습니다. 예를 들어 username만 변경하고 email은 그대로 유지할 경우, 본인의 기존 email로 UserAlreadyExistsException이 발생합니다.

Suggested change
return userMapper.toDto(user);
if (newEmail != null && !newEmail.equals(user.getEmail()) && userRepository.existsByEmail(newEmail)) {

username 체크도 동일하게 수정이 필요합니다:

if (newUsername != null && !newUsername.equals(user.getUsername()) && userRepository.existsByUsername(newUsername)) {

User user = userRepository.findById(userId)
.orElseThrow(() -> {
log.error("읽음 상태 생성 실패 - 사용자 없음: {}", userId);
return new NoSuchElementException("User with id " + userId + " does not exist");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p2. NoSuchElementException 사용 — 예외 처리 불일치

다른 서비스들은 UserNotFoundException, ChannelNotFoundException 등 커스텀 예외를 사용하는데, 이 서비스만 NoSuchElementException을 던지고 있습니다.

GlobalExceptionHandlerNoSuchElementException 핸들러가 없으므로 500 Internal Server Error로 응답됩니다. 도메인별 커스텀 예외(UserNotFoundException, ChannelNotFoundException)로 통일해 주세요.

);
})
.toList();
BinaryContent nullableProfile = optionalProfileCreateRequest
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p3. @Transactional(readOnly = true) 누락

find() 메서드에 @Transactional이 없습니다. User 엔티티는 profile(LAZY), status(LAZY) 관계를 가지고 있어, 영속성 컨텍스트 종료 후 Lazy 필드에 접근하면 LazyInitializationException이 발생할 수 있습니다.

findAll()은 fetch join을 사용하므로 괜찮을 수 있지만, find()findAll() 모두 @Transactional(readOnly = true) 추가를 권장합니다.

+ "WHERE m.channel.id=:channelId AND m.createdAt < :createdAt")
Slice<Message> findAllByChannelIdWithAuthor(@Param("channelId") UUID channelId,
@Param("createdAt") Instant createdAt,
Pageable pageable);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p3. JPQL에서 LIMIT 키워드 사용

LIMIT 1은 표준 JPQL 문법이 아닙니다. Hibernate 6+에서 지원하긴 하지만, 이식성을 위해 Pageable.ofSize(1)을 파라미터로 받거나, @Query(nativeQuery = true)로 변경하는 것을 권장합니다.

public ResponseEntity<UserStatusDto> updateUserStatusByUserId(@PathVariable("userId") UUID userId,
@Valid @RequestBody UserStatusUpdateRequest request) {
log.info("사용자 상태 수정 요청 - userId: {}", userId);
UserStatusDto updatedUserStatus = userStatusService.updateByUserId(userId, request);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p3. RuntimeException 래핑

파일 업로드 시 IOExceptionRuntimeException으로 감싸면 GlobalExceptionHandler의 일반 Exception 핸들러에 걸려 500 에러로 응답됩니다.

이미 BinaryContentUploadFailedException이 존재하니 이를 활용해 주세요:

Suggested change
UserStatusDto updatedUserStatus = userStatusService.updateByUserId(userId, request);
throw new BinaryContentUploadFailedException(profileFile.getOriginalFilename(), e);

.map(ReadStatus::getChannel)
.map(Channel::getId)
.toList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p3. 채널 삭제 시 첨부파일 스토리지 미정리

메시지와 읽음상태는 삭제하지만, 메시지에 포함된 첨부파일(BinaryContent)의 실제 스토리지 파일은 정리되지 않습니다. DB 레코드는 cascade로 삭제될 수 있지만, 로컬 파일시스템의 파일은 orphan으로 남게 됩니다. 스토리지 정리 로직 추가 또는 후속 배치 작업 등을 고려해 주세요.

"[{}] {} - details: {}",
e.getErrorCode().getCode(),
e.getErrorCode().getMessage(),
e.getDetails()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p4. 중복 핸들러 통합 가능

UserException, ChannelException, MessageException, BinaryContentException 모두 DiscodeitException을 상속한다면, handleDiscodeitException 하나로 통합할 수 있습니다. 현재 동작에 문제는 없지만, 새 도메인 예외 추가 시마다 핸들러를 만들어야 하는 불편함이 있습니다. 통합을 고려해 보세요.

return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
if (userRepository.existsByUsername(username)) {
log.warn("사용자 생성 실패 - 이미 존재하는 사용자명: {}", username);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

p5. UserStatus 명시적 저장 없음

UserStatus를 생성만 하고 userStatusRepository.save()를 호출하지 않습니다. User.statuscascade = CascadeType.ALL에 의존하는 구조인데, UserStatus 생성자에서 양방향 관계 설정(user.setStatus(this))이 되어 있어야 동작합니다. 의도된 cascade 활용이라면 주석을 남겨주시면 좋겠습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants