Skip to content

Conversation

@HuInDoL
Copy link
Collaborator

@HuInDoL HuInDoL commented May 4, 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
      public static void main(String[] args) {
          // 레포지토리 초기화
          // ...
          // 서비스 초기화
            UserService userService = new BasicUserService(userRepository);
          ChannelService channelService = new BasicChannelService(channelRepository);
          MessageService messageService = new BasicMessageService(messageRepository, channelRepository, userRepository);
      
          // ...
      }
      
      // DiscodeitApplication
      public static void main(String[] args) {
              ConfigurableApplicationContext context = SpringApplication.run(DiscodeitApplication.class, args);
          // 서비스 초기화
              // TODO context에서 Bean을 조회하여 각 서비스 구현체 할당 코드 작성하세요.
            UserService userService;
          ChannelService channelService;
          MessageService messageService;
      
          // ...
      }
    • JavaApplication의 main 메소드의 셋업, 테스트 부분의 코드를 DiscodeitApplication클래스로 복사하세요.

      public static void main(String[] args) {
          // ...
          // 셋업
          User user = setupUser(userService);
          Channel channel = setupChannel(channelService);
          // 테스트
          messageCreateTest(messageService, channel, user);
      }

Lombok 적용

  • 도메인 모델의 getter 메소드를 @Getter로 대체해보세요.
  • [ ] Basic*Service의 생성자를 @RequiredArgsConstructor로 대체해보세요.

추가 기능 요구사항

비즈니스 로직 고도화

시간 타입 변경하기

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

새로운 도메인 추가하기

  • 도메인 모델 간 참조 관계를 참고하세요.

  • 공통: 앞서 정의한 도메인 모델과 동일하게 공통 필드(id, createdAt, updatedAt)를 포함합니다.

  • ReadStatus

    • 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다.
  • UserStatus

    • 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다.
    • 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요.
      • 마지막 접속 시간이 현재 시간으로부터 5분 이내이면 현재 접속 중인 유저로 간주합니다.
  • BinaryContent

    • 이미지, 파일 등 바이너리 데이터를 표현하는 도메인 모델입니다. 사용자의 프로필 이미지, 메시지에 첨부된 파일을 저장하기 위해 활용합니다.
    • 수정 불가능한 도메인 모델로 간주합니다. 따라서 updatedAt 필드는 정의하지 않습니다.
    • [ ] UserMessage 도메인 모델과의 의존 관계 방향성을 잘 고려하여 id 참조 필드를 추가하세요.
  • 각 도메인 모델 별 레포지토리 인터페이스를 선언하세요.

    • 레포지토리 구현체(File, JCF)는 아직 구현하지 마세요. 이어지는 서비스 고도화 요구사항에 따라 레포지토리 인터페이스에 메소드가 추가될 수 있어요.

DTO 활용하기

