Skip to content

Commit

Permalink
[Feature] - [Member] 회원가입 시 추가 정보 등록 API 구현했습니다.
Browse files Browse the repository at this point in the history
[Feature] - [Member] 회원가입 시 추가 정보 등록 API 구현했습니다.
  • Loading branch information
fakerdeft authored Jan 23, 2024
2 parents e6856cb + e3dd029 commit 5b02d62
Show file tree
Hide file tree
Showing 31 changed files with 409 additions and 173 deletions.
63 changes: 42 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
## ✈ 기본

- 클래스 선언부와 필드 사이에 공백 하나 추가한다.

- 어노테이션은 클래스 혹은 메서드와 가장 관련된 것을 선언부와 가깝게 한다.

- 객체 필드와 메서드 파라미터에 ```final``` 무조건 붙인다.
- 객체 필드, 메서드 파라미터, 변수 등 재할당이 불가능한 값이면 `final` 붙인다.

- 패키지명은 단수로 한다.

- ```DTO```는 매개변수가 3개 이상일 경우 생성한다.
- `DTO`는 매개변수가 3개 이상일 경우 생성한다.

- 생성자 선언 순서

```
1. 기본 생성자
2. 모든 파라미터를 받는 생성자
3. 이후 파라미터가 많은 생성자가 상단에 오도록 선언
```

## ✈ Class

- 기능들의 의존성을 낮추기 위해 유지/보수를 위해 도메인 단위로 패키지를 나눈다.

- ```application```은 service, ```domain```은 entity/repository, ```presentation```은 controller 객체들을 포함한다.

- ```dto``````exception```은 특정 도메인 하위에서 구현한다.

```
src
└ main
Expand All @@ -47,22 +51,28 @@ src
└ ...
└ ...
```

- 객체 네이밍은 다음을 따른다.

```
Entity(도메인) + Controller/Service/Repository/...
```

## ✈ Method

### Presentation Layer

- ```CUD```는 save, update, delete로 통일한다.

- 단일건에 대한 ```Read```의 경우에는 파라미터에 따라 정한다. ```findById(final long Id)```

- 복수건에 대한 ```Read```의 경우 메서드 네임에 도메인 복수명과 파라미터에 대해서 작성한다. ```findByMemberId(final long memberId)```

- ```Read```를 할 때 URL이 /me로 끝나는 경우 My로 시작하는 메서드 네이밍으로 작성한다. ```url : /api/category/me -> findMyCategories(final long MemberId)```
- ```Read```를 할 때 URL이 /me로 끝나는 경우 My로 시작하는 메서드 네이밍으로
작성한다. ```url : /api/category/me -> findMyCategories(final long MemberId)```

### Service Layer

- ```CUD```는 컨트롤러 메서드와 네이밍을 통일한다.

- ```Read```의 경우에는 파라미터에 따라 네이밍을 정한다. ```findByIdAndMemberId(final long Id, final long memberId)```
Expand All @@ -72,18 +82,19 @@ Entity(도메인) + Controller/Service/Repository/...
- 불가피하게 검증 로직이 필요한 경우 ```validate```로 시작하고 검증하려는 로직에 대해서 적는다. ```validateExistCategory(final long Id)```

### Repository Layer
- ```Spring Data Jpa```가 지원하는 쿼리 메서드를 작성하는 네이밍 방식과 통일한다.
[[Spring Data Jpa 쿼리 메서드]](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods)

- ```Spring Data Jpa```가 지원하는 쿼리 메서드를 작성하는 네이밍 방식과 통일한다.
[[Spring Data Jpa 쿼리 메서드]](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods)

- 조회에 관한 메서드는 ```get```으로 시작한다.

## ✈ DTO

- ```DTO(Data Transfer Object)``````Request``````Response```로 나누어 제작한다. ```SignUpRequest, SignUpResponse```

## ✈ Test Code
- 테스트 메서드명을 한글로 작성한다.

- 테스트 메서드의 ```@DisplayName``` 어노테이션을 생략한다.
- 테스트 메서드명은 영어로, ```@DisplayName```을 한글로 작성한다.

- 테스트가 어려운 외부 서비스는 Test Double을 적용한다.

Expand All @@ -92,26 +103,36 @@ Entity(도메인) + Controller/Service/Repository/...
- 테스트 클래스의 ```bean```주입은 필드 주입을 사용한다.

