Skip to content

전승현[sprint7]#148

Open
seunghyeonjeon57-dot wants to merge 29 commits into
codeit-bootcamp-spring:mainfrom
seunghyeonjeon57-dot:feature/전승현

Hidden character warning

The head ref may contain hidden characters: "feature/\uc804\uc2b9\ud604"
Open

전승현[sprint7]#148
seunghyeonjeon57-dot wants to merge 29 commits into
codeit-bootcamp-spring:mainfrom
seunghyeonjeon57-dot:feature/전승현

Conversation

@seunghyeonjeon57-dot
Copy link
Copy Markdown
Collaborator

@seunghyeonjeon57-dot seunghyeonjeon57-dot commented Mar 30, 2026

요구사항

기본

프로파일 기반 설정 관리

  • 개발, 운영 환경에 대한 프로파일을 구성하세요.

  • application-dev.yaml, application-prod.yaml 파일을 생성하세요.

  • 다음과 같은 설정값을 프로파일별로 분리하세요.

  • 데이터베이스 연결 정보

  • 서버 포트
    로그 관리

  • Lombok의 @slf4j 어노테이션을 활용해 로깅을 쉽게 추가할 수 있도록 구성하세요.

  • application.yaml에 기본 로깅 레벨을 설정하세요.
    기본적으로 info 레벨로 설정합니다.

  • 환경 별 적절한 로깅 레벨을 프로파일 별로 설정해보세요.
    SQL 로그를 보기위해 설정했던 레벨은 유지합니다.
    우리가 작성한 프로젝트의 로그는 개발 환경에서 debug, 운영 환경에서는 info 레벨로 설정합니다.

  • Spring Boot의 기본 로깅 구현체인 Logback의 설정 파일을 구성하세요.

  • logback-spring.xml 파일을 생성하세요.

  • 다음 예시와 같은 로그 메시지를 출력하기 위한 로깅 패턴과 출력 방식을 커스터마이징하세요.

로그 출력 예시

패턴

{년}-{월}-{일} {시}:{분}:{초}:{밀리초} [{스레드명}] {로그 레벨(5글자로 맞춤)} {로거 이름(최대 36글자)} - {로그 메시지}{줄바꿈}

  • 콘솔과 파일에 동시에 로그를 기록하도록 설정하세요.

  • 파일은 {프로젝트 루트}/.logs 경로에 저장되도록 설정하세요.

  • 로그 파일은 일자별로 롤링되도록 구성하세요.

  • 로그 파일은 30일간 보관하도록 구성하세요.

  • 서비스 레이어와 컨트롤러 레이어의 주요 메소드에 로깅을 추가하세요.

  • 로깅 레벨을 적절히 사용하세요: ERROR, WARN, INFO, DEBUG

  • 다음과 같은 메소드에 로깅을 추가하세요:

  • 사용자 생성/수정/삭제

  • 채널 생성/수정/삭제

  • 메시지 생성/수정/삭제

  • 파일 업로드/다운로드
    예외 처리 고도화

  • 커스텀 예외를 설계하고 구현하세요.

패키지명: com.sprint.mission.discodeit.exception[.{도메인}]

  • ErrorCode Enum 클래스를 통해 예외 코드명과 메시지를 정의하세요.

  • 모든 예외의 기본이 되는 DiscodeitException 클래스를 정의하세요.

  • DiscodeitException을 상속하는 주요 도메인 별 메인 예외 클래스를 정의하세요.

UserException, ChannelException 등
실제로 활용되는 클래스라기보다는 예외 클래스의 계층 구조를 명확하게 하기 위한 클래스 입니다.

  • 도메인 메인 예외 클래스를 상속하는 구체적인 예외 클래스를 정의하세요.

  • 기존에 구현했던 예외를 커스텀 예외로 대체하세요.

  • ErrorResponse를 통해 일관된 예외 응답을 정의하세요.

  • 앞서 정의한 ErrorResponse와 @RestControllerAdvice를 활용해 예외를 처리하는 예외 핸들러를 구현하세요.

