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

[2단계 - 장바구니 기능] 허브(방대의) 미션 제출합니다. #300

Merged
merged 29 commits into from
May 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2256b19
docs: 2단계 요구사항 정리
greeng00se May 2, 2023
27e9089
test: 잘못된 테스트명 수정
greeng00se May 2, 2023
02be345
feat: data.sql 수정 및 schema.sql 파일 추가
greeng00se May 2, 2023
1a0bd1b
refactor: auditing sql의 default를 사용하도록 변경
greeng00se May 2, 2023
33355cf
refactor: 사용자 엔티티 추가
greeng00se May 2, 2023
bf96972
feat: 사용자 설정 페이지 연동 기능 추가
greeng00se May 3, 2023
64925cc
feat: 장바구니 페이지 연동 기능 추가
greeng00se May 3, 2023
2cb430b
feat: Basic Auth로 오는 값을 파싱하는 클래스 추가
greeng00se May 4, 2023
b5cf8e6
feat: 사용자 정보에 대한 ArgumentResolver 추가
greeng00se May 4, 2023
5d075c1
test: 잘못 작성된 테스트 수정
greeng00se May 4, 2023
2d16d45
feat: 장바구니 등록 및 조회 기능 추가
greeng00se May 4, 2023
f13465b
feat: 장바구니 삭제 기능 추가
greeng00se May 4, 2023
6e8d373
refactor: Cart -> CartProduct로 수정
greeng00se May 4, 2023
6040e7e
refactor: create URI 수정
greeng00se May 4, 2023
bbf1884
feat: 인증을 인터셉터가 담당하도록 설정
greeng00se May 4, 2023
3d81cf3
docs: 요구사항 업데이트
greeng00se May 4, 2023
eebb50b
feat: argumentResolver에서 threadLocal clear하는 기능 추가
greeng00se May 4, 2023
b273000
feat: Credential에서 비밀번호가 다른지 확인하는 기능 추가
greeng00se May 5, 2023
cbb8bc6
refactor: 기존에 인증과정에서 Member를 반환하던 Dao를 Credential을 반환하도록 수정
greeng00se May 5, 2023
3d3799c
feat: ExceptionHandler에 로깅 적용
greeng00se May 5, 2023
705361a
refactor: Auditing을 위한 필드 전부 제거
greeng00se May 5, 2023
ac83592
refactor: BasicAuthorizationParser의 isNotValid를 제거하고 조금 더 응집력 있는 클래스로 변경
greeng00se May 5, 2023
eb44589
refactor: Credential의 id를 memberId로 변경
greeng00se May 5, 2023
1ac95f0
feat: getMemberId 메서드에 Nullable 애너테이션 추가
greeng00se May 5, 2023
7b8de76
refactor: 패키지 구조 변경
greeng00se May 5, 2023
aa41153
feat: 도미노 페페로니 피자 추가
greeng00se May 5, 2023
43eece1
remove: todo 주석 제거
greeng00se May 5, 2023
fb42fa4
refactor: InvalidBasicCredentialException이 AuthenticationException을 상…
greeng00se May 5, 2023
dd5dee5
test: BaiscAuthorizationParser 테스트 출력 수정
greeng00se May 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
# 📄 웹 장바구니 - 요구사항

## 🎁 상품
### 🎁 상품

- [x] ID, 이름, 이미지, 가격을 가진다.
- [x] 상품명은 100자 이하여야 한다.
- [x] 상품 가격은 0원 이상이어야 한다.
- [x] 상품 예외 사항
- [x] 존재하지 않는 상품에 대해서 수정/삭제 요청
- [x] 100자 이상의 상품명을 입력
- [x] 음수 가격을 입력

## ✔️ 상품 관리 CRUD API 작성
### 장바구니

- [x] ID, 사용자 ID, 상품 ID를 가진다.

### 사용자

- [x] ID, 이메일, 비밀번호를 가진다.

