Skip to content

[연예림] sprint3 #178

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
.metadata
bin/
tmp/
log/
*.log
*.tmp
*.bak
*.swp
Expand All @@ -13,6 +15,7 @@ local.properties
.settings/
.loadpath
.recommenders
.github/

# External tool builders
.externalToolBuilders/
Expand Down
18 changes: 18 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
plugins {
id 'java'
id 'checkstyle'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.sprint.mission'
version = '1.0-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.sprint.mission.discodeit;

import com.sprint.mission.discodeit.dto.*;
import com.sprint.mission.discodeit.entity.Channel;
import com.sprint.mission.discodeit.entity.Message;
import com.sprint.mission.discodeit.entity.User;
import com.sprint.mission.discodeit.service.ChannelService;
import com.sprint.mission.discodeit.service.MessageService;
import com.sprint.mission.discodeit.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.List;
import java.util.UUID;

@SpringBootApplication
@Slf4j
public class DiscodeitApplication {

static UserResponse setupUser(UserService userService) {
return userService.createUser(new UserRequest("woody34", "woody34@codeit.com", "woody1234", null));
}

static ChannelResponse setupChannel(ChannelService channelService) {
return channelService.createPublicChannel(new ChannelRequest.CreatePublic("공지", "공지 채널입니다."));
}

static ChannelResponse setupPrivateChannel(ChannelService channelService, List<UUID> joinUsers) {
return channelService.createPrivateChannel(new ChannelRequest.CreatePrivate(joinUsers));
}

static void messageCreateTest(MessageService messageService, UUID channelId, UUID authorId) {
Message message = messageService.createMessage(new MessageRequest.Create("안녕하세요.", channelId, authorId, null));
log.info("메시지 생성: {}", message.getId());
}

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

// 서비스 초기화
// TODO context에서 Bean을 조회하여 각 서비스 구현체 할당 코드 작성하세요.
UserService userService = context.getBean(UserService.class);
ChannelService channelService = context.getBean(ChannelService.class);
MessageService messageService = context.getBean(MessageService.class);

// 셋업
UserResponse user = setupUser(userService);
ChannelResponse channel = setupChannel(channelService);
ChannelResponse privateChannel = setupPrivateChannel(channelService, List.of(user.id()));
// 테스트
messageCreateTest(messageService, channel.id(), user.id());
}
}
124 changes: 58 additions & 66 deletions src/main/java/com/sprint/mission/discodeit/JavaApplication.java
Original file line number Diff line number Diff line change
@@ -1,66 +1,58 @@
package com.sprint.mission.discodeit;

import com.sprint.mission.discodeit.entity.*;
import com.sprint.mission.discodeit.repository.ChannelRepository;
import com.sprint.mission.discodeit.repository.MessageRepository;
import com.sprint.mission.discodeit.repository.UserRepository;
import com.sprint.mission.discodeit.repository.file.FileChannelRepository;
import com.sprint.mission.discodeit.repository.file.FileMessageRepository;
import com.sprint.mission.discodeit.repository.file.FileUserRepository;
import com.sprint.mission.discodeit.repository.jcf.JCFChannelRepository;
import com.sprint.mission.discodeit.repository.jcf.JCFMessageRepository;
import com.sprint.mission.discodeit.repository.jcf.JCFUserRepository;
import com.sprint.mission.discodeit.service.*;
import com.sprint.mission.discodeit.service.basic.BasicChannelService;
import com.sprint.mission.discodeit.service.basic.BasicMassageService;
import com.sprint.mission.discodeit.service.basic.BasicUserService;
import com.sprint.mission.discodeit.service.file.*;
import com.sprint.mission.discodeit.service.jcf.*;

import java.util.List;

