2025. 3. 9. 20:04ㆍProject
이번에는 Member 관련 기능에 대해 다뤄 볼 것이다.
우선 멤버 엔티티이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "kokoa_id")
private Long kokoaId;
@Column(name = "friend_code", unique = true)
private String friendCode;
@Column(nullable = false, unique = true, name = "login_id")
private String loginId;
@Column(nullable = false)
private String password;
@Column
private String nickname;
@Enumerated(EnumType.STRING)
@Column(name = "role")
private Role role;
private String bio;
@Column(name = "profile_url")
private String profileUrl;
@Column(name = "background_url")
private String backgroundUrl;
@Column(name = "remember_me")
private Boolean rememberMe;
@Builder
public Member(String loginId, String password, String bio, String profileUrl, String backgroundUrl, String nickname, Boolean rememberMe) {
this.loginId = loginId;
this.password = password;
this.role = Role.MEMBER;
this.friendCode = loginId;
this.bio = bio != null ? bio : "상태 메시지를 적용해보세요";
this.profileUrl = profileUrl != null ? profileUrl : "https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_default_image.png";
this.backgroundUrl = backgroundUrl != null ? backgroundUrl : "https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_background.jpg";
this.nickname = nickname;
this.rememberMe = rememberMe != null ? rememberMe : false;
}
public void rememberMe(Boolean rememberMe) {
this.rememberMe = rememberMe;
}
public void updateProfileUrl(String profileUrl) {
if (profileUrl == null || profileUrl.isBlank()) {
throw new IllegalArgumentException("프로필 이미지는 비어 있을 수 없습니다.");
}
this.profileUrl = profileUrl;
}
public void updateBackgroundUrl(String backgroundUrl) {
if (backgroundUrl == null || backgroundUrl.isBlank()) {
throw new IllegalArgumentException("배경 이미지는 비어 있을 수 없습니다.");
}
this.backgroundUrl = backgroundUrl;
}
public void updateBio(String bio) {
this.bio = bio;
}
public void deleteProfileImage() {
this.profileUrl = "https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_default_image.png";
}
public void deleteBackgroundImage() {
this.backgroundUrl = "https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_background.jpg";
}
public void validateFriendShip(Member friend) {
if (this.equals(friend)) {
throw new CustomException(ExceptionCode.CANNOT_ADD_SELF);
}
}
}
Member 엔티티에는 여러 메서드가 포함되어 있는데, 이는 Member와 관련된 데이터 변경 로직을 엔티티 내부에서 처리할 수 있도록 하여 응집도를 높이기 위함이다. 이를 통해 서비스와 엔티티 간의 책임을 분리해, 서비스는 비즈니스 흐름을 조율하고, 엔티티는 자신의 상태를 스스로 관리할 수 있도록 설계한것이다.
솔직히 말하자면 이건 DDD를 따라한 무언가 정도가 된다.. 애초에 DDD를 노리고 설계했다고 하기보다 김영한님 강의에서 도메인에 비즈니스 로직을 집어넣는걸 보고 저렇게 설계하면 뭐가 좋을까? 하고 무작정 따라한것이긴 하다.
하지만 무작정 따라하고 보니 확실히 책임이 분리 된다는 점을 깨달았다. 데이터 변경로직을 내부적으로 처리해서 응집도도 높아지고 코드 가독성도 좋아지고..
하지만 무작정 따라하기보다 왜 이렇게 코드를 설계했을까를 한번정도는 생각하고 따라하는게 좋을것 같다. 물론 이번엔 운좋게도 코드를 작성하고나서 바로 깨달았지만 작성하고도 모르겠으면 따라한 의미가 없기 때문이다.
여기서는 정적스태틱 메서드로 왜 create를 안만들었나? 에대한 의문이 있을 수 있는데
사실 빌더패턴도 잘 안써봐서 어떤 방식이 더 편할지를 고민하면서 여러 방법을 써봤는데 개인적인 생각으로는
정적패턴 메서드를 그냥 빌더로 return 해주는게 젤 좋은 방법이 아닐까 생각이든다. 굳이 필드가 많지 않으면
빌더를 이용하지 않아도 되고 물론 필드가 많다면 빌더를 이용하면 훨씬 편리해진다.
이제 서비스 코드를 봐보자
S3 서비스를 이용해서 프로필 이미지 업로드 관련 로직을 처리하고있다. 사실 이미지 업로드 관련로직을 채팅에서도 써야한다는 점을 잊고 있다가 채팅에도 이미지 업로드 로직을 만들어버려서 중복 코드가 발생하고 말았다. 그래서 중복코드가 발생된 부분을 S3 관련 공통 서비스로 분리했다. 이렇게 중복코드를 없애면서 이미지 업로드부분은 S3가 맡고 Member에 관한 정보를 관리하는 부분은 MemberService가 맡게 되면서 책임도 분리되면서 더 좋은 코드가 되었다.
@Service
@RequiredArgsConstructor
@Slf4j
public class MemberService {
private final MemberRepository memberRepository;
private final S3Service s3Service;
public Member findByLoginId(String loginId) {
return memberRepository.findByLoginId(loginId).orElseThrow(() ->
new CustomException(ExceptionCode.ID_MISSMATCH));
}
@Transactional
public void uploadProfileImage(MultipartFile multipartFile, String accountId) {
String profileUrl = s3Service.uploadFile(multipartFile, "profile/");
Optional<Member> memberOptional = memberRepository.findByLoginId(accountId);
Member member = memberOptional.orElseThrow(() -> new CustomException(ExceptionCode.MEMBER_NOT_FOUND));
if (!member.getProfileUrl().equals("https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_default_image.png")) {
s3Service.deleteFileByUrl(member.getProfileUrl());
}
member.updateProfileUrl(profileUrl);
memberRepository.save(member);
}
@Transactional
public void uploadBackgroundImage(MultipartFile multipartFile, String accountId) {
String backgroundUrl = s3Service.uploadFile(multipartFile, "background/");
Member member = memberRepository.findByLoginId(accountId)
.orElseThrow(() -> new CustomException(ExceptionCode.MEMBER_NOT_FOUND));
if (!member.getBackgroundUrl().equals("https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_background.jpg")) {
s3Service.deleteFileByUrl(member.getBackgroundUrl());
}
member.updateBackgroundUrl(backgroundUrl);
memberRepository.save(member);
}
@Transactional
public void deleteProfileImage(String accountId) {
Optional<Member> memberOptional = memberRepository.findByLoginId(accountId);
Member member = memberOptional.orElseThrow(() -> new CustomException(ExceptionCode.MEMBER_NOT_FOUND));
String profileUrl = member.getProfileUrl();
if (profileUrl.equals("https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_default_image.png")) {
throw new CustomException(ExceptionCode.CANNOT_DELETE_DEFAULT_IMAGE);
}
s3Service.deleteFileByUrl(profileUrl);
member.deleteProfileImage();
memberRepository.save(member);
}
@Transactional
public void deleteBackgroundImage(String accountId) {
Optional<Member> memberOptional = memberRepository.findByLoginId(accountId);
Member member = memberOptional.orElseThrow(() -> new CustomException(ExceptionCode.MEMBER_NOT_FOUND));
String backgroundUrl = member.getBackgroundUrl();
if (backgroundUrl.equals("https://kokoatalk-bucket.s3.ap-northeast-2.amazonaws.com/kokoatalk_background.jpg")) {
throw new CustomException(ExceptionCode.CANNOT_DELETE_DEFAULT_IMAGE);
}
s3Service.deleteFileByUrl(backgroundUrl);
member.deleteBackgroundImage();
memberRepository.save(member);
}
@Transactional
public void updateBio(String bio, String accountId) {
Optional<Member> memberOptional = memberRepository.findByLoginId(accountId);
Member member = memberOptional.orElseThrow(() -> new CustomException(ExceptionCode.MEMBER_NOT_FOUND));
member.updateBio(bio);
memberRepository.save(member);
}
}
Member 엔티티는 MySql에 저장하기때문에 젤 많이 사용해본 DB라 그런지 큰 이슈는 없었다.
하지만 여기서 중요한 부분은 도메인에 어째서 비즈니스로직을 넣는지 그리고 책임분리를 해야하는 이유등을 알 수 있었다.
다음은 Friend 관련 기능을 알아보자
'Project' 카테고리의 다른 글
카카오톡 클론코딩(6) - ChatMessage (0) | 2025.03.12 |
---|---|
카카오톡 클론코딩(4) - Friend (0) | 2025.03.10 |
카카오톡 클론코딩(2) - Auth (0) | 2025.03.08 |
카카오톡 클론코딩(1) 프로젝트 설명 (0) | 2025.03.07 |
팀프로젝트 쿼리성능최적화(QueryDsl)-게시글 (0) | 2024.08.31 |