### ✔️ 상품 관리 CRUD API 작성

- [x] 생성 기능
- [x] 상품 조회 기능
Expand All @@ -15,21 +27,31 @@
- [x] 수정 기능
- [x] 삭제 기능

### 📚 API
### ✔️ 장바구니 CRUD API 작성

| 기능 | Method | URL |
|----|--------|----------------|
| 생성 | POST | /products |
| 수정 | PATCH | /products/{id} |
| 삭제 | DELETE | /products/{id} |
- [x] 장바구니에 상품 추가
- [x] 장바구니에 담긴 상품 제거
- [x] 장바구니 목록 조회

## ✔️ 페이지 연동
### 📚 상품 API

- [x] 상품 목록 페이지 연동
- [x] 관리자 도구 페이지 연동
| 기능 | Method | URL |
|-----|--------|----------------|
| 생성 | POST | /products |
| 수정 | PATCH | /products/{id} |
| 삭제 | DELETE | /products/{id} |
Comment on lines +38 to +42

Choose a reason for hiding this comment

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

API 명세서 좋네요 ㅎ
요청객체와 응답객체 형태, 그리고 발생할 수 있는 오류와 그 때의 오류 코드 및 응답객체 까지 추가한다면 현업에서도 활용할만한 API 문서가 됩니다

Copy link
Member Author

Choose a reason for hiding this comment

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

오.. 코멘트를 읽고 궁금한게 웨지는 Swagger, Restdocs 중 어떤걸 선호하시나요?
현업에서는 RestDocs에서 OpenAPI Spec을 추출해서 Swagger UI를 연동해서 많이 사용하나요?

Choose a reason for hiding this comment

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

예전엔 프로덕션 코드에 문서화 코드가 들어가는게 싫어서 restdocs 선호했는데 요샌 문서 옆에 띄워놓고 문서화 주석들을 바로 비교 / 수정이 가능한 Swagger가 더 괜찮다 싶긴해요.
회사마다 다르겠지만 저희 회사는 팀마다 문서화 방식이 달라서 각자 알아서하는데,
말씀하신 것처럼 Restdocs -> OpenApi Spec -> Swagger 같은 커스텀은 수요가 적을거 같아요 (그 작업 할 개발 리소스를 기능 개발 / 리팩토링에 투자할 거 같습니다)


### 📚 장바구니 API

| 기능 | Method | URL |
|-----|--------|---------------------|
| 생성 | POST | /cart-products |
| 삭제 | DELETE | /cart-products/{id} |
| 조회 | GET | /cart-products |

## 🛡️ 예외 사항
### ✔️ 페이지 연동

- [x] 존재하지 않는 상품에 대해서 수정/삭제 요청
- [x] 100자 이상의 상품명을 입력
- [x] 음수 가격을 입력
- [x] 상품 목록 페이지 연동
- [x] 관리자 도구 페이지 연동
- [x] 사용자 설정 페이지 연동
- [x] 장바구니 페이지 연동
18 changes: 18 additions & 0 deletions http-request.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### 상품 추가

POST http://localhost:8080/products/
Content-Type: application/json

Expand All @@ -9,6 +10,7 @@ Content-Type: application/json
}

### 상품 수정

PUT http://localhost:8080/products/1
Content-Type: application/json

Expand All @@ -21,3 +23,19 @@ Content-Type: application/json
### 상품 삭제

DELETE http://localhost:8080/products/1

### 장바구니 추가

POST http://localhost:8080/cart-products/
Authorization: Basic cGl6emExQHBpenphLmNvbTpwaXp6YQ==
Content-Type: application/json

{
"productId": 1
}

### 장바구니 조회

GET http://localhost:8080/cart-products/
Authorization: Basic cGl6emExQHBpenphLmNvbTpwaXp6YQ==
Content-Type: application/json
11 changes: 11 additions & 0 deletions src/main/java/cart/auth/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cart.auth;

