Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2abbfa0
feature: Handler 어노테이션 추가
devjun10 Feb 19, 2024
afb76b7
refactor: 빈 등록/빈 조회 메서드 추가
devjun10 Feb 19, 2024
98e6bc9
feature: 사용자 정보 업데이트 기능 개발
devjun10 Feb 19, 2024
fecc578
refactor: Path 명시
devjun10 Feb 19, 2024
18841b9
refactor: Http setAttribute 기능 추가
devjun10 Feb 19, 2024
3afbe3a
refactor: DispatcherServlet 빈 초기화 방식 변경
devjun10 Feb 19, 2024
fb5b15b
refactor: 세션 로직 변경
devjun10 Feb 20, 2024
be7ed0a
refactor: 에러 응답
devjun10 Feb 19, 2024
c0c7c64
chore: task 의존성 변경
devjun10 Mar 26, 2024
2956fb8
docs: README.md 업데이트
devjun10 Mar 26, 2024
023b4b5
refactor: 인터셉터 경로 추가
devjun10 Mar 26, 2024
424e277
feat: Application/Json 타입 사용자 정보 업데이트 API 개발
devjun10 Mar 26, 2024
fcef502
refactor: 로그인 컨트롤러 log 제거
devjun10 Mar 26, 2024
49547fa
test: @AfterEach -> @BeforeEach로 변경
devjun10 Mar 26, 2024
4350b67
feat: Application/Json을 처리하기 위한 어노테이션 추가
devjun10 Mar 26, 2024
045c610
feat: 임시 커밋 추가
devjun10 Mar 26, 2024
53aa75f
refactor: 기본 포트 8080으로 변환
devjun10 Mar 26, 2024
947a8c7
docs: README.md 내용 추가
devjun10 Mar 26, 2024
eb19568
test: 깨진 테스트 복구
devjun10 Mar 26, 2024
9959627
refactor: CheckStyle, PMD 설정 적용
devjun10 Mar 26, 2024
8b65cdb
docs: README.md 내용 추가
devjun10 Mar 26, 2024
fec3b99
refactor: 미 반영사항 적용
devjun10 Apr 2, 2024
4313365
chore: schema 추가
devjun10 Apr 7, 2024
c38b3d4
docs: README.md 내용 추가
devjun10 Apr 7, 2024
a00907a
refactor: 일급 컬렉션에서 쿠키를 추출하도록 로직 변경
devjun10 May 10, 2024
1fccab2
docs: README.md 수정
devjun10 Jun 22, 2024
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
92 changes: 75 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 🍃 Spring 서버 만들기

