Skip to content
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

[jinyoungchoi95-issue16] 현재 유저 정보 반환 및 업데이트 기능 구현 #33

Merged
merged 28 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ed3c067
[#16] feat: UserService findById 검색 기능 구현
jinyoungchoi95 Aug 19, 2021
4afceb9
[#16] test: join test docs optional 추가
jinyoungchoi95 Aug 20, 2021
beadb72
[#16] feat: "GET /api/user" api 추가 및 테스트
jinyoungchoi95 Aug 20, 2021
9bb2f86
[#16] refactor
jinyoungchoi95 Aug 20, 2021
061f34e
[#16] refactor: Duplicated 메소드 분리 (username, email)
jinyoungchoi95 Aug 20, 2021
2c1f363
[#16] feat: user update 메소드 생성 및 테스트
jinyoungchoi95 Aug 21, 2021
8768ac9
[#16] feat: 'PUT /api/user' user update api 구현 및 테스트
jinyoungchoi95 Aug 21, 2021
e97bff4
[#16] refactor: security context holder 내에 token 생성 기능 메소드 분리
jinyoungchoi95 Aug 21, 2021
b2b4397
[#16] feat: user response json 내에 null값 들어가도록 변경
jinyoungchoi95 Aug 21, 2021
fe92682
[#16] test: beforeEach SecurityContextHolder clear 추가
jinyoungchoi95 Aug 21, 2021
730749f
chore: 구아바 implementation 추가
jinyoungchoi95 Aug 26, 2021
b6f5143
chore: apache commons library 추가
jinyoungchoi95 Aug 26, 2021
7b0e701
feat: password valid guava 전환 및 테스트 수정
jinyoungchoi95 Aug 26, 2021
28f49ab
feat: password valid guava 전환으로 관련 클래스 로직 변경
jinyoungchoi95 Aug 26, 2021
d2158e4
[#16] refactor
jinyoungchoi95 Aug 26, 2021
a1e2048
[#16] refactor: 더티체킹으로 필요없는 로직 제거
jinyoungchoi95 Aug 27, 2021
7862de0
[#16] refactor
jinyoungchoi95 Aug 27, 2021
8f32d22
[#16] refactor
jinyoungchoi95 Aug 27, 2021
15c4dfa
[#16] refactor
jinyoungchoi95 Aug 27, 2021
7616243
[#16] refactor
jinyoungchoi95 Aug 27, 2021
f7eb279
[#16] refactor: password change 메소드 수정
jinyoungchoi95 Aug 27, 2021
3b1a5b2
[#16] refactor
jinyoungchoi95 Aug 27, 2021
ca19bfd
[#16] feat: username validation guava 전환
jinyoungchoi95 Aug 27, 2021
d02b358
[#16] feat: email validation guava 전환
jinyoungchoi95 Aug 28, 2021
845a389
[#16] refactor: check duplicate 및 change 메소드 통합
jinyoungchoi95 Aug 28, 2021
6bb273f
[#16] refactor
jinyoungchoi95 Aug 28, 2021
c1fbfad
[#16] refactor
jinyoungchoi95 Aug 28, 2021
2aabc23
[#16] refactor: UserUpdateRequest dto Optional 제거
jinyoungchoi95 Aug 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation group: 'com.google.guava', name: 'guava', version: '30.1.1-jre'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
import com.study.realworld.security.JwtService;
import com.study.realworld.user.controller.request.UserJoinRequest;
import com.study.realworld.user.controller.request.UserLoginRequest;
import com.study.realworld.user.controller.request.UserUpdateRequest;
import com.study.realworld.user.controller.response.UserResponse;
import com.study.realworld.user.domain.Email;
import com.study.realworld.user.domain.Password;
import com.study.realworld.user.domain.User;
import com.study.realworld.user.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -44,4 +49,25 @@ public ResponseEntity<UserResponse> login(@RequestBody UserLoginRequest request)
.body(fromUserAndToken(user, jwtService.createToken(user)));
}

@GetMapping("/user")
public ResponseEntity<UserResponse> getCurrentUser(@AuthenticationPrincipal Long loginId) {
User user = userService
.findById(loginId).orElseThrow(RuntimeException::new); // 임시
Copy link

Choose a reason for hiding this comment

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

Suggested change
User user = userService
.findById(loginId).orElseThrow(RuntimeException::new); // 임시
User user = userService.findById(loginId)
.orElseThrow(RuntimeException::new); // 임시
Suggested change
User user = userService
.findById(loginId).orElseThrow(RuntimeException::new); // 임시
User user = userService.findById(loginId).orElseThrow(RuntimeException::new); // 임시

위 2방법 중 하나로 하면 가독성이 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

ide 내 code style 적용이 잘못되어서 폭이 짧아 어쩔수없이 현재와 같이 설정했었는데 두번째 말씀드린 방법으로 수정하였습니다 :)

return ResponseEntity.ok()
.body(fromUserAndToken(user, getTokenByContextHolder()));
}

@PutMapping("/user")
public ResponseEntity<UserResponse> update(@RequestBody UserUpdateRequest request,
@AuthenticationPrincipal Long loginId) {
User user = userService
.update(request.toUserUpdateModel(), loginId);
return ResponseEntity.ok()
.body(fromUserAndToken(user, getTokenByContextHolder()));
}

private String getTokenByContextHolder() {
return SecurityContextHolder.getContext().getAuthentication().getCredentials().toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.study.realworld.user.controller.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.study.realworld.user.domain.Email;
import com.study.realworld.user.domain.Password;
import com.study.realworld.user.domain.Username;
import com.study.realworld.user.service.model.UserUpdateModel;
import java.util.Optional;

@JsonTypeName(value = "user")
@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME)
public class UserUpdateRequest {
Copy link

Choose a reason for hiding this comment

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

요청 별 DTO 분리 좋습니다~ 👍


@JsonProperty("username")
private String username;

@JsonProperty("email")
private String email;

@JsonProperty("password")
private String password;

@JsonProperty("bio")
private String bio;

@JsonProperty("image")
private String image;

protected UserUpdateRequest() {
}
Comment on lines +31 to +32
Copy link

Choose a reason for hiding this comment

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

상속 받는 구조가 없는 것 같고,
jsonpath() 사용하시는걸로 알아서 제거해도 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

jsonpath를 사용하고있기는한데 object mapper가 기본적으로 요구하는 부분에 대해서는 놔두기로 했어요.
어짜피 기본생성자는 컴파일러가 생성해주고, 기본생성자를 컴파일러가 생성해준다고해도, 직접 기본생성자를 생성해두는 편이기는 해서요 :)


public String getUsername() {
return username;
}

public String getEmail() {
return email;
}

public String getPassword() {
return password;
}

public String getBio() {
return bio;
}

public String getImage() {
return image;
}

public UserUpdateModel toUserUpdateModel() {
return new UserUpdateModel(
Optional.ofNullable(getUsername()).map(Username::new).orElse(null),
Optional.ofNullable(getEmail()).map(Email::new).orElse(null),
Optional.ofNullable(getPassword()).map(Password::new).orElse(null),
DolphaGo marked this conversation as resolved.
Show resolved Hide resolved
getBio(),
getImage()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ public static UserResponse fromUserAndToken(User user, String accessToken) {
return new UserResponse(
valueOf(user.getUsername()),
valueOf(user.getEmail()),
valueOf(user.getBio()),
valueOf(user.getImage()),
user.getBio(),
user.getImage(),
valueOf(accessToken)
);
}
Expand Down
18 changes: 14 additions & 4 deletions src/main/java/com/study/realworld/user/domain/Password.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
package com.study.realworld.user.domain;

import org.springframework.security.crypto.password.PasswordEncoder;
import static com.google.common.base.Preconditions.checkArgument;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;

@Embeddable
public class Password {

@NotBlank(message = "password must be provided.")
@Size(min = 6, max = 20, message = "password length must be between 6 and 20 characters.")
@Column(name = "password", nullable = false)
private String password;

protected Password() {
}

public Password(String password) {
checkPassword(password);

this.password = password;
}

private static void checkPassword(String password) {
checkArgument(StringUtils.isNotBlank(password), "password must be provided.");
checkArgument(
password.length() >= 6 && password.length() <= 20,
"password length must be between 6 and 20 characters."
);
}

public String getPassword() {
return password;
}
Expand All @@ -32,7 +42,7 @@ public static Password encode(Password password, PasswordEncoder passwordEncoder

public void matchPassword(Password rawPassword, PasswordEncoder passwordEncoder) {
if (!passwordEncoder.matches(rawPassword.password, this.password)) {
throw new RuntimeException("비밀번호가 다릅니다.");
throw new RuntimeException("password is different from old password.");
DolphaGo marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/study/realworld/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ public String getImage() {
return image;
}

public void changeUsername(Username username) {
this.username = username;
}

public void changeEmail(Email email) {
this.email = email;
}

public void changePassword(Password password) {
this.password = password;
}

public void changeBio(String bio) {
this.bio = bio;
}

public void changeImage(String image) {
this.image = image;
}

public void encodePassword(PasswordEncoder passwordEncoder) {
this.password = Password.encode(this.password, passwordEncoder);
}
Expand Down
50 changes: 48 additions & 2 deletions src/main/java/com/study/realworld/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.study.realworld.user.domain.User;
import com.study.realworld.user.domain.UserRepository;
import com.study.realworld.user.domain.Username;
import com.study.realworld.user.service.model.UserUpdateModel;
import java.util.Optional;
import javax.validation.Valid;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -26,7 +27,8 @@ public UserService(UserRepository userRepository, PasswordEncoder passwordEncode

@Transactional
public User join(@Valid User user) {
checkDuplicatedByUsernameOrEmail(user.getUsername(), user.getEmail());
checkDuplicatedByUsername(user.getUsername());
checkDuplicatedByEmail(user.getEmail());

user.encodePassword(passwordEncoder);
return userRepository.save(user);
Expand All @@ -39,11 +41,55 @@ public User login(@Valid Email email, @Valid Password password) {
return user;
}

private void checkDuplicatedByUsernameOrEmail(Username username, Email email) {
@Transactional(readOnly = true)
public Optional<User> findById(Long userId) {
return userRepository.findById(userId);
}

@Transactional
public User update(UserUpdateModel updateUser, Long userId) {
Copy link

Choose a reason for hiding this comment

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

해당 부분의 내부 구현 수정이 필요할 듯 합니다. 유선으로 말씀드린 대로 하나의 메소드에서 너무 많은 구현이 되고 있고, 그중 VO 단으로 넘길 수 있는 구현부도 있어 고민해보면서 수정하시면 좋을 듯 합니다! 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

처음에는 null한 부분을 처리하는데 급급해서 지금처럼 한군데 모아놓고 로직을 설계했는데 다시보니까 update 메소드 자체에서 하고있는 역할이 너무 많더라구요 😢
최대한 구현 변경을 진행해 보았습니다 :)

User user = userRepository.findById(userId).orElseThrow(RuntimeException::new);

updateUser.getUsername()
.filter(username -> !user.getUsername().equals(username))
.ifPresent(
username -> {
checkDuplicatedByUsername(username);
user.changeUsername(username);
});
Copy link

Choose a reason for hiding this comment

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

이 부분은 개선 가능할 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

유선으로 이야기했던 것처럼 UserService 자체를 변경해보았는데 확인한번 부탁드려요 :)
관련 커밋링크는 여러가지가 분리되어있어서 드리기가 어려울거같네요 ㅜㅜ

updateUser.getEmail()
.filter(email -> !user.getEmail().equals(email))
.ifPresent(
email -> {
checkDuplicatedByEmail(email);
user.changeEmail(email);
});
updateUser.getPassword()
.filter(
password -> !passwordEncoder
.matches(password.getPassword(), user.getPassword().getPassword()))
.ifPresent(
password -> {
user.changePassword(Password.encode(password, passwordEncoder));
});
updateUser.getBio()
.filter(bio -> !user.getBio().equals(bio))
.ifPresent(user::changeBio);
updateUser.getImage()
.filter(image -> !user.getImage().equals(image))
.ifPresent(user::changeImage);

return user;
}

private void checkDuplicatedByUsername(@Valid Username username) {
findByUsername(username)
.ifPresent(param -> {
throw new RuntimeException("already user username");
DolphaGo marked this conversation as resolved.
Show resolved Hide resolved
});
}

private void checkDuplicatedByEmail(@Valid Email email) {
findByEmail(email)
.ifPresent(param -> {
throw new RuntimeException("already user email");
DolphaGo marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.study.realworld.user.service.model;

import com.study.realworld.user.domain.Email;
import com.study.realworld.user.domain.Password;
import com.study.realworld.user.domain.Username;
import java.util.Optional;

public class UserUpdateModel {

private final Username username;
private final Email email;
private final Password password;
private final String bio;
private final String image;

public UserUpdateModel(Username username, Email email, Password password, String bio,
String image) {
this.username = username;
this.email = email;
this.password = password;
this.bio = bio;
this.image = image;
}

public Optional<Username> getUsername() {
return Optional.ofNullable(username);
}

public Optional<Email> getEmail() {
return Optional.ofNullable(email);
}

public Optional<Password> getPassword() {
return Optional.ofNullable(password);
}

public Optional<String> getBio() {
return Optional.ofNullable(bio);
}

public Optional<String> getImage() {
return Optional.ofNullable(image);
}
DolphaGo marked this conversation as resolved.
Show resolved Hide resolved

}