Choose a reason for hiding this comment

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

나머지는 레이어드 아키텍처 기준으로 패키지 규칙이 정해져있는데, 갑자기 도메인 기준의 패키지가 나와버리면 혼동이 올거 같아요 ㅎ auth 패키지 내에선 cart의 다른 도메인을 의존하지 않도록 잘 구성해주셨으니, 아예 cart/auth가 아니라 auth 패키지로 분리하는건 어떨까요? (이 경우 component scan 범위가 달라지는데, 해당 부분도 어떻게 해결할 수 있을지 학습해보면 좋을 거같아요)


좀 더 코드를 보다보니 Member에 대한 의존이 있었군요 ㅎ 저라면 Member를 똑같이 복사해 해당 패키지에서 다루던지, 아니면 Credential 객체를 Member 테이블로부터 직접 조회하는 방법을 쓸거같습니다
아래 두가지 방식 중 취사선택 해주세용

  1. 기존 레이어드 방식으로 코드 재구성
  2. auth에서 cart 의존성 모두 제거하고 코드분리

Copy link
Member Author

Choose a reason for hiding this comment

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

현재의 패키지 규칙을 생각하지 못하고, 모두 auth 패키지 안에 모두 작성해 일관성이 깨졌네요.. 😢 생각하지 못했습니다..
auth 패키지로 분리한다면 다음과 같이 컴포넌트 스캔을 위한 설정파일을 추가로 작성하여 해결할 수 있을 것 같습니다!

@ComponentScan(basePackages = "auth")
@Configuration
public class ComponentScanConfiguration {
}

현재는 레이어드 방식으로 코드를 재구성하는 것이 더욱 간단해 보여서 1번을 선택해보겠습니다 😄


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {

Choose a reason for hiding this comment

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

커스텀 어노테이션 활용 👍

}
35 changes: 35 additions & 0 deletions src/main/java/cart/auth/AuthArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cart.auth;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

private final CredentialThreadLocal credentialThreadLocal;

public AuthArgumentResolver(final CredentialThreadLocal credentialThreadLocal) {
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public boolean supportsParameter(final MethodParameter parameter) {
final boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Auth.class);
final boolean hasCredentialType = Credential.class.isAssignableFrom(parameter.getParameterType());
return hasParameterAnnotation && hasCredentialType;
}

@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory
) {
final Credential credential = credentialThreadLocal.get();
credentialThreadLocal.clear();
return credential;
}
}
46 changes: 46 additions & 0 deletions src/main/java/cart/auth/AuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cart.auth;

import cart.dao.CredentialDao;
import cart.exception.AuthenticationException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class AuthInterceptor implements HandlerInterceptor {

private static final String AUTHORIZATION_HEADER_NAME = "Authorization";

private final CredentialDao credentialDao;
private final BasicAuthorizationParser basicAuthorizationParser;
private final CredentialThreadLocal credentialThreadLocal;

public AuthInterceptor(
final CredentialDao credentialDao,
final BasicAuthorizationParser basicAuthorizationParser,
final CredentialThreadLocal credentialThreadLocal
) {
this.credentialDao = credentialDao;
this.basicAuthorizationParser = basicAuthorizationParser;
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public boolean preHandle(
final HttpServletRequest request,
final HttpServletResponse response,
final Object handler
) {
final String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER_NAME);

final Credential credential = basicAuthorizationParser.parse(authorizationHeader);
final Credential savedCredential = credentialDao.findByEmail(credential.getEmail())
.orElseThrow(() -> new AuthenticationException("올바르지 않은 이메일입니다. 입력값: " + credential.getEmail()));

if (credential.isNotSamePassword(savedCredential)) {
throw new AuthenticationException();
}

credentialThreadLocal.set(savedCredential);
return true;
}
}
43 changes: 43 additions & 0 deletions src/main/java/cart/auth/BasicAuthorizationParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cart.auth;