모든 핸들러는 일관된 응답(ErrorResponse)을 가져야 합니다.
유효성 검사

  • Spring Validation 의존성을 추가하세요.

  • 주요 Request DTO에 제약 조건 관련 어노테이션을 추구하세요.
    @NotNull, @notblank, @SiZe, @Email 등

  • 컨트롤러에 @Valid 를 사용해 요청 데이터를 검증하세요.

  • 검증 실패 시 발생하는 MethodArgumentNotValidException을 전역 예외 핸들러에서 처리하세요.

  • 유효성 검증 실패 시 상세한 오류 메시지를 포함한 응답을 반환하세요.
    Actuator

  • Spring Boot Actuator 의존성을 추가하세요.

  • 기본 Actuator 엔트포인트를 설정하세요.
    health, info, metrics, loggers

  • Actuator info를 위한 애플리케이션 정보를 추가하세요.
    애플리케이션 이름: Discodeit
    애플리케이션 버전: 1.7.0
    자바 버전: 17
    스프링 부트 버전: 3.4.0
    주요 설정 정보
    데이터소스: url, 드라이버 클래스 이름
    jpa: ddl-auto
    storage 설정: type, path
    multipart 설정: max-file-size, max-request-size

  • Spring Boot 서버를 실행 후 각종 정보를 확인해보세요.
    /actuator/info
    /actuator/metrics
    /actuator/health
    /actuator/loggers
    단위 테스트

  • 서비스 레이어의 주요 메소드에 대한 단위 테스트를 작성하세요.

  • 다음 서비스의 핵심 메소드에 대해 각각 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.

  • UserService: create, update, delete 메소드

  • ChannelService: create(PUBLIC, PRIVATE), update, delete, findByUserId 메소드

  • MessageService: create, update, delete, findByChannelId 메소드

  • Mockito를 활용해 Repository 의존성을 모의(mock)하세요.

  • BDDMockito를 활용해 테스트 가독성을 높이세요.
    슬라이스 테스트

  • 레포지토리 레이어의 슬라이스 테스트를 작성하세요.

  • @DataJpaTest를 활용해 테스트를 구현하세요.

  • 테스트 환경을 구성하는 프로파일을 구성하세요.

  • application-test.yaml을 생성하세요.

  • 데이터소스는 H2 인메모리 데이터 베이스를 사용하고, PostgreSQL 호환 모드로 설정하세요.

  • H2 데이터베이스를 위해 필요한 의존성을 추가하세요.

  • 테스트 시작 시 스키마를 새로 생성하도록 설정하세요.

  • 디버깅에 용이하도록 로그 레벨을 적절히 설정하세요.

  • 테스트 실행 간 test 프로파일을 활성화 하세요.

  • JPA Audit 기능을 활성화 하기 위해 테스트 클래스에 @EnableJpaAuditing을 추가하세요.

  • 주요 레포지토리(User, Channel, Message)의 주요 쿼리 메소드에 대해 각각 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.

  • 커스텀 쿼리 메소드

  • 페이징 및 정렬 메소드

  • 컨트롤러 레이어의 슬라이스 테스트를 작성하세요.

  • @WebMvcTest를 활용해 테스트를 구현하세요.

  • WebMvcTest에서 자동으로 등록되지 않는 유형의 Bean이 필요하다면 @import를 활용해 추가하세요.

예시

@import({ErrorCodeStatusMapper.class})

  • 주요 컨트롤러(User, Channel, Message)에 대해 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.

  • MockMvc를 활용해 컨트롤러를 테스트하세요.

  • 서비스 레이어를 모의(mock)하여 컨트롤러 로직만 테스트하세요.

  • JSON 응답을 검증하는 테스트를 포함하세요.

통합 테스트

  • 통합 테스트 환경을 구성하세요.
  • @SpringBootTest를 활용해 Spring 애플리케이션 컨텍스트를 로드하세요.
  • H2 인메모리 데이터베이스를 활용하세요.
  • 테스트용 프로파일을 구성하세요.
  • 주요 API 엔드포인트에 대한 통합 테스트를 작성하세요.
  • 주요 API에 대해 최소 2개 이상의 테스트 케이스를 작성하세요.
  • 사용자 관련 API (생성, 수정, 삭제, 목록 조회)
  • 채널 관련 API (생성, 수정, 삭제)
  • 메시지 관련 API (생성, 수정, 삭제, 목록 조회)
  • 각 테스트는 @transactional을 활용해 독립적으로 실행하세요.

