Skip to content

[강우진] sprint7 #206

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

Open
wants to merge 6 commits into
base: 강우진
Choose a base branch
from

Conversation

WJKANGsw
Copy link
Collaborator

@WJKANGsw WJKANGsw commented Jun 29, 2025

요구사항

기본

프로파일 기반 설정 관리
[ ] 개발, 운영 환경에 대한 프로파일을 구성하세요.
[ ] application-dev.yaml, application-prod.yaml 파일을 생성하세요.
[ ] 다음과 같은 설정값을 프로파일별로 분리하세요.
[ ] 데이터베이스 연결 정보
[ ] 서버 포트

로그 관리
[ ] Lombok의 @slf4j 어노테이션을 활용해 로깅을 쉽게 추가할 수 있도록 구성하세요.
[ ] application.yaml에 기본 로깅 레벨을 설정하세요.
[ ] 환경 별 적절한 로깅 레벨을 프로파일 별로 설정해보세요.
[ ] Spring Boot의 기본 로깅 구현체인 Logback의 설정 파일을 구성하세요.
[ ] logback-spring.xml 파일을 생성하세요.
[ ] 다음 예시와 같은 로그 메시지를 출력하기 위한 로깅 패턴과 출력 방식을 커스터마이징하세요.
[ ] 콘솔과 파일에 동시에 로그를 기록하도록 설정하세요.
[ ] 파일은 {프로젝트 루트}/.logs 경로에 저장되도록 설정하세요.
[ ] 로그 파일은 일자별로 롤링되도록 구성하세요.
[ ] 로그 파일은 30일간 보관하도록 구성하세요.

[ ] 서비스 레이어와 컨트롤러 레이어의 주요 메소드에 로깅을 추가하세요.
[ ] 로깅 레벨을 적절히 사용하세요: ERROR, WARN, INFO, DEBUG
[ ] 다음과 같은 메소드에 로깅을 추가하세요:
[ ] 사용자 생성/수정/삭제
[ ] 채널 생성/수정/삭제
[ ] 메시지 생성/수정/삭제
[ ] 파일 업로드/다운로드

예외 처리 고도화
[ ] 커스텀 예외를 설계하고 구현하세요.
패키지명: com.sprint.mission.discodeit.exception[.{도메인}]
[ ] ErrorCode Enum 클래스를 통해 예외 코드명과 메시지를 정의하세요.
[ ] 모든 예외의 기본이 되는 DiscodeitException 클래스를 정의하세요.
[ ] DiscodeitException을 상속하는 주요 도메인 별 메인 예외 클래스를 정의하세요.
[ ] 도메인 메인 예외 클래스를 상속하는 구체적인 예외 클래스를 정의하세요.
[ ] 기존에 구현했던 예외를 커스텀 예외로 대체하세요.
[ ] ErrorResponse를 통해 일관된 예외 응답을 정의하세요.
[ ] 앞서 정의한 ErrorResponse와 @RestControllerAdvice를 활용해 예외를 처리하는 예외 핸들러를 구현하세요.

유효성 검사
[ ] Spring Validation 의존성을 추가하세요.
[ ] 주요 Request DTO에 제약 조건 관련 어노테이션을 추구하세요.
[ ] 컨트롤러에 @Valid 를 사용해 요청 데이터를 검증하세요.
[ ] 검증 실패 시 발생하는 MethodArgumentNotValidException을 전역 예외 핸들러에서 처리하세요.
[ ] 유효성 검증 실패 시 상세한 오류 메시지를 포함한 응답을 반환하세요.

Actuator
[ ] Spring Boot Actuator 의존성을 추가하세요.
[ ] 기본 Actuator 엔트포인트를 설정하세요.
[ ] Actuator info를 위한 애플리케이션 정보를 추가하세요.
[ ] Spring Boot 서버를 실행 후 각종 정보를 확인해보세요.