public class JavaApplication {

static User setupUser(UserService userService) {
User user = userService.createUser("woody", "woody@codeit.com", "woody1234");
return user;
}

static Channel setupChannel(ChannelService channelService) {
Channel channel = channelService.createChannel(ChannelType.PUBLIC, "공지", "공지 채널입니다.");
return channel;
}

static void messageCreateTest(MessageService messageService, Channel channel, User author) {
Message message = messageService.createMessage("안녕하세요.", channel.getId(), author.getId());
System.out.println("메시지 생성: " + message.getId());
}

public static void main(String[] args) {

// jcf
// System.out.println("===Use JCF Repository===");
// UserRepository userRepository = new JCFUserRepository();
// ChannelRepository channelRepository = new JCFChannelRepository();
// MessageRepository messageRepository = new JCFMessageRepository();

// file
System.out.println("===Use File Repository===");
UserRepository userRepository = new FileUserRepository();
ChannelRepository channelRepository = new FileChannelRepository();
MessageRepository messageRepository = new FileMessageRepository();

// 서비스 초기화
// TODO Basic*Service 구현체를 초기화하세요.
UserService userService = new BasicUserService(userRepository);
ChannelService channelService = new BasicChannelService(channelRepository, userService);
MessageService messageService = new BasicMassageService(messageRepository, channelService, userService);

// 셋업
User user = setupUser(userService);
Channel channel = setupChannel(channelService);
// 테스트
messageCreateTest(messageService, channel, user);

}
}
//package com.sprint.mission.discodeit;
//
//import com.sprint.mission.discodeit.entity.*;
//import com.sprint.mission.discodeit.repository.ChannelRepository;
//import com.sprint.mission.discodeit.repository.MessageRepository;
//import com.sprint.mission.discodeit.repository.UserRepository;
//import com.sprint.mission.discodeit.repository.file.*;
//import com.sprint.mission.discodeit.repository.jcf.*;
//import com.sprint.mission.discodeit.service.*;
//import com.sprint.mission.discodeit.service.basic.BasicChannelService;
//import com.sprint.mission.discodeit.service.basic.BasicMassageService;
//import com.sprint.mission.discodeit.service.basic.BasicUserService;
//
//public class JavaApplication {
//
// static User setupUser(UserService userService) {
// User user = userService.createUser("woody", "woody@codeit.com", "woody1234");
// return user;
// }
//
// static Channel setupChannel(ChannelService channelService) {
// Channel channel = channelService.createChannel(Channel.ChannelType.PUBLIC, "공지", "공지 채널입니다.");
// return channel;
// }
//
// static void messageCreateTest(MessageService messageService, Channel channel, User author) {
// Message message = messageService.createMessage("안녕하세요.", channel.getId(), author.getId());
// System.out.println("메시지 생성: " + message.getId());
// }
//
// public static void main(String[] args) {
//
// // jcf
//// System.out.println("===Use JCF Repository===");
//// UserRepository userRepository = new JCFUserRepository();
//// ChannelRepository channelRepository = new JCFChannelRepository();
//// MessageRepository messageRepository = new JCFMessageRepository();
//
// // file
// System.out.println("===Use File Repository===");
// UserRepository userRepository = new FileUserRepository();
// ChannelRepository channelRepository = new FileChannelRepository();
// MessageRepository messageRepository = new FileMessageRepository();
//
// // 서비스 초기화
//// // TODO Basic*Service 구현체를 초기화하세요.
//// UserService userService = new BasicUserService(userRepository);
//// ChannelService channelService = new BasicChannelService(channelRepository);
//// MessageService messageService = new BasicMassageService(messageRepository, channelService, userService);
////
//// // 셋업
//// User user = setupUser(userService);
//// Channel channel = setupChannel(channelService);
//// // 테스트
//// messageCreateTest(messageService, channel, user);
//
// }
//}
21 changes: 21 additions & 0 deletions src/main/java/com/sprint/mission/discodeit/dto/ChannelRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.sprint.mission.discodeit.dto;

import java.util.List;
import java.util.UUID;

