Skip to content

[강호] sprint3 #67

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 4 commits into
base: 강호
Choose a base branch
from

Conversation

kangho1870
Copy link
Collaborator

@kangho1870 kangho1870 commented Apr 28, 2025

요구사항

기본 요구사항

Spring 프로젝트 초기화

  • Spring Initializr를 통해 zip 파일을 다운로드하세요.
    • 빌드 시스템은 Gradle - Groovy를 사용합니다.
    • 언어는 Java 17를 사용합니다.
    • Spring Boot의 버전은 3.4.0입니다.
    • GroupId는 com.sprint.mission입니다.
    • ArtifactId와 Name은 discodeit입니다.
    • packaging 형식은 Jar입니다
    • Dependency를 추가합니다.
      • Lombok
      • Spring Web
  • zip 파일을 압축해제하고 원래 진행 중이던 프로젝트에 붙여넣기하세요. 일부 파일은 덮어쓰기할 수 있습니다.
  • application.properties 파일을 yaml 형식으로 변경하세요.
  • DiscodeitApplication의 main 메서드를 실행하고 로그를 확인해보세요.

Bean 선언 및 테스트

  • File*Repository 구현체를 Repository 인터페이스의 Bean으로 등록하세요.
  • Basic*Service 구현체를 Service 인터페이스의 Bean으로 등록하세요.
  • JavaApplication에서 테스트했던 코드를 DiscodeitApplication에서 테스트해보세요.
    • JavaApplication 의 main 메소드를 제외한 모든 메소드를 DiscodeitApplication클래스로 복사하세요.
    • JavaApplication의 main 메소드에서 Service를 초기화하는 코드를 Spring Context를 활용하여 대체하세요.
    • JavaApplication의 main 메소드의 셋업, 테스트 부분의 코드를 DiscodeitApplication클래스로 복사하세요.

Spring 핵심 개념 이해하기

  • JavaApplication과 DiscodeitApplication에서 Service를 초기화하는 방식의 차이에 대해 다음의 키워드를 중심으로 정리해보세요.
    • IoC Container
    • Dependency Injection
    • Bean
    • JavaApplication에서는 Service의 초기화를 new 연산자를 통해 직접 생성하여 Dependency Injection을 하였지만,
      DiscodeitApplication에서는 @SpringBootApplication@componentscan을 통해 @component 계열을 찾아 Bean으로 등록하고
      이를 IoC Container가 등록된 Bean을 통해 스스로 클래스들의 관계를 파악하여 객체의 생성, 초기화, 의존성 주입까지 스스로 동작하여 Service를 초기화를 진행함
      DI 방법은 아래의 방법들이 있음
      • 생성자 주입
      • 필드 주입
      • setter 주입

Lombok 적용

비즈니스 로직 고도화

  • 다음의 기능 요구 사항을 구현하세요.

추가 기능 요구사항

시간 타입 변경하기

  • 시간을 다루는 필드의 타입은 Instant로 통일합니다.
    • 기존에 사용하던 Long보다 가독성이 뛰어나며, 시간대(Time Zone) 변환과 정밀한 시간 연산이 가능해 확장성이 높습니다.

새로운 도메인 추가하기

  • 도메인 모델 간 참조 관계를 참고하세요.
    image
  • 공통: 앞서 정의한 도메인 모델과 동일하게 공통 필드(id, createdAt, updatedAt)를 포함합니다.
  • ReadStatus
    • 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다.
  • UserStatus
    • 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다.
    • 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요.
      • 마지막 접속 시간이 현재 시간으로부터 5분 이내이면 현재 접속 중인 유저로 간주합니다.
  • BinaryContent
    • 이미지, 파일 등 바이너리 데이터를 표현하는 도메인 모델입니다. 사용자의 프로필 이미지, 메시지에 첨부된 파일을 저장하기 위해 활용합니다.
    • 수정 불가능한 도메인 모델로 간주합니다. 따라서 updatedAt 필드는 정의하지 않습니다.
    • User, Message 도메인 모델과의 의존 관계 방향성을 잘 고려하여 id 참조 필드를 추가하세요.
  • 각 도메인 모델 별 레포지토리 인터페이스를 선언하세요.
    • 레포지토리 구현체(File, JCF)는 아직 구현하지 마세요. 이어지는 서비스 고도화 요구사항에 따라 레포지토리 인터페이스에 메소드가 추가될 수 있어요.

DTO 활용하기