단위 테스트
[ ] 서비스 레이어의 주요 메소드에 대한 단위 테스트를 작성하세요.
[ ] 다음 서비스의 핵심 메소드에 대해 각각 최소 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를 활용해 추가하세요.ㅍ
[ ] 주요 컨트롤러(User, Channel, Message)에 대해 최소 2개 이상(성공, 실패)의 테스트 케이스를 작성하세요.
[ ] MockMvc를 활용해 컨트롤러를 테스트하세요.
[ ] 서비스 레이어를 모의(mock)하여 컨트롤러 로직만 테스트하세요.
[ ] JSON 응답을 검증하는 테스트를 포함하세요.

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

심화

MDC를 활용한 로깅 고도화
[ ] 요청 ID, 요청 URL, 요청 방식 등의 정보를 MDC에 추가하는 인터셉터를 구현하세요.
[ ] 클래스명: MDCLoggingInterceptor
[ ] 패키지명: com..discodeit.config
[ ] 요청 ID는 랜덤한 문자열로 생성합니다. (UUID)
[ ] 요청 ID는 응답 헤더에 포함시켜 더 많은 분석이 가능하도록 합니다.
[ ] WebMvcConfigurer를 통해 MDCLoggingInterceptor를 등록하세요.
[ ] 클래스명: WebMvcConfig
[ ] 패키지명: com.
.discodeit.config
[ ] Logback 패턴에 MDC 값을 포함시키세요.

Spring Boot Admin을 활용한 메트릭 가시화
[ ] Spring Boot Admin 서버를 구현할 모듈을 생성하세요.
[ ] admin 모듈의 메인 클래스에 @EnableAdminServer 어노테이션을 추가하고, 서버는 9090번 포트로 설정합니다.
[ ] admin 서버 실행 후 localhost:9090/applications 에 접속해봅니다.
[ ] discodeit 프로젝트에 Spring Boot Admin Client를 적용합니다.
[ ] 의존성을 추가합니다.
[ ] admin 서버에 등록될 수 있도록 설정 정보를 추가합니다.
[ ] discodeit 서버를 실행하고, admin 대시보드에 discodeit 인스턴스가 추가되었는지 확인합니다.
[ ] admin 대시보드 화면을 조작해보면서 각종 메트릭 정보를 확인해보세요.

테스트 커버리지 관리
[ ] JaCoCo 플러그인을 추가하세요.
[ ] 테스트 실행 후 생성된 리포트를 분석해보세요.
[ ] com.sprint.mission.discodeit.service.basic 패키지에 대해서 60% 이상의 코드 커버리지를 달성하세요.

멘토에게

  • 심화 요구사항 진행중입니다. 이후 리팩터링 포함한 커밋은 sprint8에 추가하겠습니다.

@WJKANGsw WJKANGsw requested a review from dongjoon1251 June 29, 2025 14:59
@WJKANGsw WJKANGsw added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다. labels Jun 29, 2025
public DiscodeitException(ErrorCode errorCode, Map<String, Object> details) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.details = details;
Copy link
Collaborator

Choose a reason for hiding this comment

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

details 객체를 그대로 할당하면 어떤 문제가 생길 수 있을까요?

문제점

1. 불변성(Immutable) 보장 실패

  • details가 외부에서 전달된 Map<String, Object> 타입 그대로 할당되고 있습니다.
  • 만약 외부에서 전달된 details 맵이 이후에 변경된다면, DiscodeitException 객체 내부의 details도 함께 변경됩니다.
  • 이는 예외 객체의 상태가 예기치 않게 변할 수 있음을 의미하며, 예외 객체는 생성 이후 상태가 변하지 않는 것이 바람직합니다.

2. 캡슐화 위반

  • 외부에서 전달된 객체를 그대로 참조하므로, 예외 객체의 내부 상태가 외부에 노출됩니다.

개선 방법

1. 방어적 복사(Defensive Copy)

  • 생성자에서 전달받은 맵을 새로운 맵으로 복사하여 할당하면, 외부 변경으로부터 내부 상태를 보호할 수 있습니다.
public DiscodeitException(ErrorCode errorCode, Map<String, Object> details) {
    super(errorCode.getMessage());
    this.errorCode = errorCode;
    this.details = (details == null) ? null : new HashMap<>(details);
}