public record ChannelRequest(
) {
public record CreatePublic(
String title,
String description
) {}

public record CreatePrivate(
List<UUID> joinUsers
) {}

public record Update(
String title,
String description
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.sprint.mission.discodeit.dto;

import com.sprint.mission.discodeit.entity.Channel;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

public record ChannelResponse(
UUID id,
String title,
String description,
Channel.ChannelType channelType,
Instant createdAt,
Instant updatedAt,
Instant lastMessageTime,
List<UUID> joinUsers
) {
public static ChannelResponse entityToDto(Channel channel, Instant lastMessageTime, List<UUID> joinUsers) {
return new ChannelResponse(channel.getId(), channel.getTitle(), channel.getDescription(), channel.getChannelType(), channel.getCreatedAt(), channel.getUpdatedAt(), lastMessageTime, joinUsers);
}
}
Comment on lines +19 to +22
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dto 안 메소드로 정의해서 변환하고 있습니다.
변환 메소드를 dto 안에서 정의하면 오히려 사용하기 쉬울 것 같았는데, 찾아보니 mapper 클래스를 별도로 만들어 관리하는 것 같습니다. 관련기능을 관리하기에는 mapper 클래스로 분리하는 게 나을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 정답이 있는 문제는 아닙니다.

modelMapper나 mapStruct를 사용하는 것이 생산성이 좋다!
라고 하는 분들이 계실거고,
modelMapper나 mapStruct를 사용해서 변환하는 것은 생산성에 큰 영향이 없고 관리해야 하는 대상이 더 늘어난다!
라고 하시는 분들이 계십니다.

멘토의 입장에서, 그리고 실무에서 일을 하고 있는 제 입장으로는 '한번쯤은 배우고 갈 만 하지만, 굳이 사용할 필요는 없다'인 것 같아요

몇가지 이유가 있는데요,

  • DTO는 본래 변환을 위한 객체
  • 엔티티와 DTO 변환 생산성이 크지 않다.
  • mapper를 빈으로 등록하여 스프링 관리 대상에 놓아야 한다.

사실 마지막 이유가 가장 꺼려지는 것 같아요. 빈으로 등록하게 되면 결국에는 의존관계를 신경써야 하는 것인데 운영 환경에서 이런 부분까지 신경을 써야 한다는게 조금 부담이 되는 것 같아요

지금은 단순한 스프린트 기간이니 한번쯤은 써보시고, 각각의 장단점과 성능을 비교해보시는 것도 좋을 것 같아요 :D
(개인적으로 아직 실무에서 Mapper를 쓰는 팀이나 회사는 못본것 같아요. 제가 2년차라서 그럴수도 있고요)

저랑 비슷한 의견의 블로그

20 changes: 20 additions & 0 deletions src/main/java/com/sprint/mission/discodeit/dto/MessageRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sprint.mission.discodeit.dto;

import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.UUID;

public record MessageRequest() {
public record Create(
String content,
UUID channelId,
UUID userId,
List<MultipartFile> files
) {}

public record Update(
String content,
List<MultipartFile> files
) {}
}
Comment on lines +8 to +20
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사실 dto 코드는 길지 않고 서로 관련이 있다고 생각해서 하나의 클래스 안에 넣었습니다. 실제로도 이렇게 많이 사용하시는지 궁금합니다.
생각해보니 계속 기능이 늘어나면 dto를 사용하는데 있어서 오히려 복잡해질 수도 있을 것 같습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

네, 참고로 저도 저렇게 쓰고 있습니다 :D record를 쓰진 않고, 인터페이스에 inner class로 쓰고는 있긴 하지만, 저런 방식도 괜찮은 것 같아요.

어차피 대부분 CRUD라서, 도메인 통채로 Response, Request를 하나의 파일로 관리하는 것 아니면 그렇게 크게 늘어나지도 않을 수 있어요

이번 스프린트가 아마 DTO에 대한 내용을 배우셨던 것 같은데요, record를 쓰시기 전에 히스토리를 배우고 가시면 좋을 것 같아요. (시온님이랑 태식님한테도 같은 리뷰를 드렸어요.)

예전에는 record를 Request DTO로 사용할 때는 상당히 불편한(?) 문제를 마주칠 수 있습니다.

  • Jackson 역직렬화 문제 (Deserialization Issue)

Spring에서는 JSON 요청을 객체로 변환할 때 Jackson을 사용해 역직렬화를 수행하는데요. record에서는 기본적으로 기본 생성자가 따로 없습니다. 그래서 아래와 같이 작성해주셔야 합니다.

public record UserRequest(
        @JsonProperty("username") String username,
        @JsonProperty("password") String password,
        @JsonProperty("email") String email,
        @JsonProperty("phoneNumber") String phoneNumber,
        @JsonProperty("profileImageId") UUID profileImageId
//        @JsonProperty("profileImageName") String profileImageName
) {
    // @JsonCreator를 추가하여 명시적으로 생성자를 지정
    @JsonCreator
    public UserRequest(
            @JsonProperty("username") String username,
            @JsonProperty("password") String password,
            @JsonProperty("email") String email,
            @JsonProperty("phoneNumber") String phoneNumber,
            @JsonProperty("profileImageId") UUID profileImageId
            // @JsonProperty("profileImageName") String profileImageName
    ) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.profileImageId = profileImageId;
        // this.profileImageName = profileImageName; // 주석처리된 부분도 필요에 맞게 추가 가능
    }
}

뭔가 상당히 불편해 보이시지 않나요 ^-^..?

@JsonCreator를 통해서 생성자를 명시적으로 지정하여 Jackson이 record 객체를 생성할 때 사용할 생성자를 알 수 있도록 해야 하고, @JsonProperty를 사용해서 JSON 필드 이름과 record 필드명을 매핑시켜줘야 역직렬화가 이루어지게 됩니다.

이러한 이유들 때문에 예전에는 안쓰는 팀도 많았습니다. 단순히 class에 Lombok 어노테이션을 쓰면 간소화 할 수 있기 때문이지요.

그런데 JDK 14부터는 그런 문제들이 해결되었다고 하더라구요. 그렇기 때문에 Record 도입은 버전을 잘 고려하시면서 적용시키면 좋을 것 같습니다

Jackson 2.12
Jackson databind

(나중에 프로젝트를 하시고, 면접에 가셨을 때 DTO를 record를 쓰신 이유를 물어보거나 역직렬화 이슈에 대해 면접관들이 물어보면, 잘 대답해주시면 면접관들이 좋아 죽습니다 😄)

19 changes: 19 additions & 0 deletions src/main/java/com/sprint/mission/discodeit/dto/UserRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.sprint.mission.discodeit.dto;

import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

public record UserRequest(
String name,
String email,
String password,
MultipartFile file
) {

public record Login(
String name,
String password
){
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/sprint/mission/discodeit/dto/UserResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sprint.mission.discodeit.dto;

import com.sprint.mission.discodeit.entity.User;
import com.sprint.mission.discodeit.entity.UserStatus;

import java.time.Instant;
import java.util.UUID;

public record UserResponse(
UUID id,
Instant createdAt,
Instant updatedAt,
String name,
String email,
UserStatus.Status status
) {
public static UserResponse entityToDto(User user, UserStatus userStatus) {
return new UserResponse(user.getId(), user.getCreatedAt(), user.getUpdatedAt(), user.getName(), user.getEmail(), userStatus.getStatus());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.sprint.mission.discodeit.entity;

import lombok.Getter;

import java.io.Serializable;
import java.time.Instant;
import java.util.UUID;

@Getter
public class BinaryContent implements Serializable {
private static final long serialVersionUID = 1L;
private final UUID id;
private Instant createdAt;
private String fileName;
private String contentType;
private byte[] bytes;
private ParentType parentType;
private UUID userId;
private UUID messageId;
Comment on lines +17 to +19
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

필드를 정의할 때 고민했었던 부분이 있습니다.
처음에는 parentType과 parentId로 구성하려고 했었습니다.
'UUID로 id 값이 같으니 오히려 parentId로만 구성한다면 값을 저장할 때 편하지 않을까?' 라고 생각했었습니다.
그래도 걸리는 부분이 2가지가 있었는데요.

  1. 데이터를 가져올 때 항상 parentType을 확인해야하고 잘못된 파일에서 값을 가져올 수도 있겠다는 부분이었습니다.
  2. 나중에 데이터베이스로 연결할 때, 참조되려면 결국 나누는게 맞다고 생각되더라구요. 그런데 생각했던 건 userId와 messageId가 구분된다면 parentType 필드가 굳이 필요할까 라는 생각이 들기도 합니다..

Copy link
Collaborator

Choose a reason for hiding this comment

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

저라면 parentId값만 가지고 올 것 같아요 :D 필요할때마다 즉각즉각 조회하면 되니깐요
(1~2번 추가 쿼리는 성능적으로 큰 영향이 없습니다, 오히려 다른 부분에 더 많은 영향을 끼치기 때문에)
만약에, parentId의 type이 변경이 된다면 당연히 BinaryContent도 조회를 하고 변경을 해줘야 하기 때문에 더 신경써야 하는 부분이 많지 않을까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아 그러네요 ㅎㅎ,,
사실 파일만 가지고 오면 되는데 Message파일인지 , User파일 구분할 필요가 없을 것 같습니다 😂
자꾸 분류해야 한다는 생각을 가지고 있었던 것 같습니다.


public enum ParentType {
USER,
MESSAGE
}

public static BinaryContent createBinaryContent(String fileName, String contentType, byte[] bytes, ParentType parentType, UUID parentId) {
if (parentType == ParentType.USER) {
return new BinaryContent(fileName, contentType, bytes, parentType, parentId, null);
} else {
return new BinaryContent(fileName, contentType, bytes, parentType, null, parentId);
}
}
Comment on lines +26 to +32
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

일단 parentType을 사용하기로 하고 생성자를 만들었습니다. 생성할 때, parentType을 조건으로 두고 생성하고 있습니다.
그런데 entity 안에서 이 로직을 쓰는게 괜찮은 건지 모르겠습니다.
지금 보니 엔티티의 책임이 너무 많은 것 같아 아래 두 가지 방법을 생각했는데요.

  1. 엔티티 안에서 createUserBinaryContent, createMessageBinaryContent로 분리하기
  2. 엔티티는 단순히 생성하고 service 에서 처리하기
    다시 정리해서 쓰다보니 둘 다 적용하면 되겠다는 생각이 드네요 ㅎㅎ..
    정적 팩토리 메서드를 쓰는 이유를 생각해보면 첫 번째를 적용해야할 것 같고, 타입 조건은 service에서 처리해야 할 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

둘 다 틀린 방식은 아닙니다!
생성만 엔티티에 할 것인지, 타입을 보고 생성하는 역할까지 엔티티에게 줄 것인지는 정말 스타일 차이인 것 같아요.

(보통 저는 생성 자체는 엔티티에, 그리고 타입을 보고 어떤 객체를 생성해야하는지는 서비스 계층에서 하는 것 같아요)

Enum을 조금 더 확장해서 다형성을 조금 이용해보면 이런 방식도 있을 것 같아요.

    public enum ParentType {
        USER {
            @Override
            public BinaryContent createBinaryContent(String fileName, String contentType, byte[] bytes, UUID parentId) {
                return new BinaryContent(fileName, contentType, bytes, this, parentId, null);
            }
        },
        MESSAGE {
            @Override
            public BinaryContent createBinaryContent(String fileName, String contentType, byte[] bytes, UUID parentId) {
                return new BinaryContent(fileName, contentType, bytes, this, null, parentId);
            }
        };

        public abstract BinaryContent createBinaryContent(String fileName, String contentType, byte[] bytes, UUID parentId);
    }
// 호출부
BinaryContent binaryContentForUser = ParentType.USER.createBinaryContent(xxx);

어떤 방식이든 좋습니다. 예림님이 생각이 담겨있는 코드로 작성해주세요 :D


private BinaryContent(String fileName, String contentType, byte[] bytes, ParentType parentType, UUID userId, UUID messageId) {
this.id = UUID.randomUUID();
this.createdAt = Instant.now();
this.fileName = fileName;
this.bytes = bytes;
this.contentType = contentType;
this.parentType = parentType;
this.userId = userId;
this.messageId = messageId;
}

@Override
public String toString() {
return "BinaryContent{id:" + id
+ ",userId:" + userId
+ ",createdAt:" + createdAt
+ ",fileName:" + fileName
+ ",contentType:" + contentType
+ ",parentType:" + parentType
+ ",userId:" + userId
+ ",messageId:" + messageId
+ "}";
}

}
Loading