-
Notifications
You must be signed in to change notification settings - Fork 57
[연예림] 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
The head ref may contain hidden characters: "part1-\uC5F0\uC608\uB9BC-sprint3"
[연예림] sprint3 #178
Changes from all commits
0aab58e
ec5e6d1
06007c6
3d0cb71
6aaed2e
f3c8351
b9929d5
2b0fb6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()); | ||
} | ||
} |
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); | ||
// | ||
// } | ||
//} |
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); | ||
} | ||
} | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 dto 코드는 길지 않고 서로 관련이 있다고 생각해서 하나의 클래스 안에 넣었습니다. 실제로도 이렇게 많이 사용하시는지 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네, 참고로 저도 저렇게 쓰고 있습니다 :D record를 쓰진 않고, 인터페이스에 inner class로 쓰고는 있긴 하지만, 저런 방식도 괜찮은 것 같아요. 어차피 대부분 CRUD라서, 도메인 통채로 Response, Request를 하나의 파일로 관리하는 것 아니면 그렇게 크게 늘어나지도 않을 수 있어요 이번 스프린트가 아마 DTO에 대한 내용을 배우셨던 것 같은데요, record를 쓰시기 전에 히스토리를 배우고 가시면 좋을 것 같아요. (시온님이랑 태식님한테도 같은 리뷰를 드렸어요.) 예전에는 record를 Request DTO로 사용할 때는 상당히 불편한(?) 문제를 마주칠 수 있습니다.
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 도입은 버전을 잘 고려하시면서 적용시키면 좋을 것 같습니다 (나중에 프로젝트를 하시고, 면접에 가셨을 때 DTO를 record를 쓰신 이유를 물어보거나 역직렬화 이슈에 대해 면접관들이 물어보면, 잘 대답해주시면 면접관들이 좋아 죽습니다 😄) |
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 | ||
){ | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필드를 정의할 때 고민했었던 부분이 있습니다.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저라면 parentId값만 가지고 올 것 같아요 :D 필요할때마다 즉각즉각 조회하면 되니깐요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 그러네요 ㅎㅎ,, |
||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일단 parentType을 사용하기로 하고 생성자를 만들었습니다. 생성할 때, parentType을 조건으로 두고 생성하고 있습니다.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
+ "}"; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dto 안 메소드로 정의해서 변환하고 있습니다.
변환 메소드를 dto 안에서 정의하면 오히려 사용하기 쉬울 것 같았는데, 찾아보니 mapper 클래스를 별도로 만들어 관리하는 것 같습니다. 관련기능을 관리하기에는 mapper 클래스로 분리하는 게 나을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 정답이 있는 문제는 아닙니다.
modelMapper나 mapStruct를 사용하는 것이 생산성이 좋다!
라고 하는 분들이 계실거고,
modelMapper나 mapStruct를 사용해서 변환하는 것은 생산성에 큰 영향이 없고 관리해야 하는 대상이 더 늘어난다!
라고 하시는 분들이 계십니다.
멘토의 입장에서, 그리고 실무에서 일을 하고 있는 제 입장으로는 '한번쯤은 배우고 갈 만 하지만, 굳이 사용할 필요는 없다'인 것 같아요
몇가지 이유가 있는데요,
사실 마지막 이유가 가장 꺼려지는 것 같아요. 빈으로 등록하게 되면 결국에는 의존관계를 신경써야 하는 것인데 운영 환경에서 이런 부분까지 신경을 써야 한다는게 조금 부담이 되는 것 같아요
지금은 단순한 스프린트 기간이니 한번쯤은 써보시고, 각각의 장단점과 성능을 비교해보시는 것도 좋을 것 같아요 :D
(개인적으로 아직 실무에서 Mapper를 쓰는 팀이나 회사는 못본것 같아요. 제가 2년차라서 그럴수도 있고요)
저랑 비슷한 의견의 블로그