UserService 고도화

  • 고도화
    • create
      • 선택적으로 프로필 이미지를 같이 등록할 수 있습니다.
      • DTO를 활용해 파라미터를 그룹화합니다.
        • 유저를 등록하기 위해 필요한 파라미터, 프로필 이미지를 등록하기 위해 필요한 파라미터 등
      • username과 email은 다른 유저와 같으면 안됩니다.
      • UserStatus를 같이 생성합니다.
    • find, findAll
      • DTO를 활용하여:
        • 사용자의 온라인 상태 정보를 같이 포함하세요.
        • 패스워드 정보는 제외하세요.
    • update
      • 선택적으로 프로필 이미지를 대체할 수 있습니다.
      • DTO를 활용해 파라미터를 그룹화합니다.
        • 수정 대상 객체의 id 파라미터, 수정할 값 파라미터
    • delete
      • 관련된 도메인도 같이 삭제합니다.
        • BinaryContent(프로필)
        • UserStatus
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

AuthService 구현

  • login
    • DTO를 활용해 파라미터를 그룹화합니다. (AuthLoginRequestDto)
    • username과 password가 일치하는 유저가 있는지 확인합니다.
    • 일치하는 유저가 있는 경우: 유저 정보를 반환합니다.
    • 일치하는 유저가 없는 경우: 예외를 발생시킵니다.
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository(UserRepository) 의존성을 주입해보세요.
      image

ChannelService 고도화

  • create
    • PRIVATE 채널과 PUBLIC 채널을 생성하는 메소드를 분리합니다.
      • PRIVATE 채널 생성 시, 채널에 참여하는 User 정보를 받아 User 별 ReadStatus 정보를 생성합니다.
      • PRIVATE 채널은 name과 description 속성을 생략합니다.
      • PUBLIC 채널은 기존 로직을 유지합니다.
    • 분리된 각각의 메소드를 DTO를 활용해 파라미터를 그룹화합니다.
  • find
    • DTO를 활용하여:
      • 해당 채널의 가장 최근 메시지의 시간 정보를 포함합니다.
      • PRIVATE 채널인 경우, 참여한 User의 id 정보를 포함합니다.
  • findAll
    • DTO를 활용하여:
      • 해당 채널의 가장 최근 메시지의 시간 정보를 포함합니다.
      • PRIVATE 채널인 경우, 참여한 User의 id 정보를 포함합니다.
      • 특정 User가 볼 수 있는 Channel 목록을 조회하도록 조회 조건을 추가하고, 메소드 명을 findAllByUserId로 변경합니다.
        • PUBLIC 채널 목록은 전체 조회합니다.
        • PRIVATE 채널은 조회한 User가 참여한 채널만 조회합니다.
  • update
    • DTO를 활용해 파라미터를 그룹화합니다.
      • 수정 대상 객체의 id 파라미터, 수정할 값 파라미터
    • PRIVATE 채널은 수정할 수 없습니다.
  • delete
    • 관련된 도메인도 같이 삭제합니다. (Message, ReadStatus)
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

MessageService 고도화

  • create
    • 선택적으로 여러 개의 첨부파일을 같이 등록할 수 있습니다.
    • DTO를 활용해 파라미터를 그룹화합니다.
  • findAll
    • 특정 Channel의 Message 목록을 조회하도록 조회 조건을 추가하고, 메소드 명을 findAllByChannelId로 변경합니다.
  • update
    • DTO를 활용해 파라미터를 그룹화합니다.
      • 수정 대상 객체의 id 파라미터, 수정할 값 파라미터
  • delete
    • 관련된 도메인도 같이 삭제합니다. (BinaryContent)
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

ReadStatusService 구현

  • create
    • DTO를 활용해 파라미터를 그룹화합니다.
    • 관련된 Channel이나 User가 존재하지 않으면 예외를 발생시킵니다.
    • 같은 Channel과 User와 관련된 객체가 이미 존재하면 예외를 발생시킵니다.
  • find
    • id로 조회합니다.
  • findAllByUserId
    • userId를 조건으로 조회합니다.
  • update
    • DTO를 활용해 파라미터를 그룹화합니다.
      • 수정 대상 객체의 id 파라미터, 수정할 값 파라미터
  • delete
    • id로 삭제합니다.
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

UserStatusService 고도화

  • create
    • DTO를 활용해 파라미터를 그룹화합니다.
    • 관련된 User가 존재하지 않으면 예외를 발생시킵니다.
    • 같은 User와 관련된 객체가 이미 존재하면 예외를 발생시킵니다.
  • find
    • id로 조회합니다.
  • findAll
    • 모든 객체를 조회합니다.
  • update
    • DTO를 활용해 파라미터를 그룹화합니다.
      • 수정 대상 객체의 id 파라미터, 수정할 값 파라미터
  • updateByUserId
    • userId로 특정 User의 객체를 업데이트합니다.
  • delete
    • id로 삭제합니다.
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