import cart.exception.InvalidBasicCredentialException;
import java.util.Base64;
import org.springframework.stereotype.Component;

@Component
public class BasicAuthorizationParser {

private static final String KEYWORD = "Basic ";
private static final String DELIMITER = ":";
private static final int VALID_CREDENTIAL_SIZE = 2;
private static final int EMAIL_INDEX = 0;
private static final int PASSWORD_INDEX = 1;
private static final String EMPTY = "";

public Credential parse(final String authorizationHeader) {
final String[] credential = parseCredential(authorizationHeader);
if (isInvalidBasicCredential(authorizationHeader)) {
throw new InvalidBasicCredentialException(authorizationHeader);
}
return new Credential(credential[EMAIL_INDEX], credential[PASSWORD_INDEX]);
}

private String[] parseCredential(final String authorizationHeader) {
final String credential = authorizationHeader.replace(KEYWORD, EMPTY);
return decodeBase64(credential).split(DELIMITER);
}

private String decodeBase64(final String credential) {
try {
return new String(Base64.getDecoder().decode(credential));
} catch (final IllegalArgumentException e) {
throw new InvalidBasicCredentialException(credential);
}
}

private boolean isInvalidBasicCredential(final String authorizationHeader) {
return !authorizationHeader.startsWith(KEYWORD) ||
parseCredential(authorizationHeader).length != VALID_CREDENTIAL_SIZE;
}
}

37 changes: 37 additions & 0 deletions src/main/java/cart/auth/Credential.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cart.auth;

import org.springframework.lang.Nullable;

public class Credential {

private final Long memberId;
private final String email;
private final String password;

public Credential(final String email, final String password) {
this(null, email, password);
}

public Credential(final Long memberId, final String email, final String password) {
this.memberId = memberId;
this.email = email;
this.password = password;
}

public boolean isNotSamePassword(final Credential credential) {
return !this.password.equals(credential.getPassword());
}

@Nullable
public Long getMemberId() {
return memberId;
}

public String getEmail() {
return email;
}

public String getPassword() {
return password;
}
}
21 changes: 21 additions & 0 deletions src/main/java/cart/auth/CredentialThreadLocal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cart.auth;

import org.springframework.stereotype.Component;

@Component
public class CredentialThreadLocal {

private final ThreadLocal<Credential> local = new ThreadLocal<>();

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.

감사합니다 😄


public void set(final Credential credential) {
local.set(credential);
}

public Credential get() {
return local.get();
}

public void clear() {
local.remove();
}
}
45 changes: 45 additions & 0 deletions src/main/java/cart/config/WebConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cart.config;

import cart.auth.AuthArgumentResolver;
import cart.auth.AuthInterceptor;
import cart.auth.BasicAuthorizationParser;
import cart.auth.CredentialThreadLocal;
import cart.dao.CredentialDao;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

Choose a reason for hiding this comment

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

스프링 제공기능인 WebMvcConfigurer를 잘 활용해주셨군요 ㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

감사합니다! 👍


private final CredentialDao credentialDao;
private final BasicAuthorizationParser basicAuthorizationParser;
private final CredentialThreadLocal credentialThreadLocal;

public WebConfiguration(
final CredentialDao credentialDao,
final BasicAuthorizationParser basicAuthorizationParser,
final CredentialThreadLocal credentialThreadLocal
) {
this.credentialDao = credentialDao;
this.basicAuthorizationParser = basicAuthorizationParser;
this.credentialThreadLocal = credentialThreadLocal;
}

@Override
public void addInterceptors(final InterceptorRegistry registry) {
final AuthInterceptor authInterceptor = new AuthInterceptor(
credentialDao,
basicAuthorizationParser,
credentialThreadLocal
);
registry.addInterceptor(authInterceptor).addPathPatterns("/cart-products/**");
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthArgumentResolver(credentialThreadLocal));
}
}
Loading