- ```given, when, then``` 주석을 명시적으로 붙인다. 생략하지 않는다.
- ```given, when, then``` 절을 나누기 곤란한 경우 ```given, when & then```처럼 ```&``` 으로 합쳐서 작성한다.
```
// given & when
// when & then
// given & when & then
```
- ```given, when, then``` 절을 나누기 곤란한 경우 ```given, when & then```처럼 ```&``` 으로 합쳐서 작성한다.
```
// given & when
// when & then
// given & when & then
```
- 예외 케이스에 대한 테스트 메서드 네이밍은 ```~ 하면 예외가 발생한다.```로 통일한다.
```
@Test
void 없는_이미지를_조회하면_예외가_발생한다() {
@DisplayName("없는 이미지를 조회하면 예외가 발생한다.")
void failIfNotExistsImage() {
// given
// when & then
}
```
- 생성 로직에 대한 테스트 메서드 명은 ```~ 생성한다.```로 통일한다.
- 작은 기능 단위로 ```@Nested```를 사용해 클래스로 그룹화한다.
```
@Test
void 카테고리를_생성한다() {
// given
// when
// then
}
@Nested
@DisplayName("카테고리 생성 시")
class createCategory{

@Test
@DisplayName("카테고리를 생성에 성공한다.")
void success() {
// given
// when
// then
}
}
```
23 changes: 17 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ java {
}