심화

  • 요청 ID, 요청 URL, 요청 방식 등의 정보를 MDC에 추가하는 인터셉터를 구현하세요.

  • 클래스명: MDCLoggingInterceptor

  • 패키지명: com.**.discodeit.config

  • 요청 ID는 랜덤한 문자열로 생성합니다. (UUID)

  • 요청 ID는 응답 헤더에 포함시켜 더 많은 분석이 가능하도록 합니다.
    헤더 이름: Discodeit-Request-ID

  • WebMvcConfigurer를 통해 MDCLoggingInterceptor를 등록하세요.

  • 클래스명: WebMvcConfig

  • 패키지명: com.**.discodeit.config

  • Spring Boot Admin 서버를 구현할 모듈을 생성하세요.

  • JaCoCo 플러그인을 추가하세요.

주요 변경사항

멘토에게

  • 매운맛으로 솔직하게 부탁드리겠습니다
  • jaCoCo를 통해 요구사항인 User, Channel, Message 도메인의 테스트를 챙겼습니다. 하지만 실무에서는 모든 클래스를 100% 채우는 것이 비효율적일 수도 있다고 들었습니다. 멘토님은 어떤 기준으로 반드시 테스트해야 할 도메인과 테스트 비중을 낮춰도 되는 도메인을 구분하시는지 궁금합니다
  • 이번에 DiscodeitException을 최상위로 두고 도메인별 예외(UserException 등)를 계층화했습니다. 프로젝트가 커지면 예외 클래스가 너무 많아질 것 같은데, 실무에서는 예외 클래스를 세분화하는 편인가요, 아니면 ErrorCode Enum으로만 구분하는 편인가요
  • 단위, 슬라이스, 통합 테스트를 모두 작성해보니 통합 테스트는 설정이 무겁고 실행 속도가 느리다는 걸 체감했습니다. 실무 프로젝트에서 테스트 피드백 속도를 높이기 위해 통합 테스트의 비중을 어느 정도로 조절하시는지 궁금합니다.
  • 이번 미션포함 해서 미션을 하면 할수록 AI에 대한 의존도가 좀 높아지는 기분인데 어떤식으로 사용해야 효율적일지 궁금합니다. 현재는 계속 질문을 던지고 그 개념에 대해 파고들어 이해하는 형식으로 사용하고 있습니다

@eedys1234
Copy link
Copy Markdown
Collaborator

@seunghyeonjeon57-dot 님 merge 하는 브랜치가 잘못된 것 같습니다.

@eedys1234
Copy link
Copy Markdown
Collaborator

.gitignore 파일 안에 .ser 파일 관련하여 명시하여 원격 저장소에 저장되는 것을 제외해야 할 것 같습니다.

@RequiredArgsConstructor
public class AuthController implements AuthApi {

private final UserMapper mapper;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mapper 사용처가 없어보이는데 맞을까요? 맞다면 제거해도 될 것 같습니다.


<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

환경 별로 세분화하신 부분 👍👍
구현하신 것과 같이 현업에서도 환경 별로 출력하는 곳을 다르게 설정합니다.

</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archived/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

파일의 크기가 너무 크면 서버에서 보기가 어려워 질 수 있어 조그마한 크기로 조각내는데 디테일하게 잘 구분해주셨습니다.

datasource:
url: jdbc:postgresql://localhost:5432/discodeit
username: discodeit_user
password: discodeit1234
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

동일 username 이지만 망 별로 패스워드를 다르게 하는 것을 추천드려요.

@@ -0,0 +1,49 @@
spring:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

prod에는 server.port 가 존재하지 않는 것 같아요.
공통적으로 사용되는 부분들은 applicaiton.yaml 파일을 생성하여 선언해주세요.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Transactional
@AutoConfigureMockMvc
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

선언하신 부분을 커스텀 애노테이션으로 선언하여 사용하면 어떨까요?

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Transactional
@AutoConfigureMockMvc
public @interface IntegrationTest {
}

애노테이션 명을 통해 의미적으로 부여할 수 있을 것 같습니다.

.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isCreated());
var createChannel = channelRepository.findByName("하이").orElseThrow();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

