Skip to content

Commit

Permalink
Merge pull request #65 from f-lab-edu/feature/64-logincheck
Browse files Browse the repository at this point in the history
[#64] 로그인 AOP/Argument Resolver 추가
  • Loading branch information
hoa0217 authored Mar 26, 2024
2 parents 013a3cf + 1863d94 commit ba87738
Show file tree
Hide file tree
Showing 37 changed files with 489 additions and 451 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

## ModooSpace
스터디룸, 회의실, 연습실, 파티룸, 스튜디오 등 모든 공간을 시간단위로 대여할 수 있는 공간 대여 플랫폼입니다.
> ❗️ 단 호스트의 승인이 있어야 사용이 가능합니다.
### 프로젝트 목표
- 국내 [SpaceCloud](https://www.spacecloud.kr/)를 모티브로 공간대여 플랫폼을 구현하였습니다.
- 비즈니스 로직을 객체에게 최대한 위임하여 Service Layer에서 객체가 서로 협력하여 요청을 수행할 수 있도록 아키텍처를 구성하였습니다.
- 해당 프로젝트에서는 Mock없는 테스트를 지향하며 Domain 단위테스트, Service 통합테스트를 수행하여 TestCoverage 80%를 달성하였습니다.
- 단순 기능만 구현한 것이 아닌, 성능 테스트를 통해 높은 트래픽을 가정한 상황에서도 안정적인 서비스를 유지할 수 있도록 지속적으로 서버 구조를 개선 중입니다.

### 사용 기술
<img width="637" alt="스크린샷 2024-03-27 오전 12 19 02" src="https://github.com/f-lab-edu/modoospace/assets/48192141/94b581e1-0863-49af-8ea8-bc3f80a29807">

### ERD 구조
<img width="1002" alt="스크린샷 2024-03-26 오후 11 10 16" src="https://github.com/f-lab-edu/modoospace/assets/48192141/8acd7fe9-b624-4081-8a2e-45a54f28831d">

### 1차 서버 아키텍처
![image](https://github.com/f-lab-edu/modoospace/assets/48192141/b8b63d8c-a09b-492f-a825-cd5b981d34e4)

### 주요 기술 Issue
- [CI/CD를 구축해보자1 - NCP서버 생성 및 Docker로 어플리케이션 배포하기](https://velog.io/@gjwjdghk123/CI-CD1)
- [CI/CD를 구축해보자2 - JaCoCo와 GitHub Actions으로 CI/CD구축해보기](https://velog.io/@gjwjdghk123/CI-CD2)
- [ObjectOptimisticLockingFailureException과 고아객체(Orphan) 그리고 한방 쿼리](https://velog.io/@gjwjdghk123/ObjectOptimisticLockingFailureException)
- [nGrinder를 이용한 성능 테스트 및 성능 개선(ElasticSearch, Redis)](https://velog.io/@gjwjdghk123/nGrinder%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0ElasticSearch-Redis)
- [ElasticSearch TimeOutException 해결과정](https://velog.io/@gjwjdghk123/ElasticSearch-TimeOutException-%ED%95%B4%EA%B2%B0%EA%B3%BC%EC%A0%95)
16 changes: 9 additions & 7 deletions src/main/java/com/modoospace/MainController.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package com.modoospace;

import com.modoospace.config.auth.LoginEmail;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpSession;

@RequiredArgsConstructor
@Controller
public class MainController {

@GetMapping({"", "/"})
public String index(Model model, @LoginEmail String loginEmail) {
if (loginEmail != null) {
model.addAttribute("userName", loginEmail);
@GetMapping({"", "/"})
public String index(Model model, HttpSession session) {
String email = (String) session.getAttribute("member");
if (email != null) {
model.addAttribute("userName", email);
}
return "index";
}
return "index";
}
}
19 changes: 12 additions & 7 deletions src/main/java/com/modoospace/alarm/controller/AlarmController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.modoospace.alarm.controller.dto.AlarmResponse;
import com.modoospace.alarm.domain.AlarmType;
import com.modoospace.alarm.service.AlarmService;
import com.modoospace.config.auth.LoginEmail;
import com.modoospace.config.auth.aop.CheckLogin;
import com.modoospace.config.auth.resolver.LoginMember;
import com.modoospace.member.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -18,23 +20,26 @@ public class AlarmController {

private final AlarmService alarmService;

@CheckLogin
@GetMapping()
public ResponseEntity<Page<AlarmResponse>> search(@LoginEmail String loginEmail,
public ResponseEntity<Page<AlarmResponse>> search(@LoginMember Member loginMember,
Pageable pageable) {
Page<AlarmResponse> alarms = alarmService.searchAlarms(loginEmail, pageable);
Page<AlarmResponse> alarms = alarmService.searchAlarms(loginMember, pageable);
return ResponseEntity.ok().body(alarms);
}

@CheckLogin
@DeleteMapping("/{alarmId}")
public ResponseEntity<Void> delete(@PathVariable Long alarmId,
@LoginEmail String loginEmail) {
alarmService.delete(alarmId, loginEmail);
@LoginMember Member loginMember) {
alarmService.delete(alarmId, loginMember);
return ResponseEntity.noContent().build();
}

@CheckLogin
@GetMapping(value = "/subscribe", produces = "text/event-stream")
public ResponseEntity<SseEmitter> subscribe(@LoginEmail String loginEmail) {
return ResponseEntity.ok(alarmService.connectAlarm(loginEmail));
public ResponseEntity<SseEmitter> subscribe(@LoginMember Member loginMember) {
return ResponseEntity.ok(alarmService.connectAlarm(loginMember.getEmail()));
}

@PostMapping(value = "/send/{email}")
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/com/modoospace/alarm/service/AlarmService.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ public class AlarmService {
private final AlarmQueryRepository alarmQueryRepository;
private final EmitterLocalCacheRepository emitterRepository;

public Page<AlarmResponse> searchAlarms(String loginEmail, Pageable pageable) {
Member loginMember = memberService.findMemberByEmail(loginEmail);
public Page<AlarmResponse> searchAlarms(Member loginMember, Pageable pageable) {

return alarmQueryRepository.searchByMember(loginMember, pageable);
}
Expand Down Expand Up @@ -80,9 +79,8 @@ private void sendToClient(SseEmitter emitter, String email, Object data) {
}

@Transactional
@CachePrefixEvict(cacheNames = "searchAlarms", key = "#loginEmail")
public void delete(Long alarmId, String loginEmail) {
Member loginMember = memberService.findMemberByEmail(loginEmail);
@CachePrefixEvict(cacheNames = "searchAlarms", key = "#loginMember.email")
public void delete(Long alarmId, Member loginMember) {
Alarm alarm = findAlarmById(alarmId);

alarm.verifyManagementPermission(loginMember);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public ResponseEntity<String> handlePermissionDeniedException(PermissionDeniedEx
return new ResponseEntity<>(exception.getMessage(), HttpStatus.FORBIDDEN);
}

@ExceptionHandler(UnAuthenticatedException.class)
public ResponseEntity<String> handleUnAuthenticatedException(UnAuthenticatedException exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.UNAUTHORIZED);
}

@ExceptionHandler(NotFoundEntityException.class)
public ResponseEntity<String> handleNotFoundEntityException(NotFoundEntityException exception) {
return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.modoospace.common.exception;

public class UnAuthenticatedException extends RuntimeException {

private static final String MESSAGE = "로그인이 필요합니다.";

public UnAuthenticatedException() {
super(MESSAGE);
}
}
6 changes: 3 additions & 3 deletions src/main/java/com/modoospace/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.modoospace.config;

import com.modoospace.config.auth.LoginEmailArgumentResolver;
import com.modoospace.config.auth.resolver.LoginMemberArgumentResolver;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -12,11 +12,11 @@
@Configuration
public class WebConfig implements WebMvcConfigurer {

private final LoginEmailArgumentResolver loginEmailArgumentResolver;
private final LoginMemberArgumentResolver loginMemberArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginEmailArgumentResolver);
resolvers.add(loginMemberArgumentResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.modoospace.config.auth;

import com.modoospace.config.auth.dto.OAuthAttributes;
import com.modoospace.config.auth.dto.SessionMember;
import com.modoospace.member.domain.Member;
import com.modoospace.member.domain.MemberRepository;
import com.modoospace.member.repository.MemberCacheRepository;
Expand Down Expand Up @@ -40,11 +39,8 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
OAuthAttributes attributes = OAuthAttributes
.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

//4. SessionUser : 세션에 사용자 정보를 저장하기 위한 Dto클래스
// 왜 Member를 사용하지않고 SessionMember를 사용 ? Member 클래스가 엔티티이기 때문.
// 엔티티 클래스를 직렬화한다면, 의존관계를 갖는 다른 엔티티들까지 직렬화할 가능성이 있어 성능이 느려질 수 있다.
Member member = saveOrUpdate(attributes);
httpSession.setAttribute("member", new SessionMember(member));
httpSession.setAttribute("member", member.getEmail());

return new DefaultOAuth2User(
Collections.singleton(member.createGrantedAuthority()),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/api/v1/facilities/*/schedules/**",
"/api/v1/visitors/reservations/facilities/*/availability/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/v1/alarms/send/**").permitAll()
// .antMatchers(HttpMethod.POST, "/api/v1/space").hasRole(Role.HOST.name())
.antMatchers("/api/v1/admin/**").hasRole(Role.ADMIN.name())
.anyRequest().authenticated()
)
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/modoospace/config/auth/aop/CheckLogin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.modoospace.config.auth.aop;

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

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
}
28 changes: 28 additions & 0 deletions src/main/java/com/modoospace/config/auth/aop/CheckLoginAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.modoospace.config.auth.aop;

import com.modoospace.common.exception.UnAuthenticatedException;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;

import javax.servlet.http.HttpSession;

@Component
@Aspect
@RequiredArgsConstructor
public class CheckLoginAspect {

private final HttpSession httpSession;

@Before("@annotation(com.modoospace.config.auth.aop.CheckLogin)")
public void checkLogin() throws HttpClientErrorException {

String email = (String) httpSession.getAttribute("member");

if (email == null) {
throw new UnAuthenticatedException();
}
}
}
19 changes: 0 additions & 19 deletions src/main/java/com/modoospace/config/auth/dto/SessionMember.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.modoospace.config.auth;
package com.modoospace.config.auth.resolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -7,6 +7,6 @@

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.modoospace.config.auth.resolver;

import com.modoospace.common.exception.UnAuthenticatedException;
import com.modoospace.member.domain.Member;
import com.modoospace.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
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;

import javax.servlet.http.HttpSession;

@RequiredArgsConstructor
@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final HttpSession httpSession;
private final MemberService memberService;

/**
* 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginMember.class) && parameter.getParameterType().equals(Member.class);
}

/**
* 파라미터에 전달할 객체 생성
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
try {
String email = (String) httpSession.getAttribute("member");
return memberService.findMemberByEmail(email);
} catch (RuntimeException e) {
throw new UnAuthenticatedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public RestHighLevelClient elasticsearchClient() {
httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
.setMaxConnTotal(100) // 전체 최대 연결 수를 100개로 설정
.setMaxConnPerRoute(50) // 단일 라우트(호스트) 당 최대 연결 수 -> 단일 node이므로 50개가 최대임.
.setMaxConnPerRoute(50) // 단일 라우트(호스트) 당 최대 연결 수
);
return new RestHighLevelClient(builder);
}
Expand Down
Loading

0 comments on commit ba87738

Please sign in to comment.