BinaryContentService 구현

  • create
    • DTO를 활용해 파라미터를 그룹화합니다.
  • find
    • id로 조회합니다.
  • findAllByIdIn
    • id 목록으로 조회합니다.
  • delete
    • id로 삭제합니다.
  • 의존성
    • 같은 레이어 간 의존성 주입은 순환 참조 방지를 위해 지양합니다. 다른 Service 대신 필요한 Repository 의존성을 주입해보세요.
      image

새로운 도메인 Repository 구현체 구현

  • 지금까지 인터페이스로 설계한 각각의 Repository를 JCF, File로 각각 구현하세요.
    image

심화 요구사항

bean 다루기

  • Repository 구현체 중에 어떤 구현체를 Bean으로 등록할지 Java 코드의 변경 없이 application.yaml 설정 값을 통해 제어해보세요.
    • discodeit.repository.type 설정값에 따라 Repository 구현체가 정해집니다.
      • 값이 jcf 이거나 없으면 JCF*Repository 구현체가 Bean으로 등록되어야 합니다.
      • 값이 file 이면 File*Repository 구현체가 Bean으로 등록되어야 합니다.
    • File*Repository 구현체의 파일을 저장할 디렉토리 경로를 application.yaml 설정 값을 통해 제어해보세요.

스크린샷

  • User 정보 (profile 정보 포함)
    image

  • Channel 정보 (public : 참여한 memberId 제외, private : 참여한 memberId)
    image

  • Message 정보 (파일 포함)
    image

  • FileRepository 구현체의 파일 저장 디렉토리
    image

멘토에게

  • mission1 ~ 3까지 진행하면서 느낀 부분이 새로운 요구사항이 생길 때마다 수정해야 할 부분이 전반적으로 너무 많이 생기더라구요. 물론 저의 부족한 실력 때문에 생기는 파급효과라고 생각합니다... 앞으로 미션을 진행하다 보면 수정할 부분이 더욱 많아질 거라고 생각이 드는데, 혹시 멘토님은 설계를 하는 과정에서 어떤 부분을 신경 쓰시는지, 아니면 어떤 부분을 더 고려하면 코드의 변경 사항을 그나마 줄일수 있을지..? 팁..?이 궁금합니다.
  • 미션을 진행하면서 ChannelService 고도화의 find 메소드의 경우 channel의 id만 파라미터로 받으면 될 것 같은데 굳이 DTO를 만들어 사용해야 하는지..?

@kangho1870 kangho1870 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Apr 29, 2025
Copy link
Collaborator

@IDontHaveBrain IDontHaveBrain left a comment

Choose a reason for hiding this comment

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

레포지토리 영역과 서비스 영역이 전반적으로 혼재되어 있습니다!
또 어떤 레포지토리에선 단건조회에 Optional을 반환, Entity를 반환, Error를 Throw 등 각 XXXRepository 마다 일관성이나 기준점이 없기에 해당 레포지토리를 사용하려는 다른 개발자의 입장에선 동작에 대한 예측이 힘들고, 상세 내부 구현을 모두 살펴볼수 밖에 없게 됩니다!

우선적으로 Repository에 일관성 있는 반환(Optional로 반환할지, Entity를 그대로 반환할지 etc...) 등과 같은 기준점을 잡으시고 일관성있게 통일하셔야 합니다!

이후 서비스 레이어의 영역과 레포지토리 레이어의 영역을 조금더 분리 하셔야 합니다.
Repository 는 최대한 복잡한 로직없이 단순하게 데이터의 "검색, 수정, 추가, 삭제" 에만 집중해야 합니다!
권한체크, 검색한 혹은 수정하려는 데이터가 없을시 에러를 throw 등의 역할은 서비스에서 수행하셔야 합니다!

implementation("org.springframework:spring-aop:6.2.3")

implementation("org.aspectj:aspectjweaver:1.9.21")
implementation("org.aspectj:aspectjrt:1.9.21")
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

spring-aop 의존성은 spring-boot-starter-web 에 이미 포함되어 있습니다.

aspectjweaver, aspectjrt 의존성은 현재 사용하고 계시는 Spring Boot 3.4.4 권장 버전보다 낮은 버전으로 설정하신 이유가 있으실까요 ?