configurations {
asciidoctorExtentions
asciidoctorExtensions
compileOnly {
extendsFrom annotationProcessor
}
Expand Down Expand Up @@ -42,7 +42,7 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'

asciidoctorExtentions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

Expand All @@ -56,11 +56,22 @@ tasks.named('test') {
}

asciidoctor {
configurations 'asciidoctorExtensions'
baseDirFollowsSourceFile()
inputs.dir snippetsDir
dependsOn test
configurations 'asciidoctorExtentions'
}

tasks.named('asciidoctor') {
inputs.dir snippetsDir
dependsOn test
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}

tasks.register('copyDocument', Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}

bootJar {
dependsOn copyDocument
}
37 changes: 23 additions & 14 deletions src/main/java/com/coverflow/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final String[] ALLOWED_URIS = {"/", "/index.html"};
private final LoginService loginService;
private final JwtService jwtService;
Expand All @@ -41,7 +42,9 @@ public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(
final HttpSecurity http
) throws Exception {
http
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
Expand All @@ -58,19 +61,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.successHandler(oAuth2LoginSuccessHandler) // 동의하기 눌렀을 때 핸들러 설정
.failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
);

// 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작
// 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정
// 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomJsonUsernamePasswordAuthenticationFilter
http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class);
)
// 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작
// 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정
// 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomJsonUsernamePasswordAuthenticationFilter
.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class)
.addFilterBefore(jwtAuthenticationFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class)
;

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
public PasswordEncoder passwordEncoder(
) {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Expand All @@ -82,7 +86,8 @@ public PasswordEncoder passwordEncoder() {
* 또한, FormLogin과 동일하게 AuthenticationManager로는 구현체인 ProviderManager 사용(return ProviderManager)
*/
@Bean
public AuthenticationManager authenticationManager() {
public AuthenticationManager authenticationManager(
) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
Expand All @@ -93,15 +98,17 @@ public AuthenticationManager authenticationManager() {
* 로그인 성공 시 호출되는 LoginSuccessJWTProviderHandler 빈 등록
*/
@Bean
public LoginSuccessHandler loginSuccessHandler() {
public LoginSuccessHandler loginSuccessHandler(
) {
return new LoginSuccessHandler(jwtService, memberRepository);
}

/**
* 로그인 실패 시 호출되는 LoginFailureHandler 빈 등록
*/
@Bean
public LoginFailureHandler loginFailureHandler() {
public LoginFailureHandler loginFailureHandler(
) {
return new LoginFailureHandler();
}

Expand All @@ -112,7 +119,8 @@ public LoginFailureHandler loginFailureHandler() {
* 로그인 성공 시 호출할 handler, 실패 시 호출할 handler로 위에서 등록한 handler 설정
*/
@Bean
public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter() {
public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter(
) {
CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordLoginFilter
= new CustomJsonUsernamePasswordAuthenticationFilter(objectMapper);
customJsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
Expand All @@ -122,7 +130,8 @@ public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePassword
}

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
public JwtAuthenticationFilter jwtAuthenticationFilter(
) {
return new JwtAuthenticationFilter(jwtService, memberRepository);
}

Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/coverflow/global/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ public abstract class BaseTimeEntity {
private LocalDateTime modifiedAt;

@PrePersist
public void prePersist() {
public void prePersist(
) {

LocalDateTime now = LocalDateTime.now();
createdAt = now;
}

@PreUpdate
public void preUpdate() {
public void preUpdate(
) {

modifiedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,36 @@
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String NO_CHECK_URL = "/login"; // "/login"으로 들어오는 요청은 Filter 작동 X
private final JwtService jwtService;
private final MemberRepository memberRepository;
private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain
) throws ServletException, IOException {
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
return; // return으로 이후 현재 필터 진행 막기 (안해주면 아래로 내려가서 계속 필터 진행시킴)
}

// 사용자 요청 헤더에서 RefreshToken 추출
// -> RefreshToken이 없거나 유효하지 않다면(DB에 저장된 RefreshToken과 다르다면) null을 반환
// 사용자의 요청 헤더에 RefreshToken이 있는 경우는, AccessToken이 만료되어 요청한 경우밖에 없다.
// 따라서, 위의 경우를 제외하면 추출한 refreshToken은 모두 null
String refreshToken = jwtService.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);

// 리프레시 토큰이 요청 헤더에 존재했다면, 사용자가 AccessToken이 만료되어서
// RefreshToken까지 보낸 것이므로 리프레시 토큰이 DB의 리프레시 토큰과 일치하는지 판단 후,
// 일치한다면 AccessToken을 재발급해준다.
if (refreshToken != null) {
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
return; // RefreshToken을 보낸 경우에는 AccessToken을 재발급 하고 인증 처리는 하지 않게 하기위해 바로 return으로 필터 진행 막기
}

// RefreshToken이 없거나 유효하지 않다면, AccessToken을 검사하고 인증을 처리하는 로직 수행
// AccessToken이 없거나 유효하지 않다면, 인증 객체가 담기지 않은 상태로 다음 필터로 넘어가기 때문에 403 에러 발생
// AccessToken이 유효하다면, 인증 객체가 담긴 상태로 다음 필터로 넘어가기 때문에 인증 성공
Expand All @@ -76,7 +78,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
* reIssueRefreshToken()로 리프레시 토큰 재발급 & DB에 리프레시 토큰 업데이트 메소드 호출
* 그 후 JwtService.sendAccessTokenAndRefreshToken()으로 응답 헤더에 보내기
*/
public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
public void checkRefreshTokenAndReIssueAccessToken(
final HttpServletResponse response,
final String refreshToken
) {

memberRepository.findByRefreshToken(refreshToken)
.ifPresent(user -> {
String reIssuedRefreshToken = reIssueRefreshToken(user);
Expand All @@ -90,7 +96,10 @@ public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response,
* jwtService.createRefreshToken()으로 리프레시 토큰 재발급 후
* DB에 재발급한 리프레시 토큰 업데이트 후 Flush
*/
private String reIssueRefreshToken(Member member) {
private String reIssueRefreshToken(
final Member member
) {

String reIssuedRefreshToken = jwtService.createRefreshToken();
member.updateRefreshToken(reIssuedRefreshToken);
memberRepository.saveAndFlush(member);
Expand All @@ -105,8 +114,11 @@ private String reIssueRefreshToken(Member member) {
* 인증 허가 처리된 객체를 SecurityContextHolder에 담기
* 그 후 다음 인증 필터로 진행
*/
public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
public void checkAccessTokenAndAuthentication(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain
) throws ServletException, IOException {
log.info("checkAccessTokenAndAuthentication() 호출");
jwtService.extractAccessToken(request)
.filter(jwtService::isTokenValid)
Expand All @@ -131,7 +143,9 @@ public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpSe
* SecurityContextHolder.getContext()로 SecurityContext를 꺼낸 후,
* setAuthentication()을 이용하여 위에서 만든 Authentication 객체에 대한 인증 허가 처리
*/
public void saveAuthentication(Member myMember) {
public void saveAuthentication(
final Member myMember
) {
String password = myMember.getPassword();
if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정
password = PasswordUtil.generateRandomPassword();
Expand Down
Loading

0 comments on commit 5b02d62

Please sign in to comment.