Notice
Recent Posts
Recent Comments
Link
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

코딩 이래요래

Sprint Mission Part 2, 3 본문

JAVA/Mission

Sprint Mission Part 2, 3

강범호 2025. 4. 30. 14:28

Sprint Mission Part 2, 3 요구사항 정리

본 글에서는 스프린트 미션 Part-2, 3에서 진행한
Java, SpringBoot를 활용한 디스코드(Discord) 서비스 도메인 모델링 및 CRUD 서비스 구현 내용을 정리함


✅ 미션 요구사항 개요

  • Mission Part 1에서 진행한 프로젝트 고도화
  • File I/O를 활용한 객체 직렬화/역직렬화 기능 구현
  • 기존 Java 프로젝트를 Spring 프로젝트로 마이그레이션
  • 각 객체들의 의존성 관리를 IoC Container에 위임하도록 리팩토링
  • 새로운 도메인 추가
    • ReadStatus : 사용자가 채널별로 마지막으로 메세지를 읽은 시간을 표현하는 도메인
      • 사용자별 각 채널에 읽지 않은 메세지를 확인하기 위해 활용될 예정
    • UserStatus : 사용자 별 마지막으로 접속한 시간을 표현하는 도메인
      • 사용자의 온라인 상태를 확인하기 위해 사용될 예정
      • 마지막 접속 시간이 현재 시간으로 부터 5분 이내면 접속중인 유저로 간주하는 메소드 선언
    • BinaryContent : 이미지, 파일 등 바이너리 테이터를 표현하는 도메인\
      • 사용자의 프로필 이미지, 메시지의 첨부파일을 저장하기 위함
      • 수정은 불가 updatedAt 필드 X
      • User, Message 도메인 모델의 의존관계를 고려하여 id 참조 필드 추가
  • 각 서비스 비즈니스 로직 고도화
  • Repository 구현체의 조건부 Bean 구성 (application.yaml)
  • FileRepository 구현체의 파일을 저장할 디렉토리 경로 설정 (application.yaml)

📌 목차

1️⃣ File I/O 활용 객체 영속화

① 각 도메인 수정

  • 객체 직렬화를 위한 각 도메인 Serializable implements

② File*Repository 정의

public Map<UUID, User> loadFromFile() {
    File file = new File(FILE_PATH);
    if (!file.exists()) return new HashMap<>();

    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
        return (Map<UUID, User>) ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
        return new HashMap<>();
    }
}

public void saveToFile(Map<UUID, User> users) {
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) {
        oos.writeObject(users);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 각 Repsitory 객체 저장 및 불러오기 로직 정의

③ Java 프로젝트에서 Spring 프로젝트로 마이그레이션

  • Spring Initializr를 통해 Java 프로젝트 덮어쓰기

④ 의존성 관리 IoC Container로 위임

  • 각 Service, Repository @Service, @Repository 어노테이션 선언
    • @Service, @Repository 내부에 @Component 포함하고 있음

  • @SpringBootApplication 어노테이션을 붙은 클래스인 DiscodeitApplication 클래스 실행
  • @SpringBootApplication 내부 @ComponentScan 어노테이션이 @Component 계열을 찾아 Bean으로 등록

⑤ 각 서비스 비즈니스 로직 고도화

1. UserService

  • create
    • DTO를 활용하여 파라미터 그룹화
    • User 생성 후 UserStatus 및 UserProfileImage BinaryContent 생성

 

  • find
    • UserResponseDto를 이용해 User 정보 전달
    • UserStatus, UserProfile 정보 포함

2. ChannelService

  • create
    • DTO를 활용하여 메소드 오버로딩을 이용해 Public, Private 채널 생성 구분
    • Private 채널 생성 시 참여하는 각 User별 ReadStatus 생성

  • find
    • DTO를 활용하여 메소드 오버로딩을 이용해 Public, Private 채널 조회 구분
    • ChannelResponseDto를 이용해 정보 전달
    • 공통으로 해당 채널의 마지막 메세지 전달 시간 포함
    • Private 채널일 경우 해당 채널 참여 User들의 Id 정보 포함

3. MessageService

  • create
    • DTO를 활용하여 파라미터 그룹화
    • Message는 여러개의 파일을 포함할 수 있음

  • find
    • 파라미터로 채널의 id를 받아 해당 채널의 모든 메세지 조회
    • 각 메세지가 포함한 파일 정보들도 포함

4. BinaryContent

  • BinaryContent Entity
    • ownerId를 통해 파일 다중 저장 가능 (메세지의 경우)

 

  • BinaryContent Repository
    • ownerId를 Key로 저장하여 메세지는 다중 저장, 유저 프로필은 삭제 후 업로드

  •  create
    • UserProfile은 최대 1개만 업로드 가능
    • MessageFile은 여러개 업로드 가능

⑥ 조건부 Bean 구성 (application.yaml)

  • application.yaml 설정
    • file → File*Repository Bean 등록
    • jcf → JCF*Repository Bean 등록
discodeit:
  repository:
    type: file | jcf

 

  • File*Repository Class
    • name : ymal에 정의한 Key
    • havingValue : yaml에 정의한 Value
@Repository
@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file")
public class FileChannelContentRepository implements ChannelRepository {
	// 코드
}

 

  • JCF*Repository Class
@Repository
@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf")
public class JCFChannelRepository implements ChannelRepository {
	// 코드 
}

 

  • DiscodeitApplication Class
@SpringBootApplication
public class DiscodeitApplication {

    public static void main(String[] args) {
		// ApplicationContext 생성
            ApplicationContext context = SpringApplication.run(DiscodeitApplication.class, args);

            DiscodeitApplication app = context.getBean(DiscodeitApplication.class);
            app.createRepositoryDirectory();
            
    		// ApplicationContext로 등록된 bean 가져오기
            UserService userService = context.getBean(UserService.class);
            ChannelService channelService = context.getBean(ChannelService.class);
            MessageService messageService = context.getBean(MessageService.class);
            
            // 코드
	}
}

⑦ File*Repository 구현체의 파일을 저장할 디렉토리 설정 (application.yaml)

  • application.yaml 설정
discodeit:
  repository:
    type: file
    file-directory: .discodeit
  • Application이 실행되면 폴더 생성

🎯 미션 회고 및 후기

느낀 점 및 배운 점

  • 프로젝트의 초기 설계의 중요성
    • mission 2, 3 을 진행하면서 Spring 프로젝트로 변경했기 때문에 수정사항이 많은 부분도 있었지만, 작은 기능 하나를 추가하더라도 여러 부분을 함께 수정해야 하는 상황이 자주 발생했고, 이를 통해 프로젝트 설계의 중요성을 깨달았음
    • OCP를 더 신중하게 고려하고 설계했다면, 미션을 진행하는 시간을 좀 더 단축시킬 수 있지 않을까 하는 생각이 듬
  • ConcurrentModificationException
    • delete 로직에 for loop를 순회하면서 remove() 메소드를 호출하는 부분이 많았음
    • for 루프를 순회하는 도중 원본의 데이터를 삭제하면 Collection구조가 변경되 때문에 ConcurrentModificationException이 발생했음
    • removeIf() 메소드나, Iterator, 원본 객체를 복사해서 순회하는 방법으로 ConcurrntModificationException 예외를 방지할 수 있었음

'JAVA > Mission' 카테고리의 다른 글

Sprint Mission Part-1  (0) 2025.04.07