코딩 이래요래
Sprint Mission Part 2, 3 본문
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 참조 필드 추가
- ReadStatus : 사용자가 채널별로 마지막으로 메세지를 읽은 시간을 표현하는 도메인
- 각 서비스 비즈니스 로직 고도화
- 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 |
---|