저장 API 호출 이후 저장된 데이터가 정상적으로 저장되었는지 확인하려면 조회 API를 별도로 호출하는 것을 추천드려요.

즉, 저장 API -> 조회 API 순이 될 것 같아요.

}

@Test
@DisplayName("채널 삭제 API를 호출하면 Db에서 채널 삭제가되어야한다")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

위와 동일하게 삭제하기 위해 저장 -> 삭제 -> 조회 순으로 API를 호출하는 것을 권장드려요.

기능 별 API 호출 함수를 선언하여 사용하는 방식이면 재사용성이 좋을 것 같아요.

@InjectMocks
private BasicChannelService channelService;

@Nested
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

테스트들을 그룹핑하는 방법 좋습니다. 👍👍

assertThat(result).isNotNull();
assertThat(result.name()).isEqualTo("공용채널1");

verify(channelRepository).save(any(Channel.class));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

값 뿐만 아니라 호출 행위를 검증하는 부분도 좋습니다.

@eedys1234
Copy link
Copy Markdown
Collaborator

jaCoCo를 통해 요구사항인 User, Channel, Message 도메인의 테스트를 챙겼습니다. 하지만 실무에서는 모든 클래스를 100% 채우는 것이 비효율적일 수도 있다고 들었습니다. 멘토님은 어떤 기준으로 반드시 테스트해야 할 도메인과 테스트 비중을 낮춰도 되는 도메인을 구분하시는지 궁금합니다

저는 버그가 발생할 가능성이 높은 즉 로직 복잡도가 높은 코드들에 대해 집중적으로 테스트 코드를 작성하는 편입니다.
추가적으로, 이후 개발자가 테스트를 진행하면서 버그가 발생한 코드가 있다면 추가 케이스에 대한 테스트 코드를 작성합니다.

이번에 DiscodeitException을 최상위로 두고 도메인별 예외(UserException 등)를 계층화했습니다. 프로젝트가 커지면 예외 클래스가 너무 많아질 것 같은데, 실무에서는 예외 클래스를 세분화하는 편인가요, 아니면 ErrorCode Enum으로만 구분하는 편인가요
단위, 슬라이스, 통합 테스트를 모두 작성해보니 통합 테스트는 설정이 무겁고 실행 속도가 느리다는 걸 체감했습니다. 실무 프로젝트에서 테스트 피드백 속도를 높이기 위해 통합 테스트의 비중을 어느 정도로 조절하시는지 궁금합니다.

개인적으로 통합 테스트를 선호하지 않아, 정말 통합 테스트가 필요한 기능들만 작성하는 편입니다.
인프라 별 예외 클래스는 세분화 하지만, 도메인 별 예외 케이스는 특정 예외 클래스를 선언하여 다음과 같이 활용하는 편입니다.

인프라 별 예외(특정 도메인에 종속되는 게 아닌 전역적으로 사용되는)
FeatureFlagException
AuthenticationException
public class ChannelExceptionFactory {
   public static ApplicationException requiredChannelName() {
      return new ApplicationException("채널명이 존재하지 않습니다.");
   }
}

throw ChannelExceptionFactory.requiredChannelName();

이번 미션포함 해서 미션을 하면 할수록 AI에 대한 의존도가 좀 높아지는 기분인데 어떤식으로 사용해야 효율적일지 궁금합니다. 현재는 계속 질문을 던지고 그 개념에 대해 파고들어 이해하는 형식으로 사용하고 있습니다

잘하고 계시는 것 같아요.
어떤 기능을 구현할지 질의를 한 다음 AI가 응답하는 소스코드를 기반으로 모르는 부분들을 세부적으로 다시 질의합니다.
또한 상세한 개념 뿐만 아니라 개념을 활용한 기능 혹은 결과물이 무엇인지 익히면 좋을 것 같습니다.

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