아래 URL을 통해서 Spring Boot 특정 버전에서 권장하는 디펜던시 버전이 명시되어 있습니다.
아래 URL에 포함된 의존성은 특정한 이유가 있는게 아니라면, 다음과 같이 버전을 명시적으로 설정 안하시는게 안전합니다. implementation("org.aspectj:aspectjweaver")
"io.spring.dependency-management" 플러그인으로 자동으로 호환 및 권장되는 버전이 설정됩니다.

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent/3.4.4
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/3.4.4

Comment on lines +24 to +38
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
String method = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
log.info("▶▶ Start: {}", method);
log.info("▶▶ Args: {}", Arrays.toString(args));
}

@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
String method = joinPoint.getSignature().toShortString();
log.info("✅ End: {}", method);
log.info("✅ Returned: {}", result);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

AOP를 활용하여 보신 부분은 인상적입니다!
만약 매개변수나 리턴값이 기본 자료형 or DTO가 아닌 바이너리 데이터(이미지, 엑셀, etc...) 등이라면 식별하기 힘든 로그가 너무 길게 출력될 수 있습니다.
현재 단계와 테스트 환경에서는 인상적이고 좋은 시도입니다!

Comment on lines 49 to 64
@Override
public Optional<User> getUser(UUID id) {
return Optional.ofNullable(userRepository.loadFromFile().get(id));
public Optional<UserResponseDto> getUser(UUID id) {
Optional<User> user = userRepository.getUser(id);

userStatusRepository.findAllStatus().forEach(userStatus -> {
if (userStatus.getUserId().equals(id)) {
user.get().setOnline(userStatus.isOnline(Instant.now()));
}
});

BinaryContent profileImg = binaryContentRepository.findBinaryContentById(user.get().getId());
user.get().setProfileImage(profileImg.getData());

return Optional.of(createUserResponseDto(user.get()));
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Optional 을 사용하여 반환하는 이유에 대해 조금더 고민하여 보시면 좋을 것 같습니다!
Optional 로 반환된 user를 isPresent 체크나 orElseXXX 등으로 체크나 예외를 던지는 등의 동작없이 get 하여 바로 사용한다면 그냥 null을 반환하는 것과 별다른 차이가 없게됩니다!

Comment on lines +12 to +34
@Setter
@Getter
@ToString
public class MessageResponseDto {
private UUID userId;
private String messageContent;
private UUID messageId;
private List<byte[]> messageFile;

public MessageResponseDto(Message message, List<byte[]> messageFile) {
this.userId = message.getSender();
this.messageContent = message.getContent();
this.messageId = message.getId();
this.messageFile = messageFile;
}

public MessageResponseDto(Message message) {
this.userId = message.getSender();
this.messageContent = message.getContent();
this.messageId = message.getId();
this.messageFile = new ArrayList<>();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

@DaTa, @Setter 는 주의해야 좋습니다!
MessageResponseDto 는 응답용 데이터 전송 객체이므로, 생성된 이후 상태가 변경될 필요가 없습니다.
현재 구현하신 구현 또한 생성자를 통해서 모두 멤버변수를 모두 초기화 하고 계시니 차후 의도치않은 불변성이 깨지는걸 방지하기 위해 불필요한 @Setter 사용은 안하시는걸 권장 드립니다.

현재 구현하신 생성자를 통한 초기화도 좋고, Lombok에 @builder 라는 빌더패턴을 지원하는 어노테이션, 그리고 팩토리 메서드를 통한 초기화도 있으니 세가지 방법 모두 한번 알아보시길 추천드립니다.
개발자별 스타일이나 선호도 차이지만 빌더패턴과 팩토리메서드 모두 자주쓰이는 유형이니 한번 알아보시면 좋습니다!

public List<BinaryContent> findAllBinaryContentById(UUID id);

public boolean deleteBinaryContentById(UUID id);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 인터페이스 설계와 실제 구현 모두 id의 주체가 매우 혼동되어 있습니다 ㅠㅠ.

인터페이스만 보기엔 id가 BinaryContent 자체의 id 기준으로 동작되어야 합니다.
하지만 실제 구현은 검색에는 ownerId가, 삭제에는 BinaryContent의 고유 id 기준으로 동작하고 있습니다.

현재 구현에서는 아래와 같은 메서드명으로 변경하는게 적합하여 보입니다.
findProfileImageByOwnerId(UUID ownerId)
findAllAttachmentsByOwnerId(UUID ownerId)

Interface 만 보고도 동작이 예측 가능해야 합니다!

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