해당 레포지토리는 자바 웹 프로그래밍 Next Step을 참조해 진행합니다.
해당 레포지토리는 [자바 웹 프로그래밍 Next Step](https://m.yes24.com/Goods/Detail/31869154) 및 [해당 레포지토리](https://github.com/next-step)를
참조해 진행합니다.

> html/css는 [해당 레포지토리](https://github.com/Origogi/DreamCoding-FE-Portfolio-Clone)를 참조했습니다.

Expand All @@ -9,11 +10,59 @@
<br>
<br>

## 💻 프로그램 실행

app 모듈 application.yml 파일/설정 추가 후 데이터베이스 설정 값 등록. test 디렉토리에도 추가.

````yaml
spring:
datasource:
driver-class-name: ${DRIVER_CLASS_NAME}
url: ${URL}
username: ${USERNAME}
password: ${PASSWORD}
````

<br>
<br>
<br>
<br>

데이터베이스 스키마 생성. app 모듈의 resource 패키지 참조.

```sql
CREATE TABLE user
(
id BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'PK',
username VARCHAR(40) NOT NULL COMMENT '사용자 이름',
password VARCHAR(255) NOT NULL COMMENT '패스워드',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
last_modified_at TIMESTAMP NULL DEFAULT NULL COMMENT '최종 수정일',
deleted VARCHAR(10) NOT NULL COMMENT '삭제 유무'
) engine 'InnoDB';
```

<br>
<br>
<br>
<br>

빌드 후 프로그램을 실행합니다.

```shell
./gradlew build
./gradlew bootJar
```

<br>
<br>
<br>
<br>

## 📝 공통 요구사항

1. 전체 미션은 7단계로 나뉘어져 있으며, 각 Step에는 `필수/선택 구현 사항`, `학습 목표`가 주어집니다.
1. 전체 미션은 4단계로 나뉘어져 있으며, 각 Step에는 `필수/선택 구현 사항`, `학습 목표`가 주어집니다.
- 다음 단계로 넘어가기 위해서는 최소 1명 이상의 Approve가 필요합니다.
- 필수 스텝은 5단계 까지이며, 나머지 단계는 자율적으로 진행합니다.
- 필수 구현사항은 반드시 구현해야 하며, 선택 구현 사항은 구현하지 않아도 됩니다.
- 선택 구현사항에는 [선택] 이 명시 돼 있으며, 없다면 필수 구현사항입니다.
- 현재 진행중인 Step이 완료되지 않으면, 다음 단계로 넘어갈 수 없습니다.
Expand All @@ -27,10 +76,8 @@
<br>
<br>
<br>
<br>
<br>

## Step1. 사용자 정보를 저장한다.
## [Step1] 사용자 정보를 저장한다.

네트워크로 부터 전송된 데이터를 파싱해 사용자 정보를 저장한다.

Expand All @@ -39,7 +86,6 @@
- [x] 데이터베이스는 애플리케이션 내부 인메모리 데이터베이스를 사용한다.
- [x] 어떤 정보를 저장할 지는 자유롭게 정의한다.

<br>
<br>

### 학습 목표
Expand All @@ -50,10 +96,8 @@
<br>
<br>
<br>
<br>
<br>

## Step2. 로그인 기능을 구현한다.
## [Step2] 로그인 기능을 구현한다.

사용자 정보를 바탕으로 로그인 기능을 구현한다.

Expand All @@ -65,8 +109,6 @@

- [x] 개인 정보 상세 조회 기능을 개발한다.


<br>
<br>

### 학습 목표
Expand All @@ -80,23 +122,39 @@
<br>
<br>
<br>
<br>
<br>

## Step3. 데이터베이스를 교체한다.
## [Step3] 데이터베이스를 교체한다.

애플리케이션 내부에 저장하던 데이터를 외부 데이터베이스에 저장한다.

1. 데이터베이스 종류는 자유롭게 선택 한다.
- RDB, Redis 등
2. JDBC 템플릿을 구현한다.

<br>
<br>

### 학습 목표

1. 추상화에 대해 이해한다.
1. 추상화에 대해 학습한다.
2. 데이터베이스 통신 과정에 대해 이해한다.
3. 각 데이터베이스의 특징에 대해 이해한다.
4. 트랜잭션에 대해 학습한다.

<br>
<br>
<br>
<br>

## [Step4] 데이터 전송 방식을 일부 변경한다.

매 번 정적 리소스를 가져오는 것은 비효율적이기 때문에, 한 페이지에서 일부 데이터만 변경할 수 있도록, 데이터 전송 방식을 변경한다.

1. 모든 API에 적용할 필요 없으며, 개인정보 수정만 적용한다.
2. [선택] 코드를 리팩토링한다.

<br>

### 학습 목표

1. 각 데이터 전송 방식에 대해 학습한다.
2. 정적 리소스를 가져오는 비용을 최적화 하는 방법에 대해 학습한다.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,5 @@ task downloadYml {
}

tasks.named("downloadYml") {
dependsOn downloadYml
Copy link

Choose a reason for hiding this comment

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

dependsOn downloadYml이라고 검색해보도 잘 안나와서 그런데,
어떤 작업을 하신건지 설명해주실 수 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

외부 저장소에 저장된 yml 값을 받아오는 역할을 합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이전 글 한 번 참조해보면 좋을 것 같습니다.

dependsOn compileJava
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package project.server.app.common.configuration;

import project.server.app.common.configuration.interceptor.SessionCheckHandlerInterceptor;
import project.server.app.core.web.user.application.UserLoginUseCase;
import project.server.app.core.web.user.presentation.validator.UserValidator;
import project.server.mvc.springframework.annotation.Component;
import static project.server.mvc.springframework.context.ApplicationContext.getBean;
import static project.server.mvc.springframework.context.ApplicationContext.register;
import project.server.mvc.springframework.web.InterceptorRegistry;
import project.server.mvc.springframework.web.WebMvcConfigurer;

@Component
public class WebConfiguration implements WebMvcConfigurer {

private final UserValidator validator;
private final UserLoginUseCase loginUseCase;

public WebConfiguration(
UserValidator validator,
UserLoginUseCase loginUseCase
) {
this.validator = validator;
this.loginUseCase = loginUseCase;
register(new InterceptorRegistry());
addInterceptors(getBean(InterceptorRegistry.class));
}

@Override

Choose a reason for hiding this comment

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

인터셉터를 직접 구현하셨군요! 인터셉터를 구현하신 이유가 있나요? 인터셉터의 역할은 무엇인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

각 컨트롤러에서 권한 체크를 하니, 로직의 중복이 발생했는데요, 이를 제거하기 위해 인터셉터를 만들었습니다. 즉, 권한 체크가 인터셉터의 역할입니다.

public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SessionCheckHandlerInterceptor(validator, loginUseCase))
.addPathPatterns("/my-info.html", "/my-info", "/api/users");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package project.server.app.common.configuration.interceptor;

import lombok.extern.slf4j.Slf4j;
import project.server.app.common.login.LoginUser;
import project.server.app.common.login.Session;
import static project.server.app.common.utils.HeaderUtils.getSessionId;
import project.server.app.core.web.user.application.UserLoginUseCase;
import project.server.app.core.web.user.presentation.validator.UserValidator;
import project.server.mvc.servlet.HttpServletRequest;
import project.server.mvc.servlet.HttpServletResponse;
import project.server.mvc.springframework.handler.HandlerInterceptor;
import project.server.mvc.springframework.web.servlet.ModelAndView;

@Slf4j
public class SessionCheckHandlerInterceptor implements HandlerInterceptor {

private final UserValidator validator;
private final UserLoginUseCase loginUseCase;

public SessionCheckHandlerInterceptor(
UserValidator validator,
UserLoginUseCase loginUseCase
) {
this.validator = validator;
this.loginUseCase = loginUseCase;
}

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
Long sessionId = getSessionId(request.getCookies());
Copy link

Choose a reason for hiding this comment

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

getSessionId를 HeaderUtils에 선언하셨던데 HeaderUtils에 선언하기로 결정한 이유에 대해서 들을 수 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Header에서 여러 유형의 값들을 파싱할 수 있다고 판단해서 별도의 파싱 클래스를 생성했습니다. Session 이외에도 여러 값들을 파싱할 수 있기 때문입니다.

Copy link

Choose a reason for hiding this comment

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

그럼 그건 HttpHeaders가 가질 수 있는 책임 아니였을까요??
HeaderUtils의 디렉토리 위치를 보면 common필드 내부에 존재하는데 HeaderParser와 같은 클래스를 정의해서 HeaderUtils의 필드로 넣거나, 혹은 HeaderUtils의 내부 메서드로 정의하는게 더 이상적인 방식이라고 느껴서요.

HttpHeaders 클래스에 존재하는

public int getContentLength() {
        List<HttpHeader> headers = this.headers.getOrDefault(
            CONTENT_LENGTH, List.of(new HttpHeader(CONTENT_LENGTH, "0"))
        );
        return Integer.parseInt(headers.get(0).getValue());
    }

위 메서드는 headers중에 contentLength를 찾아서 반환하는데 이것이 session과 관련된 메서드를 찾아서 가져오는 것과 어떤 차이가 있나요? 비슷한 성질의 역할을 하는 메서드인데 contentLength를 찾아오는 메서드는 HttpHeaders에 존재하고 sessionId를 찾아오는 메서드는 HeadersUtils에 존재하는 것이 납득되지 않습니다.

또한 common 패키지 밑에 HeadersUtils가 들어가는 것도 이해가 잘 안됩니다.
HeadersUtils는 util클래스라 명명되어 있지만, HttpHeaders라는 객체에 관련된 일만 할 것이 명백한 상황입니다.
그렇다면 적어도 HttpHeaders와 같은 디렉토리인 http 패키지 아래로 들어가는 것이 더 이상적인 구조지 않았을까요?

Copy link

Choose a reason for hiding this comment

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

sessionId로 UUID가 아니라 Long을 사용하신 이유가 궁금합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이거 userId 같은데 제 실수 같네요. 수정하겠습니다!

validator.validateSessionId(sessionId, response);
Copy link

Choose a reason for hiding this comment

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

validateSessionId를 보면 파라미터로 userId가 선언되어있는데..
잘못 된거 아닌가요?

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

맞습니다. 위에 변수명 실수 같아요.


Session findSession = loginUseCase.findSessionById(sessionId);
Copy link

Choose a reason for hiding this comment

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

findSession에서 find의 수동태는 found라 foundSession이 맞지않나요?

저는 그냥 session이라 선언해도 의미 전달에 전혀 문제가 없다 느껴져요. 굳이 변수명이 길어지는 것 보다는 session으로 두는 것이 더 낫다고 생각하는데 어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그것도 괜찮죠. 다만 찾아온 값들의 접두사로 find{$NAME} 형태로 사용하다보니 전체를 이렇게 통일했습니다.

log.info("Session:{}", findSession);
validator.validateSession(findSession, response);
Copy link

Choose a reason for hiding this comment

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

image
invalid일 경우 응답을 내릴 때, max-age=0;으로 응답을 내리던데 max-age를 왜 0으로 설정하는시 설명부탁드립니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

쿠키 만료 시간을 0으로 해야 브라우저에 저장된 값을 삭제할 수 있기 때문입니다.


LoginUser loginUser = new LoginUser(findSession);
request.setAttribute("loginUser", loginUser);
return false;
}

@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
HandlerInterceptor.super.afterCompletion(request, response, handler);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package project.server.app.common.utils;

import java.util.Map;
import project.server.mvc.servlet.http.Cookie;
import project.server.mvc.servlet.http.Cookies;
import project.server.mvc.servlet.http.*;

public final class HeaderUtils {

Expand All @@ -13,8 +11,7 @@ private HeaderUtils() {
}

public static Long getSessionId(Cookies cookies) {
Map<String, Cookie> cookiesMap = cookies.getCookiesMap();
Cookie findCookie = cookiesMap.get(SESSION_ID);
Cookie findCookie = cookies.get(SESSION_ID);
if (findCookie != null) {
return extractSessionId(findCookie);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface UserRepository {

Optional<User> findByUsernameAndPassword(String username, String password);

void update(Long id, String password);

void delete(User user);

List<User> findAll();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package project.server.app.core.web.user.application;

import project.server.app.common.login.LoginUser;

public interface UserUpdateUseCase {
void update(LoginUser loginUser, String password);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import project.server.app.core.web.user.application.UserDeleteUseCase;
import project.server.app.core.web.user.application.UserSaveUseCase;
import project.server.app.core.web.user.application.UserSearchUseCase;
import project.server.app.core.web.user.application.UserUpdateUseCase;
import project.server.app.core.web.user.exception.DuplicatedUsernameException;
import project.server.app.core.web.user.exception.UserNotFoundException;
import project.server.mvc.springframework.annotation.Service;

@Service
public class UserService implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase {
public class UserService implements UserSaveUseCase, UserSearchUseCase, UserUpdateUseCase, UserDeleteUseCase {

private final UserRepository userRepository;

Expand Down Expand Up @@ -43,6 +44,16 @@ public User findById(Long userId) {
return findUser;
}

@Override
public void update(
LoginUser loginUser,
String password
) {
User findUser = userRepository.findById(loginUser.getUserId())
.orElseThrow(UserNotFoundException::new);
userRepository.update(findUser.getId(), password);
}

@Override
public void delete(LoginUser loginUser) {
User findUser = userRepository.findById(loginUser.getUserId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import project.server.app.core.web.user.application.UserDeleteUseCase;
import project.server.app.core.web.user.application.UserSaveUseCase;
import project.server.app.core.web.user.application.UserSearchUseCase;
import project.server.app.core.web.user.application.UserUpdateUseCase;
import project.server.jdbc.core.exception.DataAccessException;
import static project.server.jdbc.core.transaction.DefaultTransactionDefinition.createTransactionDefinition;
import project.server.jdbc.core.transaction.PlatformTransactionManager;
Expand All @@ -18,7 +19,7 @@

@Slf4j
@Component
public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserDeleteUseCase {
public class UserServiceProxy implements UserSaveUseCase, UserSearchUseCase, UserUpdateUseCase, UserDeleteUseCase {

private final PlatformTransactionManager txManager;
private final UserService target;
Expand Down Expand Up @@ -70,6 +71,24 @@ public User findById(Long userId) {
}
}

@Override
public void update(
LoginUser loginUser,
String password
) {
TransactionStatus txStatus = getTransactionStatus(true);
log.debug("txStatus:[{}]", txStatus.getTransaction());
try {
target.update(loginUser, password);
txManager.commit(txStatus);
log.debug("Transaction finished.");
} catch (BusinessException | DataAccessException exception) {
txManager.rollback(txStatus);
log.error("{}", exception.getMessage());
throw exception;
}
}

@Override
public void delete(LoginUser loginUser) {
TransactionStatus txStatus = getTransactionStatus(false);
Expand Down
Loading