[이규빈] Sprint7#235
Conversation
-모든 FileRepository 구현체에 ReentrantLock 적용 - SpringDoc OpenAPI 의존성 추가
- UserStatus 업데이트 로직 및 PATCH 메서드 수정 - UserController 엔드포인트 변경 및 RequestBody 적용 - ChannelDto.Response 필드명 수정 (lastMessageAt)
- ChannelController 및 Service의 채널 생성 로직 수정 - UserController의 생성, 수정 메서드 RequestPart 어노테이션 적용 - 제공된 정적 리소스 파일 추가
- MessageController, ReadStatusController, BinaryContentController 로직 수정 - 연관된 Service 및 Entity 비즈니스 로직 수정
- Update 요청 DTO 필드명 변경 - RequestParam 어노테이션에 키(value) 지정 - 프론트엔드 연동 및 API 일관성 확보를 위한 리팩토링
- ErrorResponse 및 BusinessLogicException 기반의 공통 예외 응답 규격 적용 - 서비스 계층 내 하드코딩된 예외를 커스텀 예외로 교체 - BasicChannelService 참여자 목록 동기화 누락 해결 - 미사용 Import 제거 및 코드 최적화 진행
- BasicChannelService 채널 전체 조회 시 lastMessageAt 필드 누락 문제 해결 - 공개 및 비공개 채널의 최근 메시지 시간(Instant) 매핑 로직 적용 - 프라이빗 메서드 추출(getParticipantIds, getLastMessageAt)을 통한 코드 가독성 개선
- 내부 클래스로 관리되던 DTO를 도메인별 개별 클래스로 추출 - 어노테이션을 통한 요청 데이터 유효성 검증 로직 적용 - DTO 클래스 분리에 따른 연관 Service 및 Controller 참조 로직 수정
- build.gradle: Spring Data JPA 및 MapStruct 의존성 추가 - application.yaml: 데이터베이스 연결 및 JPA/SQL 디버깅 로그 설정 추가 - schema.sql: 초기 테이블 생성을 위한 데이터베이스 스키마 정의 파일 추가
- BaseEntity 추상 클래스 생성 및 JPA Auditing 적용 - 엔티티 간 ID 참조 방식을 객체 참조 기반의 연관관계 매핑으로 개편 - ManyToOne, OneToMany 설정 및 영속성 전이(cascade), 고아 객체 제거 적용 - JpaRepository 기반 레포지토리 재작성 및 기존(JCF, File) 구현체 삭제
- BinaryContentStorage 인터페이스 설계 및 로컬 구현체 추가 - ConditionalOnProperty 기반 스토리지 자동 빈 등록 설정 - 파일 다운로드 API 구현 및 ResponseEntity 반환 타입 적용
- Pageable을 활용한 오프셋 방식 페이지네이션 로직 적용 - 공통 응답 규격 PageResponse DTO 도입 및 데이터 직렬화 최적화
- N+1 문제 해결 및 OSIV 비활성화에 따른 쿼리 효율성 개선 - Transactional 경계 설정 및 변경 감지(Dirty Checking) 기반 로직 리팩토링
- ReadStatus 엔티티 업데이트 로직 수정 및 요청 DTO 필드 추가 - Channel 엔티티 (name, description) Nullable 허용 - 파일 다운로드 Content-Disposition 헤더 추가 및 로그인 401 상태 코드 반영 - ErrorResponse 수정
- MessageRepository, BasicMessageService: 페이징 로직 수정 - LocalBinaryContentStorage: 디렉토리 위치 변경
- BaseUpdatableEntity: updateIfChanged 추가 - Channel, User, Message: 변경된 수정 로직 적용 - schema.sql: timestamp with time zone 적용 - frontend 폴더: 정적 리소스와 중복되어 삭제
- application-dev.yml: 개발 환경에 대한 프로필 추가 - application-prod.yml: 운영 환경에 대한 프로필 추가 - logback-spring.xml: 로그 파일 출력 및 롤링 설정
- User, Channel, Message, BinaryContent의 CUD 작업에 INFO 레벨 로깅 적용 - 목록 및 단건 조회(Read) 메서드에 DEBUG 레벨 로깅 일관성 있게 적용
- DiscodeitException 기반의 도메인별 예외 계층 구조(User, Channel 등) 구축 - ErrorResponse 규격에 필드별 상세 오류 정보(details) 및 클래스명(getSimpleName) 반영 - 모든 서비스 레이어에서 예외 발생 시 관련 식별자(ID, Username 등)를 details 맵에 포함하도록 개선
- Valid를 활용한 검증 로직 고도화 - GlobalExceptionHandler: - MethodArgumentNotValidException 처리 로직 추가 - ConstraintViolationException 처리 로직 추가
- 프론트엔드 정적리소스 업데이트 - BinaryContentException을 상속받지 않던 예외들 수정
- Spring Boot Actuator 의존성 추가 및 엔드포인트 노출 설정 - application.yml에 앱 이름, 버전, 자바/스프링 버전 및 주요 설정 정보(info) 추가 - MessageController에서 첨부파일(attachments)이 null일 때 .size() 호출로 인한 NPE 발생 로직 수정
- UserService: create, update, delete 성공/실패 케이스 작성 - ChannelService: 공개/비공개 채널 생성 및 수정/삭제/조회 로직 검증 - MessageService: 첨부파일 포함 생성, 비공개 채널 권한 체크, 커서 기반 페이징 로직 검증
- 비공개 채널 생성 로직 수정 - 존재하지 않는 참여자 ID가 포함된 경우 무시하지 않고 즉시 예외를 발생시키도록 변경 - 메시지 목록 조회(findAllByChannelId) 로직 개선 - 조회 전 채널 존재 여부를 확인하는 방어 로직 추가 및 관련 실패 테스트 케이스 작성 - 애플리케이션 로깅 및 예외 처리 레벨 최적화 - 컨트롤러와 서비스 계층 간의 중복된 로그 제거 - DTO 유효성 검증(Validation) 강화 - UserCreateRequest: email 필드에 null 및 공백 방지를 위한 NotBlank 추가 - PrivateChannelCreateRequest: 참가자 리스트 내부 UUID에 대한 NotNull 검증 추가
- MDCLoggingInterceptor: - 요청 진입 시 UUID, HTTP 메소드, URI 정보를 MDC에 저장 - 응답 헤더에 'Discodeit-Request-ID' 추가 - WebMvcConfig: 애플리케이션의 모든 API 경로에 MDC 인터셉터 동작 등록 - logback-spring.xml: 출력 패턴 변경 - schema.sql: timestamp with time zone이 누락된 부분 수정
- Admin 서버 모듈 추가: - admin 모듈 구성 및 settings.gradle 등록 - EnableAdminServer 적용 및 9090 포트 실행 - Discodeit: - build.gradle: spring-boot-admin-starter-client 의존성 추가
- build.gradle에 jacoco 플러그인 도입
- DataJpaTest를 이용한 리포지토리 계층 슬라이스 테스트 구현 - WebMvcTest를 이용한 컨트롤러 슬라이스 테스트 구현 - JPA Auditing 설정을 JpaAuditingConfig로 분리
- - User, Channel, Message 주요 API 통합 테스트 작성 - 기존 테스트 코드 불필요한 import 제거
joonfluence
left a comment
There was a problem hiding this comment.
PR 리뷰: [이규빈] Sprint7
전체 요약
프로파일 기반 설정 분리, Logback 로깅/MDC 인터셉터, 커스텀 예외 계층 설계, Spring Validation, Actuator, Spring Boot Admin, JaCoCo, 그리고 단위/슬라이스/통합 테스트를 망라한 스프린트7 구현입니다. MDCLoggingInterceptor와 DiscodeitException 계층 설계, 테스트 코드 구조가 전반적으로 우수합니다.
👍 잘한 점
DiscodeitException의 생성자 오버로드(errorCode,errorCode+details,errorCode+cause,errorCode+details+cause)가 다양한 상황에 대응할 수 있어 확장성이 좋습니다.MDCLoggingInterceptor가afterCompletion에서MDC.clear()를 호출해 ThreadLocal 메모리 누수를 방지하는 점이 훌륭합니다.- 통합 테스트에서
@Transactional을 사용해 각 테스트가 독립적으로 실행되고, DB 상태 검증까지 철저히 하고 있습니다.
[P3] UserRepositoryTest 누락
테스트 디렉터리에 ChannelRepositoryTest, MessageRepositoryTest는 있지만 UserRepositoryTest.java가 없습니다. 요구사항에 주요 레포지토리(User, Channel, Message) 모두에 대한 슬라이스 테스트를 작성하도록 명시되어 있으니 추가해 주세요.
| datasource: | ||
| url: jdbc:postgresql://localhost:5432/discodeit | ||
| username: discodeit_user | ||
| password: discodeit1234 |
There was a problem hiding this comment.
[P1] 운영 DB 자격증명 하드코딩 — 반드시 수정 필요
운영 환경 DB의 username과 password가 소스 코드에 직접 노출되어 있습니다. Git 히스토리에 영구적으로 남으므로 나중에 삭제해도 완전히 제거되지 않습니다. 환경 변수로 주입받도록 변경해주세요.
| password: discodeit1234 | |
| username: ${DB_USERNAME} | |
| password: ${DB_PASSWORD} |
| } | ||
| @ExceptionHandler | ||
| public ResponseEntity<ErrorResponse> handleBusinessLogicException(DiscodeitException e) { | ||
| log.warn("비즈니스 예외 발생 - code: {}, message: {}, details: {}", |
There was a problem hiding this comment.
[P2] 비즈니스 예외에 스택 트레이스 과도 출력
SLF4J에서 마지막 인수가 Throwable이면 자동으로 스택 트레이스를 출력합니다. UserNotFoundException, ChannelNotFoundException 등 예상된 비즈니스 예외가 발생할 때마다 스택 트레이스 전체가 출력되어 운영 로그가 오염됩니다. 비즈니스 예외는 코드/메시지 정도만 로깅하는 것이 적절합니다.
| log.warn("비즈니스 예외 발생 - code: {}, message: {}, details: {}", | |
| log.warn("비즈니스 예외 발생 - code: {}, message: {}, details: {}", | |
| e.getErrorCode().name(), e.getMessage(), e.getDetails()); |
| </encoder> | ||
| </appender> | ||
|
|
||
| <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
There was a problem hiding this comment.
[P3] RollingFileAppender에 <file> 태그 누락
<file> 태그가 없으면 현재 날짜의 로그가 날짜 패턴 파일로만 기록되고 '현재 로그 파일'이 존재하지 않습니다. tail -f .logs/app.log 처럼 오늘 로그를 실시간 추적하는 운영 작업이 불편해집니다. 아래와 같이 활성 파일을 지정해 주세요.
| <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> | |
| <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> | |
| <file>${LOG_DIR}/app.log</file> | |
| <encoder> |
| annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' | ||
| annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' | ||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' |
There was a problem hiding this comment.
[P3] testImplementation 의존성 중복 선언
org.springframework.boot:spring-boot-starter-test가 바로 위 줄과 동일하게 두 번 선언되어 있습니다. 한 줄을 제거해주세요.
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | |
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' |
| } | ||
|
|
||
| ext { | ||
| set('springBootAdminVersion', "3.5.8") |
There was a problem hiding this comment.
[P3] Spring Boot Admin 서버-클라이언트 버전 불일치
admin 모듈은 Spring Boot Admin 3.5.8을 사용하지만, discodeit 메인 모듈의 build.gradle에서는 클라이언트를 de.codecentric:spring-boot-admin-starter-client:3.4.5로 직접 고정해 버전이 다릅니다. 마이너 버전 차이도 API 비호환을 유발할 수 있으니 동일한 버전으로 맞춰 주세요.
| @Override | ||
| public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | ||
| throws Exception { | ||
| String requestId = UUID.randomUUID().toString().substring(0, 8); |
There was a problem hiding this comment.
[P4] request_id로 UUID 앞 8자리만 사용
substring(0, 8)로 앞 8자리(약 43억 가지)만 사용합니다. 트래픽이 높은 운영 환경에서 충돌 가능성이 있습니다. 전체 UUID를 사용하면 충돌 확률이 사실상 0에 수렴합니다.
| String requestId = UUID.randomUUID().toString().substring(0, 8); | |
| String requestId = UUID.randomUUID().toString(); |
|
|
||
| @Getter | ||
| public enum ErrorCode { | ||
| USER_NOT_FOUND(404, "유저를 찾을 수 없습니다."), |
There was a problem hiding this comment.
[P4] HTTP 상태 코드를 int 리터럴로 직접 관리
404, 409, 401 등 숫자를 직접 사용하면 실수가 발생하기 쉽습니다. HttpStatus enum을 사용하면 타입 안전성과 가독성이 높아집니다. 현재 동작에는 문제없으나 참고해보세요.
프로젝트 마일스톤
베이스 코드
이전 미션에서 아직 달성하지 못한 요구사항이 있어 이 미션을 수행하기 어렵다면 베이스 코드를 참고해보세요.
주요 변경 사항
H2 데이터베이스와 호환을 고려해 schema.sql이 변경되었습니다.
timestamptz→timestamp with time zone프론트엔드가 변경되었습니다.
v1.2.3예외 메시지 표시 모달 UI가 개선되었습니다.
한글 메시지 입력 시 중복 전송되는 버그가 픽스되었습니다.
정적리소스: 베이스 코드에 적용되어 있습니다.
프론트엔드 소스 코드는 참고용으로만 활용하세요. 수정하여 활용하는 경우 이어지는 요구사항 또는 미션을 수행하는 데 어려움이 있을 수 있습니다.
기본 요구사항
프로파일 기반 설정 관리
application-dev.yml,application-prod.yml파일을 생성하세요.로그 관리
@Slf4j어노테이션을 활용해 로깅을 쉽게 추가할 수 있도록 구성하세요.application.yaml에 기본 로깅 레벨을 설정하세요.info레벨로 설정합니다.debug, 운영 환경에서는info레벨로 설정합니다.logback-spring.xml파일을 생성하세요.다음 예시와 같은 로그 메시지를 출력하기 위한 로깅 패턴과 출력 방식을 커스터마이징하세요.
로그 출력 예시
콘솔과 파일에 동시에 로그를 기록하도록 설정하세요.
{프로젝트 루트}/.logs경로에 저장되도록 설정하세요.로그 파일은 일자별로 롤링되도록 구성하세요.
로그 파일은 30일간 보관하도록 구성하세요.
예외 처리 고도화
커스텀 예외를 설계하고 구현하세요.
패키지명:
com.sprint.mission.discodeit.exception[.{도메인}]ErrorCodeEnum 클래스를 통해 예외 코드명과 메시지를 정의하세요.아래는 예시입니다. 필요하다고 판단되는 다양한 코드를 정의하세요.
예시
모든 예외의 기본이 되는
DiscodeitException클래스를 정의하세요.클래스 다이어그램
details는 예외 발생 상황에 대한 추가정보를 저장하기 위한 속성입니다.DiscodeitException을 상속하는 주요 도메인 별 메인 예외 클래스를 정의하세요.UserException,ChannelException등도메인 메인 예외 클래스를 상속하는 구체적인 예외 클래스를 정의하세요.
UserNotFoundException,UserAlreadyExistException등 필요한 예외를 정의하세요.예시
기존에 구현했던 예외를 커스텀 예외로 대체하세요.
NoSuchElementExceptionIllegalArgumentExceptionErrorResponse를 통해 일관된 예외 응답을 정의하세요.클래스 다이어그램
int status: HTTP 상태코드String exceptionType: 발생한 예외의 클래스 이름앞서 정의한
ErrorResponse와@RestControllerAdvice를 활용해 예외를 처리하는 예외 핸들러를 구현하세요.ErrorResponse)을 가져야 합니다.유효성 검사
@NotNull,@NotBlank,@Size,@Email등@Valid를 사용해 요청 데이터를 검증하세요.MethodArgumentNotValidException을 전역 예외 핸들러에서 처리하세요.Actuator
Discodeit1.7.0173.4.0/actuator/info/actuator/metrics/actuator/health/actuator/loggers단위 테스트
Mockito를 활용해 Repository 의존성을 모의(mock)하세요.BDDMockito를 활용해 테스트 가독성을 높이세요.슬라이스 테스트
@DataJpaTest를 활용해 테스트를 구현하세요.application-test.yml을 생성하세요.test프로파일을 활성화 하세요.@EnableJpaAuditing을 추가하세요.@WebMvcTest를 활용해 테스트를 구현하세요.WebMvcTest에서 자동으로 등록되지 않는 유형의 Bean이 필요하다면@Import를 활용해 추가하세요.예시
주요 컨트롤러(User, Channel, Message)에 대해 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
MockMvc를 활용해 컨트롤러를 테스트하세요.
서비스 레이어를 모의(mock)하여 컨트롤러 로직만 테스트하세요.
JSON 응답을 검증하는 테스트를 포함하세요.
통합 테스트
@SpringBootTest를 활용해 Spring 애플리케이션 컨텍스트를 로드하세요.@Transactional을 활용해 독립적으로 실행하세요.심화 요구사항
MDC를 활용한 로깅 고도화
MDCLoggingInterceptorcom.**.discodeit.configDiscodeit-Request-IDWebMvcConfigurer를 통해MDCLoggingInterceptor를 등록하세요.WebMvcConfigcom.**.discodeit.config로그 출력 예시
Spring Boot Admin을 활용한 메트릭 가시화
Spring Boot Admin 서버를 구현할 모듈을 생성하세요.
IntelliJ 화면 참고
모듈 정보는 다음과 같습니다.
의존성
admin모듈의 메인 클래스에@EnableAdminServer어노테이션을 추가하고, 서버는 9090번 포트로 설정합니다.admin서버 실행 후 localhost:9090/applications 에 접속해봅니다.discodeit 프로젝트에 Spring Boot Admin Client를 적용합니다.
의존성을 추가합니다.
dependencies { ... implementation 'de.codecentric:spring-boot-admin-starter-client:3.4.5 }admin 서버에 등록될 수 있도록 설정 정보를 추가합니다.
discodeit 서버를 실행하고, admin 대시보드에 discodeit 인스턴스가 추가되었는지 확인합니다.
admin 대시보드 화면을 조작해보면서 각종 메트릭 정보를 확인해보세요.
테스트 커버리지 관리
JaCoCo 플러그인을 추가하세요.
테스트 실행 후 생성된 리포트를 분석해보세요.
build/reports/jacoco경로에서 확인할 수 있습니다.com.sprint.mission.discodeit.service.basic패키지에 대해서 60% 이상의 코드 커버리지를 달성하세요.변경사항
멘토에게