[DTO란?](https://www.codeit.kr/topics/6789bcf706985c5b264f9ee9/lessons/679c2fe348fe9b1f66b1df32)

UserService 고도화

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

AuthService 구현

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

ChannelService 고도화

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

MessageService 고도화

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

ReadStatusService 구현

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

UserStatusService 고도화

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

BinaryContentService 구현

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

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

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

심화 요구사항

Bean 다루기

  • [ 세모 ] Repository 구현체 중에 어떤 구현체를 Bean으로 등록할지 Java 코드의 변경 없이 application.yaml 설정 값을 통해 제어해보세요.

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

Spring 핵심 개념 이해하기

  • [ ] [ ] JavaApplication과 DiscodeitApplication에서 Service를 초기화하는 방식의 차이에 대해 다음의 키워드를 중심으로 정리해보세요.
    • IoC Container
    • Dependency Injection
    • Bean

이 내용은 PR에 첨부해주세요.

  • JavaApplication은 순수 Java 코드로 Service 객체를 수동으로 생성하여 사용한다. 또한 new 키워드를 사용하여 Service를 명시적으로 생성하고, 생성자나 메서드 파라미터를 통해 의존성을 주입한다. 한마디로 자동 의존성 주입 없이 수동으로 객체를 생성한다.
  • DiscodeitApplication은 Spring Framework를 활용한 Spring Boot 애플리케이션으로 @service 어노테이션과 @bean을 통해 Spring Boot가 초기화와 의존성 주입을 자동으로 한다. 이는 코드의 간결함과 유지 보수성을 높여주는 방식이다.

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.
  • 늦게 제출드려 죄송합니다. 부끄럽지만 완성할 수 있을 줄 알고 늦장부리다가 완성도 못하고 git push 하는 방법도 다시 찾아보느라 늦어버렸습니다.
  • 설계를 제 방식으로 하느라 뭔가 정리되지 않고 위임도 잘 안돼서 제멋대로였는데, 프로젝트 시작 전 요구사항에 따라 설계를 할 때에 무엇을 중점에 두고 요구사항의 '체크리스트(?)'를 작성해야 하는지 헷갈렸습니다.
    멘토님도 제 글을 보고 어리둥절 하실 것 같은데, 제 말은 요구사항을 정리하여 어떻게 작업을 할 지 구상할 때, 어느 정도까지 지침을 두고 시작해야 하는지, 예를 들어서 클래스와 메서드, 메서드의 파라미터, 메서드의 리턴 값을 구상하고, 각 연결관계를 위임으로 연결하는 것까지 해야 하는지 그 이상 혹은 그 이하인지 잘 모르겠습니다. 멘토님이 처음 요구사항을 받고 구상하시는 방법을 살짝 알려주시면 정말 감사하겠습니다.

@HuInDoL HuInDoL changed the base branch from 이지현 to 임정현 May 7, 2025 01:00
@HuInDoL HuInDoL added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다. labels May 7, 2025
@codingjigi codingjigi self-requested a review May 7, 2025 14:26
Copy link
Collaborator

@codingjigi codingjigi left a comment

Choose a reason for hiding this comment

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

이전보다 훨씬 성장하셨네요.. 노력한만큼 결과가 나오고 있는 것 같아서 저도 기분이 좋습니다~
고생 많으셨어요!

//}

@Configuration
public class AppConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

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

config 패키지를 따로 만들어 책임을 분리하는 방법은 어떠세요?

// return user;
// }

static Channel setupChannel(ChannelService channelService) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

모의 객체 생성을 위한 클래스를 만들어서 책임을 분리하는게 좋을 것 같아요. 메인 메서드는 실행만 두는게 좋습니다!

Channel channel = setupChannel(channelService);
setupMessage(messageService, channel, user);
// 테스트
messageCreateTest(messageService, channel, user);
Copy link
Collaborator

Choose a reason for hiding this comment

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

테스트도 이제 테스트 패키지에서 직접 실습해봐도 좋을 것 같아요. 잘하셨네요!

this.readStatus = null; // 채널을 읽은 유저들의 상태
}

// public UUID getId() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

사용하지 않는 코드는 제거해주세요~

package com.sprint.mission.discodeit.entity;

public enum ChannelType {
PUBLIC,
Copy link
Collaborator

Choose a reason for hiding this comment

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

}

public boolean isCurrenlyLoggedIn() {
return lastLoginAt.plusSeconds(300).isAfter(Instant.now()); // true: 로그인
Copy link
Collaborator

Choose a reason for hiding this comment

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

300 이라는 숫자가 의미를 가지고 있는 매직넘버라면 상수로 처리하는게 좋아보여요.


@Bean
public UserStatusRepository userStatusRepository() {
if ("file".equalsIgnoreCase(repositoryType)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

잘하셨네요. 혹시 이렇게 구현하신 이유를 알 수 있을까요?


public class JCFBinaryContentRepository implements BinaryContentRepository {

Map<UUID, BinaryContent> binaryContentMap;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Map 은 동시성 이슈가 발생할 수 있어서, 동시성 이슈가 발생하지 않는 자료구조를 한번 찾아보세요~

Map<UUID, List<ReadStatus>> readStatusMapWithUserId;

public JCFReadStatusRepository() {
this.readStatusMap = new HashMap<>(); // 이게 얼마나 비효율적일까
Copy link
Collaborator

Choose a reason for hiding this comment

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

오.. 목적에 맞게 잘 설계하셨네요! 비효율적인건 없어요~ 사람마다 구현방법은 다르니까요

.stream()
.toList();

for (User user : userList) { // O(n) 시간 복잡도
Copy link
Collaborator

Choose a reason for hiding this comment

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

@codingjigi codingjigi merged commit b6e0c4c into codeit-bootcamp-spring:임정현 May 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성🛠️ 스프린트 미션 제출일이지만 미완성했습니다. 죄송합니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants