-
Notifications
You must be signed in to change notification settings - Fork 24
[안여경] sprint7 #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 안여경
Are you sure you want to change the base?
The head ref may contain hidden characters: "\uC548\uC5EC\uACBD-sprint7"
[안여경] sprint7 #192
Conversation
() -> new ResourceNotFoundException("channelId = " + createRequest.channelId())); | ||
|
||
/* 비공개 채널이면 -> 유저가 해당 채널에 있는지 validation check */ | ||
if (this.channelRepository.findById(createRequest.channelId()).get().getType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
channelRepository.findById()가 Optional을 반환하네요.
그러면 null이 존재할 가능성도 고려해서 코드 작성하셔야 할거 같아요.
바로 get() 하시면 NullpointerException이 발생할 가능성이 있어보입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아. 위에 56번째 줄에서 이미 하고 있군요. https://github.com/codeit-bootcamp-spring/3-sprint-mission/pull/192/files#diff-7a57ded53b972aa54e4115b0531fdd0ce6fb32587e8d60c7b98b5422314ae008L56
그런데 이렇게 하면 조회를 두번하게 되네요.
if 문 내부를 아래 처럼 바꾸셔야 될 것 같아요.
if (channel.getType() == ChannelType.PRIVATE) {
|
||
given(channelService.create(request)).willReturn(createdDto); | ||
|
||
// when & then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
작성하기 편해서 when 부분이랑 then 부분을 같이 작성하신 것 같아요.
하지만 저는 이렇게 작성하면 코드 가독성이 떨어지고 실수하기 쉽다고 생각합니다.
when이랑 then이랑 명확하게 구분하는게 미래에 이 코드를 읽거나 추가하는 사람이 실수할 여지가 적어질 것 같아요.
// When
ResultActions result = mockMvc.perform(post("/api/channels/private")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)));
// Then
result.andExpect(status().isCreated())
.andExpect(jsonPath("$.type").value("PRIVATE"));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가로 좀 더 가독성을 높히는 방법으로 given, when, then 각 절 내부에서는 빈 줄바꿈을 두지 않고 절 구분에만 빈 줄바꿈을 두는 게 좋습니다.
// given
List<UUID> memberIds = List.of(UUID.randomUUID());
PrivateChannelCreateRequest request = new PrivateChannelCreateRequest(memberIds);
ChannelDto createdDto = new ChannelDto(
UUID.randomUUID(),
ChannelType.PRIVATE,
null,
null,
List.of(), // 참여자 정보 생략
Instant.now()
);
given(channelService.create(request)).willReturn(createdDto);
<- (여기에만 빈 줄바꿈)
// when
ResultActions result = mockMvc.perform(post("/api/channels/private")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)));
<- (여기에만 빈 줄바꿈)
// then
result.andExpect(status().isCreated())
.andExpect(jsonPath("$.type").value("PRIVATE"));
public record PrivateChannelCreateRequest( | ||
@NotNull | ||
List<UUID> participantIds, | ||
UUID ownerId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아래 주석 보니 ownerId가 없는 경우에는 default value로 설정된다고 되어있는데,
오너가 없는 private 채널이 되는건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ownerId는 원래 요구사항에 없었는데, 제가 필요할것같아서 추가로 넣은것입니다. 근데 계속 개발하다보니 아직까지 필요한 부분이 없어서 임시로 빈값을 넣어주고있습니다.
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> | ||
<!-- 일별 로그 파일 --> | ||
<fileNamePattern>./logs/app.%d{yyyy-MM-dd}.log</fileNamePattern> | ||
<maxHistory>30</maxHistory> <!-- 30일 보관 --> | ||
</rollingPolicy> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로깅 파일을 시간단위로 롤링하는 정책은 현업에서도 많이 쓰고 굉장히 잘 하신것 같아요.
하지만 파일이 너무 커져서 문제가 되는 경우도 있거든요.
그래서 보통은 최대 파일크기도 같이 설정으로 걸어둬요.
아래 파일 크기 + 시간 주기로 롤링하는 정책입니다.
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize> <!-- 파일 하나가 100MB 넘으면 새로 만듦 -->
<maxHistory>30</maxHistory> <!-- 최대 30일 보관 -->
<totalSizeCap>5GB</totalSizeCap> <!-- 전체 합이 5GB 넘으면 오래된 것부터 삭제 -->
</rollingPolicy>
<encoder>
<pattern>${ROLLING_PATTERN}</pattern>
</encoder>
</appender>
|
||
// 메시지 생성 요청 준비 | ||
MessageCreateRequest createRequest = new MessageCreateRequest("Hello world", userId, | ||
channel.getId()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 결과랑은 상관 없을것 같긴한데, channel.getId()의 값이 여기에서 null 일것 같네요.
|
||
// Then | ||
|
||
assertNotNull(lastMessageAt); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
messageRepository 구현체는 조회 결과를 항상 Optional로 감싸서 반환해서 항상 not null 일것 같은데 이걸 테스트할 필요가 있을까요?
제 생각에 아래 같은 코드를 기대하신거 같은데, 맞나요?
assertTrue(lastMessageAt.isPresent());
전체적으로 현업 코드에서 작성하는 코드와 거의 비슷하다고 느껴지고 아주 잘하셨다 생각합니다. 딱히 남길만한 코멘트가 없어서 코드 컨벤션 관련해서 조금 남겼습니다. |
….yaml && restructure application.yaml
@ssjf409 리뷰감사합니다 :) 감사합니다 😊 |
기본 요구사항
프로파일 기반 설정 관리
application-dev.yaml
,application-prod.yaml
파일을 생성하세요.로그 관리
Lombok의
@Slf4j
어노테이션을 활용해 로깅을 쉽게 추가할 수 있도록 구성하세요.[ ]
application.yaml
에 기본 로깅 레벨을 설정하세요.기본적으로
info
레벨로 설정합니다.환경 별 적절한 로깅 레벨을 프로파일 별로 설정해보세요.
SQL 로그를 보기위해 설정했던 레벨은 유지합니다.
우리가 작성한 프로젝트의 로그는 개발 환경에서
debug
, 운영 환경에서는info
레벨로 설정합니다.Spring Boot의 기본 로깅 구현체인 Logback의 설정 파일을 구성하세요.
logback-spring.xml
파일을 생성하세요.다음 예시와 같은 로그 메시지를 출력하기 위한 로깅 패턴과 출력 방식을 커스터마이징하세요.
로그 출력 예시
콘솔과 파일에 동시에 로그를 기록하도록 설정하세요.
파일은
{프로젝트 루트}/.logs
경로에 저장되도록 설정하세요.로그 파일은 일자별로 롤링되도록 구성하세요.
로그 파일은 30일간 보관하도록 구성하세요.
서비스 레이어와 컨트롤러 레이어의 주요 메소드에 로깅을 추가하세요.
로깅 레벨을 적절히 사용하세요: ERROR, WARN, INFO, DEBUG
다음과 같은 메소드에 로깅을 추가하세요:
사용자 생성/수정/삭제
채널 생성/수정/삭제
메시지 생성/수정/삭제
파일 업로드/다운로드
예외 처리 고도화
com.sprint.mission.discodeit.exception[.{도메인}]
ErrorCode
Enum 클래스를 통해 예외 코드명과 메시지를 정의하세요.6ag3lzl9i-image.png
DiscodeitException
클래스를 정의하세요.j5vtp941a-image.png
details
는 예외 발생 상황에 대한 추가정보를 저장하기 위한 속성입니다.DiscodeitException
을 상속하는 주요 도메인 별 메인 예외 클래스를 정의하세요.UserException
,ChannelException
등UserNotFoundException
,UserAlreadyExistException
등 필요한 예외를 정의하세요.a6f585icy-image.png
기존에 구현했던 예외를 커스텀 예외로 대체하세요.
NoSuchElementException
IllegalArgumentException
…
ErrorResponse
를 통해 일관된 예외 응답을 정의하세요.클래스 다이어그램
3gqeampkw-image.png
int status
: HTTP 상태코드String exceptionType
: 발생한 예외의 클래스 이름앞서 정의한
ErrorResponse
와@RestControllerAdvice
를 활용해 예외를 처리하는 예외 핸들러를 구현하세요.모든 핸들러는 일관된 응답(
ErrorResponse
)을 가져야 합니다.유효성 검사
@NotNull
,@NotBlank
,@Size
,@Email
등@Valid
를 사용해 요청 데이터를 검증하세요.MethodArgumentNotValidException
을 전역 예외 핸들러에서 처리하세요.Actuator
Discodeit
1.7.0
17
3.4.0
/actuator/info
/actuator/metrics
/actuator/health
/actuator/loggers
단위 테스트
Mockito
를 활용해 Repository 의존성을 모의(mock)하세요.BDDMockito
를 활용해 테스트 가독성을 높이세요.슬라이스 테스트
레포지토리 레이어의 슬라이스 테스트를 작성하세요.
[ ]
@DataJpaTest
를 활용해 테스트를 구현하세요.테스트 환경을 구성하는 프로파일을 구성하세요.
[ ]
application-test.yaml
을 생성하세요.데이터소스는 H2 인메모리 데이터 베이스를 사용하고, PostgreSQL 호환 모드로 설정하세요.
H2 데이터베이스를 위해 필요한 의존성을 추가하세요.
테스트 시작 시 스키마를 새로 생성하도록 설정하세요.
디버깅에 용이하도록 로그 레벨을 적절히 설정하세요.
테스트 실행 간
test
프로파일을 활성화 하세요.JPA Audit 기능을 활성화 하기 위해 테스트 클래스에
@EnableJpaAuditing
을 추가하세요.주요 레포지토리(User, Channel, Message)의 주요 쿼리 메소드에 대해 각각 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
커스텀 쿼리 메소드
페이징 및 정렬 메소드
컨트롤러 레이어의 슬라이스 테스트를 작성하세요.
@WebMvcTest
를 활용해 테스트를 구현하세요.WebMvcTest
에서 자동으로 등록되지 않는 유형의 Bean이 필요하다면@Import
를 활용해 추가하세요.예시
주요 컨트롤러(User, Channel, Message)에 대해 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
MockMvc를 활용해 컨트롤러를 테스트하세요.
서비스 레이어를 모의(mock)하여 컨트롤러 로직만 테스트하세요.
JSON 응답을 검증하는 테스트를 포함하세요.
통합 테스트
@SpringBootTest
를 활용해 Spring 애플리케이션 컨텍스트를 로드하세요.@Transactional
을 활용해 독립적으로 실행하세요.심화 요구사항
MDC를 활용한 로깅 고도화
요청 ID, 요청 URL, 요청 방식 등의 정보를 MDC에 추가하는 인터셉터를 구현하세요.
클래스명:
MDCLoggingInterceptor
패키지명:
com.**.discodeit.config
요청 ID는 랜덤한 문자열로 생성합니다. (UUID)
요청 ID는 응답 헤더에 포함시켜 더 많은 분석이 가능하도록 합니다.
헤더 이름:
Discodeit-Request-ID
[ ]
WebMvcConfigurer
를 통해MDCLoggingInterceptor
를 등록하세요.클래스명:
WebMvcConfig
패키지명:
com.**.discodeit.config
Logback 패턴에 MDC 값을 포함시키세요.
로그 출력 예시
Spring Boot Admin을 활용한 메트릭 가시화
Spring Boot Admin 서버를 구현할 모듈을 생성하세요.
IntelliJ 화면 참고
9l6b0q2dn-image.png
모듈 정보는 다음과 같습니다.
b8812d4su-image.png
의존성
zjh3frl0m-image.png
admin
모듈의 메인 클래스에@EnableAdminServer
어노테이션을 추가하고, 서버는 9090번 포트로 설정합니다.admin
서버 실행 후 localhost:9090/applications 에 접속해봅니다.discodeit 프로젝트에 Spring Boot Admin Client를 적용합니다.
의존성을 추가합니다.
admin 서버에 등록될 수 있도록 설정 정보를 추가합니다.
discodeit 서버를 실행하고, admin 대시보드에 discodeit 인스턴스가 추가되었는지 확인합니다.
admin 대시보드 화면을 조작해보면서 각종 메트릭 정보를 확인해보세요.
주요 API의 요청 횟수, 응답시간 등
서비스 정보
테스트 커버리지 관리
JaCoCo 플러그인을 추가하세요.
테스트 실행 후 생성된 리포트를 분석해보세요.
리포트는
build/reports/jacoco
경로에서 확인할 수 있습니다.com.sprint.mission.discodeit.service.basic
패키지에 대해서 60% 이상의 코드 커버리지를 달성하세요.