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

[hoony-lab-issue12] User 도메인 구현 #19

Merged
merged 31 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7905b5a
[#12] chore: jpa, security, lombok dependency 추가
hoony-lab Aug 1, 2021
9ebe365
[#12] chore: lombok dependency 추가
hoony-lab Aug 2, 2021
ea16e7b
[#12] feat: User Entity 추가
hoony-lab Aug 3, 2021
6713f70
[#12] feat: User에 BaseTimeEntity 추가
hoony-lab Aug 3, 2021
f8fda88
[#12] test: RealworldApplicationTest 수정
hoony-lab Aug 3, 2021
6c474b6
[#12] feat: UserRepository 추가
hoony-lab Aug 3, 2021
214377d
[#12] test: UserTest 추가
hoony-lab Aug 3, 2021
d958304
[#12] feat: UserService 추가
hoony-lab Aug 3, 2021
d2817b8
[#12] test: UserServiceTest 추가
hoony-lab Aug 3, 2021
48e6cf0
[#12] feat: UserJoinRequest 추가
hoony-lab Aug 3, 2021
0623bb6
[#12] feat: UserResponse 추가
hoony-lab Aug 3, 2021
e43fb1f
[#12] feat: UserLoginRequest 추가
hoony-lab Aug 3, 2021
e60bde9
[#12] feat: UserContoller 추가
hoony-lab Aug 3, 2021
f77ffb7
[#12] feat: SecurityConfig 추가
hoony-lab Aug 3, 2021
6ce2d98
[#12] feat: PasswordEncoder Bean 추가
hoony-lab Aug 3, 2021
3612e99
[#12] feat: 비밀번호 암호화 및 확인 로직 추가
hoony-lab Aug 3, 2021
74abc86
[#12] test: UserControllerTest (@SpringBootTest) 추가
hoony-lab Aug 3, 2021
9a6f361
chore: test.gradle coverage change
jinyoungchoi95 Aug 5, 2021
91cddf0
chore: build.yml push, pr branch retouch
jinyoungchoi95 Aug 5, 2021
496fc01
[#12] feat: passwordEncoder를 controller에서 service로 이동
hoony-lab Aug 7, 2021
02df674
[#12] chore
hoony-lab Aug 7, 2021
fa4bffc
[#12] test: userServiceTest 수정
hoony-lab Aug 7, 2021
e96f72a
[#12] test: userControllerTest 수정
hoony-lab Aug 7, 2021
61940b0
Merge branch 'main' into hoony-lab-issue12
hoony-lab Aug 7, 2021
35d86b6
[#12] feat: UserNotFoundException 삭제
hoony-lab Aug 7, 2021
5ce0037
[#12] proposal:
hoony-lab Aug 8, 2021
812e27f
[#12] proposal
hoony-lab Aug 8, 2021
cec6f69
[#12] feat: UserRepositoryTest 추가
hoony-lab Aug 11, 2021
c68c196
fix: User Builder에 id 삭제
hoony-lab Aug 12, 2021
86bf551
[#12] fix: UserService 중복되는 함수 삭제
hoony-lab Aug 12, 2021
51e51e6
[#12] fix: UserService 부가 기능 수정
hoony-lab Aug 12, 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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: build

on:
push:
branches: [ main, chance0523, com8599, DolphaGo, hoony-lab, jinyoungchoi95, JunHo-YH, povia, sujl95 , kwj1270 ]
branches: [ main, chance0523, com8599, DolphaGo, hoony-lab, jinyoungchoi95, povia , kwj1270 , ERrorASER ]
pull_request:
branches: [ main, chance0523, com8599, DolphaGo, hoony-lab, jinyoungchoi95, JunHo-YH, povia, sujl95 , kwj1270 ]
branches: [ main, chance0523, com8599, DolphaGo, hoony-lab, jinyoungchoi95, povia , kwj1270 , ERrorASER ]

jobs:
build:
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/study/realworld/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.study.realworld.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@Configuration
public class JpaConfig {}
34 changes: 34 additions & 0 deletions src/main/java/com/study/realworld/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.study.realworld.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/api/hello", "/h2-console/**").permitAll()
.antMatchers("/api/users/**", "/api/users/login/**").permitAll()
.antMatchers("/api/articles/**").permitAll()
.antMatchers("/api/tags/**").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.study.realworld.global.common;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

}
79 changes: 79 additions & 0 deletions src/main/java/com/study/realworld/user/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.study.realworld.user.domain;

import com.study.realworld.global.common.BaseTimeEntity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.*;
import java.util.Objects;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;

@Column(nullable = false, length = 20)
private String email;

@Column(nullable = false, length = 50)
private String username;

@Column(nullable = false)
private String password;

@Column
private String bio;

@Column
private String image;

@Builder
public User(Long id, String email, String username, String password, String bio, String image) {
this.id = id;
Copy link

Choose a reason for hiding this comment

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

GenerationType.IDENTITY의 경우 id를 제외한 빌더를 사용해도 상관없으니 참고 부탁해요 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

아직 토큰 구현 안하셔서이지 않을까 싶어요

넵 맞습니다

GenerationType.IDENTITY의 경우 id를 제외한 빌더를 사용해도 상관없으니 참고 부탁해요 :)

DB쪽(?) 에서 케어하는 부분이라 상관없나보군요 🆗

Copy link
Member Author

Choose a reason for hiding this comment

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

위에꺼는 왜 코멘트를 달 수 있는 부분이 안보일까요 ㅠㅠ

Copy link

Choose a reason for hiding this comment

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

이전에 코멘트 달았는데 outdate 되어서 그런것 같아요 ㅎㅎ

this.email = email;
this.username = username;
this.password = password;
this.bio = bio;
this.image = image;
}

public static User of(User user) {
return User.builder()
.email(user.getEmail())
.username(user.getUsername())
.password(user.getPassword())
.bio(user.getBio())
.image(user.getImage())
.build();
}
Comment on lines +47 to +55
Copy link
Member

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.

지금은 안쓰이네요 ㅠ
테스트할 때도 커버리지 안걸려서 나중에 쓰겠지하고 일단 냅뒀는데...
User 쪽 다만들고도 안쓰면 지우겠습니다 !


public void encodePassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(password);
}

public boolean matchesPassword(String rawPassword, PasswordEncoder passwordEncoder) {
return passwordEncoder.matches(rawPassword, this.password);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}

@Override
public int hashCode() {
return Objects.hash(email);
}
Comment on lines +66 to +76
Copy link

Choose a reason for hiding this comment

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

프록시 객체 활용시 값이 null 이 나올 수 있어서 email()로 사용해주면 좋을 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

프록시 객체 라는 걸 확인해보도록 하겠습니다 !


}
10 changes: 10 additions & 0 deletions src/main/java/com/study/realworld/user/domain/UserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.study.realworld.user.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);

}
39 changes: 39 additions & 0 deletions src/main/java/com/study/realworld/user/dto/UserJoinRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.study.realworld.user.dto;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.study.realworld.user.domain.User;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME;

@JsonTypeName("user")
@JsonTypeInfo(include = WRAPPER_OBJECT, use = NAME)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UserJoinRequest {

private String username;
private String email;
private String password;

@Builder
public UserJoinRequest(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}

public static User toUser(UserJoinRequest request) {
return User.builder()
.username(request.getUsername())
.email(request.getEmail())
.password(request.getPassword())
.build();
}

}
28 changes: 28 additions & 0 deletions src/main/java/com/study/realworld/user/dto/UserLoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.study.realworld.user.dto;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME;

@JsonTypeName("user")
@JsonTypeInfo(include = WRAPPER_OBJECT, use = NAME)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UserLoginRequest {

private String email;
private String password;

@Builder
public UserLoginRequest(String email, String password) {
this.email = email;
this.password = password;
}

}
42 changes: 42 additions & 0 deletions src/main/java/com/study/realworld/user/dto/UserResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.study.realworld.user.dto;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.study.realworld.user.domain.User;
import lombok.Builder;
import lombok.Getter;

import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME;

@JsonTypeName("user")
@JsonTypeInfo(include = WRAPPER_OBJECT, use = NAME)
@Getter
public class UserResponse {

private final String email;
private final String token;
private final String username;
private final String bio;
private final String image;

@Builder
public UserResponse(String email, String token, String username, String bio, String image) {
this.email = email;
this.token = token;
this.username = username;
this.bio = bio;
this.image = image;
}

public static UserResponse of(User user, String token) {
return UserResponse.builder()
.email(user.getEmail())
.token(token)
.username(user.getUsername())
.bio(user.getBio())
.image(user.getImage())
.build();
}

}
71 changes: 71 additions & 0 deletions src/main/java/com/study/realworld/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.study.realworld.user.service;

import com.study.realworld.user.domain.User;
import com.study.realworld.user.domain.UserRepository;
import com.study.realworld.user.dto.UserJoinRequest;
import com.study.realworld.user.dto.UserLoginRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;

@RequiredArgsConstructor
@Transactional
Copy link

Choose a reason for hiding this comment

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

@Transactional은 보통 Class단에 어노테이션으로 먹일 때에는 readOnly = true, CUD 메소드에 먹일 때 readOnly = false로 먹이니 참고만 해주셔용

@Service
public class UserService {

private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;

@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}

@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException(id + " not found"));
}

@Transactional(readOnly = true)
public User findByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new NoSuchElementException(email + " not found"));
}

public User save(UserJoinRequest request) {
userRepository.findByEmail(request.getEmail())
.ifPresent(o -> new DuplicateKeyException(o.getEmail() + " already exist"));
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.

👍


User user = UserJoinRequest.toUser(request);
user.encodePassword(passwordEncoder);

return userRepository.save(user);
}

public User deleteById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException(id + " not found"));

userRepository.delete(user);
return user;
}

@Transactional(readOnly = true)
public User login(UserLoginRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new NoSuchElementException(request.getEmail() + " not found"));
Copy link

Choose a reason for hiding this comment

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

findByEmail이 중복되는 것 같은데 메서드로 분리해서 재사용도 좋을 것 같네요!

Copy link
Member Author

Choose a reason for hiding this comment

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

👍


if(!user.matchesPassword(request.getPassword(), passwordEncoder)) {
throw new NoSuchElementException(request.getPassword() + " wrong wrong wrong triple wrong" + user.getPassword());
}
Copy link

Choose a reason for hiding this comment

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

에러 검증 로직이 login()이라는 비즈니스 로직과는 조금 거리가 있다고 생각해요
이럴 경우 메서드로 분리해서 내부가 어떻게 돌아가는지 감추는 것도 좋을 것 같습니다

Copy link
Member Author

Choose a reason for hiding this comment

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

Service > login 에는 비즈니스 로직만...(?)
메서드로 분리 라는게 클린 코드 에 해당 하는 부분이라고 생각하면 될까요 ?

참고할수있는 키워드 하나만 주시면 더 공부해보겠습니다 !

Copy link

@kwj1270 kwj1270 Aug 12, 2021

Choose a reason for hiding this comment

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

사람마다 기준점은 다르지만 저 같은 경우
login()은 순수하게 로그인 기능만을 표현해야 된다고 생각해요
저 같은 경우 이런 예외를 던지는 부가 기능 부분에 있어서 최대한 메서드로 분리시키려고 해요

Copy link

@kwj1270 kwj1270 Aug 12, 2021

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.

감사합니다 뇌섹 우지님 💘


return user;
}

}