-
Notifications
You must be signed in to change notification settings - Fork 37
[김지협] sprint7 #266
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: part2-김지협
Are you sure you want to change the base?
The head ref may contain hidden characters: "part3-\uAE40\uC9C0\uD611-sprint7"
[김지협] sprint7 #266
Conversation
지협님, 안녕하세요. PR 업데이트 해주시면 감사하겠습니다! |
public ResponseEntity<MessageDto> update(@PathVariable("messageId") UUID messageId, | ||
@RequestBody MessageUpdateRequest request) { | ||
MessageDto updatedMessage = messageService.update(messageId, request); | ||
return ResponseEntity |
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.
아래도 동일한 동작이니, 참고하시면 좋을 것 같습니다!
ResponseEntity.ok(updatedMessage);
@RequestMapping("/api/users") | ||
public class UserController implements UserApi { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(UserController.class); |
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.
lombok을 사용하고 계신다면, LoggerFactory와 동일한 기능을 간단하게 설정할 수 있는데요.
아래와 같이 class레벨에 @slf4j 어노테이션을 추가해주시면 됩니다.
@Slf4j
public class XXXController {
@GetMapping
public Response getResources() {
log.info("");
log.debug("");
}
}
@RequestPart("userCreateRequest") @Valid UserCreateRequest userCreateRequest, | ||
@RequestPart(value = "profile", required = false) MultipartFile profile | ||
) { | ||
log.info("Attempting to create user with data: {}", userCreateRequest); |
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.
로그 잘 남겨주셨네요! 관련해서 slf4j로 로그를 남기는 이유를 좀 더 말씀드려보겠습니다!
1. 먼저, 로그를 레벨로 관리할 수 있습니다.
- 로그에는
trace < debug < info < warn < error
순으로 레벨이 존재하는데요. - 따라서, 로그 설정으로 아래와 같이 출력을 제한할 수 있습니다.
- 예:
logging.level.root=INFO
→trace
,debug
로그는 출력되지 않음
- 예:
- 보통 개발 환경은
debug
, 운영 환경은 이슈를 파악할 수 있게warn
이상으로 출력을 설정합니다.
그 외에
- 로깅 구현체와 분리할 수 있다는 장점과
- 로그 파일, 일자별 분리 등 설정을 할 수 있다는 점. 이는 ELK과 같은 모니터링 툴과 연동하여 디버깅 / 알람에 활용할 수 있습니다.
마지막으로 System.out.println은 사용하면 안되는 이유를 말씀드리면,
- slf4j와 다르게
로그 레벨 구분이 불가
하여 개발/운영환경 모두 동일하게 로그가 남습니다. 파일 저장 및 연동이 불가
하여 모니터링 툴과 연동하는 것이 어렵습니다.- 마지막으로 Blocking I/O로 다량 출력 시
성능 저하
가 있을 수 있습니다. (SLF4J는 Async 로깅도 가능)
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.
추가로 request/response에 대한 로그를 분리하는 법 공유드립니다.
- 아래와 같이 filter를 설정하신다면, 모든 request/response의 값을 추상화하여 로깅할 수 있습니다.
- 따라서, 코드 구현체 부분에서는 비즈니스 로직과 관련된 로그만 남길 수 있기에 코드 가독성이 좋아지는 장점이 있습니다.
@Slf4j
@Component
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
long start = System.currentTimeMillis();
chain.doFilter(requestWrapper, responseWrapper);
long end = System.currentTimeMillis();
log.info("\n" +
"[REQUEST] {} - {} {} - {}\n" +
"Headers : {}\n" +
"Request : {}\n" +
"Response : {}\n",
((HttpServletRequest) request).getMethod(),
((HttpServletRequest) request).getRequestURI(),
responseWrapper.getStatus(),
(end - start) / 1000.0,
getHeaders((HttpServletRequest) request),
getRequestBody(requestWrapper),
getResponseBody(responseWrapper));
}
private Map getHeaders(HttpServletRequest request) {
Map headerMap = new HashMap<>();
Enumeration headerArray = request.getHeaderNames();
while (headerArray.hasMoreElements()) {
String headerName = (String) headerArray.nextElement();
headerMap.put(headerName, request.getHeader(headerName));
}
return headerMap;
}
private String getRequestBody(ContentCachingRequestWrapper request) {
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
try {
return new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return " - ";
}
}
}
return " - ";
}
private String getResponseBody(final HttpServletResponse response) throws IOException {
String payload = null;
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
wrapper.copyBodyToResponse();
}
}
return null == payload ? " - " : payload;
}
}
this(null, errorCode, null); | ||
} | ||
|
||
public DiscodeitException(ErrorCode errorCode, Throwable cause) { |
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.
데이터 초기화 목적에 맞게 여러 생성자 잘 만들어주셨네요!
public enum ErrorCode { | ||
|
||
//User 에러 | ||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "유저를 찾을 수 없습니다"), |
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.
ErrorCode안에 HttpStatus코드까지 잘 넣어주셨습니다!
ControllerAdvice에서 중복된 코드를 줄일 수 잇을 것으로 기대됩니다!
.body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler(Exception.class) |
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.
마지막으로 Exception 처리까지 잘 해주셨네요!
|
||
public interface MessageRepository extends JpaRepository<Message, UUID> { | ||
|
||
@Query("SELECT m FROM Message m " |
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.
자바15부터 text block을 지원해서 아래 처럼도 코드 작성이 가능하니, 참고해주세요!
@Query("""
SELECT m FROM Message m
LEFT JOIN FETCH m.author a
JOIN FETCH a.status
LEFT JOIN FETCH a.profile
WHERE m.channel.id = :channelId AND m.createdAt < :createdAt
""")
driver-class-name: org.postgresql.Driver | ||
url: jdbc:postgresql://localhost:5432/discodeit | ||
username: discodeit_user | ||
password: discodeit1234 |
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.
민감정보 잘 숨겨주셨는데요.
데이터베이스 비밀번호도 추가로 환경변수로 설정해주시면 좋을 것 같습니다!
아래는 IntelliJ에서 값을 설정하는 법 공유드립ㄹ니다.
- Run → Edit Configurations... 클릭
- 환경변수 입력 칸(Environment variables)에 값 추가:
DB_PASSWORD=discodeit1234;SPRING_BOOT_ADMIN_CLIENT_URL=http://localhost:8081
|
||
import static org.assertj.core.api.Assertions.*; | ||
|
||
@DataJpaTest |
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.
데이터 롤백될 수 있도록 잘 설정해주셨네요!
//create(PUBLIC) | ||
@Test | ||
void givenPublicRequest_whenCreate_thenReturnsDto() { | ||
var req = new PublicChannelCreateRequest("Announcements", "All users"); |
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부분에서 데이터를 설정하실 때, 반복되는 작업으로 어려움이 있으실 것 같아요.
저 같은 경우는 dataset를 하나의 파일에서 가져다 사용했는데요.
findOne, findAll과 같이 1개의 Entity와 Entity List를 조회할 수 있도록 했습니다.
위와 같이 진행하신다면, 중복되는 작업을 해소할 수 있을 것 같아요!
기본 요구사항
프로파일 기반 설정 관리
[x] 개발, 운영 환경에 대한 프로파일을 구성하세요.
[x] application-dev.yaml, application-prod.yaml 파일을 생성하세요.
[x] 다음과 같은 설정값을 프로파일별로 분리하세요.
[x] 데이터베이스 연결 정보
[x] 서버 포트
로그 관리
[x] Lombok의 @slf4j 어노테이션을 활용해 로깅을 쉽게 추가할 수 있도록 구성하세요.
[x] application.yaml에 기본 로깅 레벨을 설정하세요.
기본적으로 info 레벨로 설정합니다.
[x] 환경 별 적절한 로깅 레벨을 프로파일 별로 설정해보세요.
SQL 로그를 보기위해 설정했던 레벨은 유지합니다.
우리가 작성한 프로젝트의 로그는 개발 환경에서 debug, 운영 환경에서는 info 레벨로 설정합니다.
[x] Spring Boot의 기본 로깅 구현체인 Logback의 설정 파일을 구성하세요.
[x] logback-spring.xml 파일을 생성하세요.
[x] 다음 예시와 같은 로그 메시지를 출력하기 위한 로깅 패턴과 출력 방식을 커스터마이징하세요.
로그 출력 예시
패턴
{년}-{월}-{일} {시}:{분}:{초}:{밀리초} [{스레드명}] {로그 레벨(5글자로 맞춤)} {로거 이름(최대 36글자)} - {로그 메시지}{줄바꿈}
예시
25-01-01 10:33:55.740 [main] DEBUG c.s.m.discodeit.DiscodeitApplication - Running with Spring Boot v3.4.0, Spring v6.2.0
[x] 콘솔과 파일에 동시에 로그를 기록하도록 설정하세요.
[x] 파일은 {프로젝트 루트}/.logs 경로에 저장되도록 설정하세요.
[x] 로그 파일은 일자별로 롤링되도록 구성하세요.
[x] 로그 파일은 30일간 보관하도록 구성하세요.
[x] 서비스 레이어와 컨트롤러 레이어의 주요 메소드에 로깅을 추가하세요.
[x] 로깅 레벨을 적절히 사용하세요: ERROR, WARN, INFO, DEBUG
[x] 다음과 같은 메소드에 로깅을 추가하세요:
[x] 사용자 생성/수정/삭제
[x] 채널 생성/수정/삭제
[x] 메시지 생성/수정/삭제
[x] 파일 업로드/다운로드
예외 처리 고도화
[x] 커스텀 예외를 설계하고 구현하세요.
패키지명: com.sprint.mission.discodeit.exception[.{도메인}]
[x] ErrorCode Enum 클래스를 통해 예외 코드명과 메시지를 정의하세요.
[x] 모든 예외의 기본이 되는 DiscodeitException 클래스를 정의하세요.
[x] DiscodeitException을 상속하는 주요 도메인 별 메인 예외 클래스를 정의하세요.
[x] 도메인 메인 예외 클래스를 상속하는 구체적인 예외 클래스를 정의하세요.
[x] 기존에 구현했던 예외를 커스텀 예외로 대체하세요.
[x] ErrorResponse를 통해 일관된 예외 응답을 정의하세요.
[x] 앞서 정의한 ErrorResponse와 @RestControllerAdvice를 활용해 예외를 처리하는 예외 핸들러를 구현하세요.
[x] Spring Validation 의존성을 추가하세요.
[x] 주요 Request DTO에 제약 조건 관련 어노테이션을 추구하세요.
@NotNull, @notblank, @SiZe, @Email 등
[x] 컨트롤러에 @Valid 를 사용해 요청 데이터를 검증하세요.
[x] 검증 실패 시 발생하는 MethodArgumentNotValidException을 전역 예외 핸들러에서 처리하세요.
[x] 유효성 검증 실패 시 상세한 오류 메시지를 포함한 응답을 반환하세요.
Actuator
[x] Spring Boot Actuator 의존성을 추가하세요.
[x] 기본 Actuator 엔트포인트를 설정하세요.
health, info, metrics, loggers
[x] Actuator info를 위한 애플리케이션 정보를 추가하세요.
애플리케이션 이름: Discodeit
애플리케이션 버전: 1.7.0
자바 버전: 17
스프링 부트 버전: 3.4.0
주요 설정 정보
데이터소스: url, 드라이버 클래스 이름
jpa: ddl-auto
storage 설정: type, path
multipart 설정: max-file-size, max-request-size
[x] Spring Boot 서버를 실행 후 각종 정보를 확인해보세요.
/actuator/info
/actuator/metrics
/actuator/health
/actuator/loggers
단위 테스트
[x] 서비스 레이어의 주요 메소드에 대한 단위 테스트를 작성하세요.
[x] 다음 서비스의 핵심 메소드에 대해 각각 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
[x] UserService: create, update, delete 메소드
[x] ChannelService: create(PUBLIC, PRIVATE), update, delete, findByUserId 메소드
[x] MessageService: create, update, delete, findByChannelId 메소드
[x] Mockito를 활용해 Repository 의존성을 모의(mock)하세요.
[x] BDDMockito를 활용해 테스트 가독성을 높이세요.
슬라이스 테스트
[x] 레포지토리 레이어의 슬라이스 테스트를 작성하세요.
[x] @DataJpaTest를 활용해 테스트를 구현하세요.
[x] 테스트 환경을 구성하는 프로파일을 구성하세요.
[x] application-test.yaml을 생성하세요.
[x] 데이터소스는 H2 인메모리 데이터 베이스를 사용하고, PostgreSQL 호환 모드로 설정하세요.
[x] H2 데이터베이스를 위해 필요한 의존성을 추가하세요.
[x] 테스트 시작 시 스키마를 새로 생성하도록 설정하세요.
[x] 디버깅에 용이하도록 로그 레벨을 적절히 설정하세요.
[x] 테스트 실행 간 test 프로파일을 활성화 하세요.
[x] JPA Audit 기능을 활성화 하기 위해 테스트 클래스에 @EnableJpaAuditing을 추가하세요.
[x] 주요 레포지토리(User, Channel, Message)의 주요 쿼리 메소드에 대해 각각 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
[x] 커스텀 쿼리 메소드
[x] 페이징 및 정렬 메소드
[ ] 컨트롤러 레이어의 슬라이스 테스트를 작성하세요.
[ ] @WebMvcTest를 활용해 테스트를 구현하세요.
[ ] WebMvcTest에서 자동으로 등록되지 않는 유형의 Bean이 필요하다면 @import를 활용해 추가하세요.
예시
@import({ErrorCodeStatusMapper.class})
[ ] 주요 컨트롤러(User, Channel, Message)에 대해 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
[ ] MockMvc를 활용해 컨트롤러를 테스트하세요.
[ ] 서비스 레이어를 모의(mock)하여 컨트롤러 로직만 테스트하세요.
[ ] JSON 응답을 검증하는 테스트를 포함하세요.
통합 테스트
[ ] 통합 테스트 환경을 구성하세요.
[ ] @SpringBootTest를 활용해 Spring 애플리케이션 컨텍스트를 로드하세요.
[ ] H2 인메모리 데이터베이스를 활용하세요.
[ ] 테스트용 프로파일을 구성하세요.
[ ] 주요 API 엔드포인트에 대한 통합 테스트를 작성하세요.
[ ] 주요 API에 대해 최소 2개 이상의 테스트 케이스를 작성하세요.
[ ] 사용자 관련 API (생성, 수정, 삭제, 목록 조회)
[ ] 채널 관련 API (생성, 수정, 삭제)
[ ] 메시지 관련 API (생성, 수정, 삭제, 목록 조회)
[ ] 각 테스트는 @transactional을 활용해 독립적으로 실행하세요.
멘토에게 하고싶은 말:
테스트 코드에 대한 이해도 부족 과 게으름으로 완성 하지 못했습니다..