2. 불변 맵 사용

  • Java 9 이상이라면 Map.copyOf()를 사용할 수도 있습니다.
this.details = (details == null) ? null : Map.copyOf(details);

결론

  • 예외 객체의 내부 상태가 외부에 의해 변경될 수 있으므로, 방어적 복사를 통해 불변성을 보장하는 것이 좋습니다.
  • 특히 예외 객체는 로그나 디버깅 용도로 많이 사용되므로, 상태가 변하지 않도록 주의해야 합니다.

@@ -54,14 +62,17 @@ public ResponseEntity<MessageDto> createMessage(
file.getBytes()
);
} catch (IOException e) {
log.error("첨부파일 변환 실패 - 파일명: {}", file.getOriginalFilename(), e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

예외 로깅 👍

private String exceptionType;
private int status;

public static ErrorResponse of(ErrorCode error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

정적생성자명을 아래 기준으로 다시 맞춰보면 어떨까요?

이펙티브 자바에서 권장하는 정적 생성자(Static Factory Method) 네이밍 컨벤션 중 of, from, with의 차이점과 용도는 다음과 같습니다.

메소드명 용도 및 의미 예시 설명
of 여러 개의 인자를 받아 객체를 생성할 때 사용 EnumSet.of(JACK, QUEEN, KING)
Optional.of("value")
인자들이 객체의 내부 상태를 직접적으로 구성할 때 사용. 가장 일반적이고 간결한 네이밍.
from 하나의 값을 변환하거나, 다른 타입에서 변환할 때 사용 Date.from(instant)
Money.from(string)
외부 타입이나 값에서 해당 타입의 객체로 변환할 때 사용. 변환(Conversion)이나 타입 간 변환에 적합.
with 기존 객체에 일부 속성을 추가/변경하여 새로운 객체를 만들 때 사용 Person.withName("Alice") 불변 객체에서 일부 속성만 바꾼 새로운 인스턴스를 만들 때 주로 사용. 플루언트 API 스타일에 적합.

추가 설명

  • of

    • 가장 범용적으로 쓰이며, 여러 매개변수를 받아 객체를 생성할 때 적합합니다.
    • 내부 상태를 직접적으로 구성하는 경우에 사용합니다.
    • 예시:
      Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
      Optional<String> value = Optional.of("test");
  • from

    • 하나의 매개변수를 받아 다른 타입에서 변환하는 경우에 사용합니다.
    • 주로 "타입 변환"이나 "파싱"의 의미가 강합니다.
    • 예시:
      Date d = Date.from(instant);
      Money money = Money.from("1000");
  • with

    • 기존 객체에서 일부 속성만 변경한 새로운 객체를 만들 때 사용합니다.
    • 불변 객체 패턴(Immutable Object Pattern)에서 많이 사용됩니다.
    • 예시:
      Person newPerson = oldPerson.withName("Alice");

참고

  • 이펙티브 자바에서는 네이밍 컨벤션을 통해 메소드의 의도를 명확히 드러내는 것이 중요하다고 강조합니다.
  • JDK나 유명 라이브러리에서도 위와 같은 네이밍을 일관되게 사용하고 있습니다.

요약:

  • of: 여러 값으로 객체 생성
  • from: 변환(Conversion)
  • with: 일부 속성만 바꾼 새 객체 생성

필요에 따라 이 컨벤션을 적용하면 코드의 의도가 명확해집니다.

Comment on lines +44 to 50
public boolean isPrivate() {
return ChannelType.PRIVATE.equals(this.type);
}

public boolean isPublic() {
return ChannelType.PUBLIC.equals(this.type);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

this.type이 notnull인 것이 보장된다면 아래와 같이 쓸 수도 있지 않을까요?

return this.type.isPrivate();

BINARY_CONTENT_NOT_FOUND(404, "B001", "첨부파일을 찾을 수 없습니다."),
BINARY_CONTENT_INVALID(400, "B002", "유효하지 않은 첨부파일입니다.");

private final int status;
Copy link
Collaborator

Choose a reason for hiding this comment

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

숫자를 바로 넣지 않고 org.springframework.http.HttpStatus enum을 사용해보면 어떨까요?

MethodArgumentNotValidException e) {
Map<String, Object> details = e.getBindingResult()
.getFieldErrors().stream()
.collect(Collectors.toMap(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Collectors.toMap 사용 👍

Comment on lines +58 to +59
.code("VALIDATION_FAILED")
.message("요청 데이터 유효성 검증에 실패하였습니다.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

상수들은 final static 클래스 멤버변수로 만들어서 사용하면 어떨까요?

Comment on lines +22 to +29
@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")
Slice<Message> findAllByChannelIdWithAuthor(@Param("channelId") UUID channelId,
@Param("createdAt") Instant createdAt,
Pageable pageable);
Copy link
Collaborator

Choose a reason for hiding this comment

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

fetch join이 동작하는지 테스트해볼 수 있을까요?

Hibernate.isInitialized(Object proxy) 는 Hibernate에서 프록시 객체나 지연 로딩(lazy loading) 컬렉션이 실제로 초기화(로딩)되었는지 여부를 확인하는 정적 메소드입니다. 이 메소드는 다음과 같은 상황에서 주로 사용됩니다.

  • 지연 로딩(lazy loading) 필드나 컬렉션이 실제 DB에서 로딩되었는지(초기화되었는지) 확인할 때
  • 세션이 닫힌 후 LazyInitializationException이 발생하지 않도록, 접근 전에 초기화 여부를 미리 체크할 때

동작 방식

  • proxy 또는 persistent collection 객체가 전달되면,
    • 이미 초기화(로딩)된 경우 true 반환
    • 아직 초기화되지 않은 경우 false 반환
    • 일반 객체(프록시나 컬렉션이 아닌 경우)는 항상 true 반환

public static boolean isInitialized(java.lang.Object proxy)
Check if the proxy or persistent collection is initialized.
Returns: true if the argument is already initialized, or is not a proxy or collection.

예시 코드

if (!Hibernate.isInitialized(entity.getLazyCollection())) {
    Hibernate.initialize(entity.getLazyCollection());
}
  • 위 예시처럼, 컬렉션이나 연관 엔티티가 초기화되지 않았다면 Hibernate.initialize()로 강제 초기화할 수 있습니다.

활용 시 주의사항

  • Detached 상태(세션이 닫힌 후)에서는 초기화할 수 없으므로, 반드시 세션이 열려 있을 때 사용해야 합니다.
  • 컬렉션의 경우, 컬렉션 객체 자체의 초기화 여부만 판단합니다. 컬렉션 내부의 요소까지 모두 초기화되었는지는 보장하지 않습니다.

요약

  • **Hibernate.isInitialized(obj)**는 해당 객체(프록시, 컬렉션)가 실제로 DB에서 로딩되었는지 확인하는 메소드입니다.
  • Lazy Loading 환경에서 안전하게 객체 접근이 필요한 경우, 이 메소드로 초기화 여부를 확인 후 사용하세요.

.filter(user -> user.getUsername().equals(request.username()))
.filter(user -> user.getPassword().equals(request.password()))
.findFirst()
return userRepository.findByUsernameAndPassword(request.username(), request.password())
.map(userMapper::toDto)
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 username & password 입니다."));
Copy link
Collaborator

Choose a reason for hiding this comment

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

username이 잘못되었는지, password가 잘못되었는지
실패 원인을 상세하게 알 수 있게 하면 어떨까요?
그리고 custom한 예외로 이 부분도 수정해주면 어떨까요?

@DisplayName("새로운 입력값으로 채널을 수정할 수 있다.")
void updateChannel_success() {
//given
PublicChannelUpdateRequest request = new PublicChannelUpdateRequest("이름1", "설명1");
Copy link
Collaborator

Choose a reason for hiding this comment

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

이부분이 중복되는 것 같은데 request도 fixture로 분리해주면 어떨까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants