Project

Project List

팀 프로젝트

개인 프로젝트



개발자를 위한 지역기반 채팅 서비스 - DevMingle

GitHub
dev-mingle-server
프로젝트 구분
Backend 팀 프로젝트
프로젝트 기간
2023.08 ~ 진행중
팀원 수
백엔드 4명
기술스택

Alpine Linux 3.16 Java 17 Spring Boot 3.1.3 JPA Gradle 8.2.1 PostgreSQL 15.3 Redis MongoDB Flyway AWS EC2, S3

프로젝트 소개

DevMingle은 개발자들이 소통할 수 있는 게시판, 채팅 플랫폼 서비스입니다.
게시판과 채팅은 지역기반으로 운영되며, 가입자의 등록 위치와 현재 사용자의 위치를 기반으로 2가지 위치의 게시판을 확인할 수 있습니다. 채팅 구현은 STOMP를 이용한 sub/pub 구조로 MongoDB를 사용합니다.

프로젝트 담당 역할

프로젝트에서 User단을 담당하게 되었습니다.
사용자 Entity, 회원 관련 API, Spring security 인증/인가 부분을 담당했습니다. 그리고 JWT를 통한 토큰인증을 기반으로 access token, refresh token을 이용했으며, 현재 구글, 깃헙을 이용한 Oauth 회원가입, 로그인을 개발 중입니다.

주요 고려사항

1. 협업을 위한 깃헙 컨벤션

깃헙을 사용하면서 제대로 된 컨벤션을 만들어서 지키는 습관을 기르는 것도 이번 프로젝트의 큰 목표 중 하나였습니다. 저희가 정한 깃헙 컨벤션은 Commit, PR, Issue 등록시 유사했습니다. 그리고 저는 Issue로 세세하게 만들어야할 일들을 분류해서 작업했습니다.

추가로 개발 작업들은 develop 브런치에서 API 개발단위로 나눠 feature로 브랜치를 따서 사용한 뒤 다시 develop 브랜치에 merge하는 방식으로 진행했습니다.

2. 토큰 발급과 검증

Token 필터에서 Header로 들어온 access, refresh 토큰을 가지고 유효성을 검증합니다. 이 때 access 토큰이 만료된 경우에는 별도의 refresh 토큰을 이용한 발급요청이 없이도 refresh 토큰을 보고 재발급이 될 수 있도록 했습니다. 그리고 새로 발급된 토큰은 response에 신규 토큰이 반영될 수 있도록 setAuthenticationTokens 처리해주었습니다.

TokenFilter.java

tokenProvider.validateToken(accessToken);
if(tokenProvider.expiredToken(accessToken) && !tokenProvider.expiredToken(refreshToken)){
  tokenProvider.validateToken(refreshToken);
  Map<String,String> map = tokenProvider.rebuildToken(refreshToken);
  accessToken = map.get("accessToken");
  refreshToken = map.get("refreshToken");
}

setSecurityContextHolder(accessToken);
tokenProvider.setAuthenticationTokens(response, accessToken, refreshToken);
filterChain.doFilter(request, response);

토큰에 담긴 값을 바탕으로 UserDetails 구현체인 LoginUser에 넣어 Security Context Holder로 Controller단에서 @AuthenticationPrincipal 사용할 수 있도록 했습니다. 회원 닉네임, 비밀번호 변경시에도 이를 반영한 재발급해서 토큰을 리턴합니다.

@PostMapping("/password/reset/confirm")
  public ResponseEntity<ApiResponse> isRandomPassword(@AuthenticationPrincipal LoginUser loginUser){
    boolean isRandomPassword = usersRepository.findIsRandomPasswordById(loginUser.getId());
    return responseBuilder(isRandomPassword, HttpStatus.OK);
  }

3. 인증이 필요하지 않은 url의 처리

인증이 필요없는 url들을 security config에 다 나열하기에는 복잡하고 코드가 깔끔하지 않습니다. 더군다나 토큰 필터에서의 별도 검증 또한 필요하지 않은 경우는 url 중복이 발생하게 됩니다. 같은 url들을 양쪽에 업데이트하는 과정에서 불필요한 유지보수가 발생할 수 있기 때문에 한 곳에서 관리하는게 좋을 것 같아 application-auth.yml에서 별도로 관리할 수 있도록 처리했습니다.

상세코드를 보면, application-auth.yml에서 아래와 같이 등록합니다. prefix(/api/v1)가 붙는 url과 아닌 것으로 분류했습니다.

url:
  permit:
    get:
      - /
      - /actuator/health
    prefixGet:
      - /categories
      - /posts/**
    prefixPost:
      - /users/login
      - /users
      - /users/otp
      - /users/nickname

이후 이 url들은 SecurityConfig보다 우선 순위를 두어 Configuration에 list로 불러왔습니다. 각각의 get, prefixGet의 리스트는 prefix를 붙여 하나의 리스트로 구성했습니다. 즉, GET, POST 매핑할 url들로 분리했습니다.

@ConfigurationProperties(prefix = "url.permit")

그리고 각각 SecurityConfig에서 인증을 하지 않아도 접근가능하도록 permitAll()를 적용하고, TokenFilter에서는 token 검증없이 다른 filter로 진행되도록 처리했습니다.
인증과정보다 Token 유효성을 확인하고 Security Context Holder에 사용자 정보를 넣도록 되어 있기 때문에 addFilterBeforeUsernamePasswordAuthenticationFilter보다 토큰 검증과정이 선행될 수 있도록 했습니다.

SecurityConfig.java

http.httpBasic(HttpBasicConfigurer::disable)
        .cors(Customizer.withDefaults())
        .csrf(CsrfConfigurer::disable)
        .sessionManagement(config -> config.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
        .authorizeHttpRequests(auth -> {
            for (String url : urlProperties.getGet()) {
                auth.requestMatchers("GET", url).permitAll();
            }
            for (String url : urlProperties.getPost()) {
                auth.requestMatchers("POST", url).permitAll();
            }
            auth.anyRequest().authenticated();
        })
        .exceptionHandling(exception -> exception
                .authenticationEntryPoint(customEntryPoint)
                .accessDeniedHandler(customEntryPoint)
        );

TokenFilter.java

private boolean doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
  throws ServletException, IOException {
    boolean b = false;
    String method = request.getMethod();
    String url = request.getRequestURI();
    if (("GET".equals(method) && matchUrl(urlProperties.getGet(), request))
        || ("POST".equals(method) && matchUrl(urlProperties.getPost(), request))) {
      filterChain.doFilter(request, response);
      b=true;
    }
    return b;
}

private boolean matchUrl(List<String> list, HttpServletRequest request) {
    AntPathRequestMatcher matcher;
    for (String str : list) {
      matcher = new AntPathRequestMatcher(str);
      if (matcher.matches(new HttpServletRequestWrapper(request))) {
        return true;
      }
    }
    return false;
}



데일리 개발학습 학습지 - TOCO

GitHub
toco-java
프로젝트 구분
Fullstack 개인 프로젝트
프로젝트 기간
2023.06 ~ 진행중
기술스택

Java 17 Spring Boot 3.0.1 JPA Gradle Querydsl MySQL JUnit5 JS

프로젝트 소개

TOCO는 개발자을 배우고 싶은 사람들에게 정한 날짜에 정기적으로 메일링 서비스로 주어진 강의를 스스로 학습할 수 있도록 가이드를 제공하는 서비스입니다.
지금은 Fullstack으로 되어 있지만 향후에는 프론트와 백단을 나눠서 배포하고 버전 관리할 예정입니다.

주요 고려사항

1. Repository Custom 인터페이스의 분리

Querydsl을 공부하면서 처음 적용한 프로젝트였는데 여기서 Custom 인터페이스를 이용하는 방법과 그냥 Impl로 만들어버리는 방법에 대해 고민했습니다. 사실 인터페이스의 분리가 오히려 불편하고 코드가 복잡해보인다는 말도 있었습니다. 물론 큰 규모의 프로젝트라면 그럴 수도 있을 것이라는 생각이 들기도 했지만, EducationContentCustom와 같이 Querydsl 적용을 위한 메서드를 따로 빼서 관리하게 되면 향후 유지보수나 기능을 추가했을 때 Impl에 구현하지 않아 생기는 불상사를 막을 수 있을 것 같아서 Custom 인터페이스를 사용해서 적용했습니다.

@RequiredArgsConstructor
public class EducationContentRepositoryImpl implements EducationContentCustom {
  private final JPAQueryFactory jpaQueryFactory;

  @Override
  public EducationContent getNextUuid(int nextChapter, Education education) {
    return jpaQueryFactory
        .selectFrom(educationContent)
        .where(chapterEq(nextChapter),educationEq(education))
        .fetchOne();
  }
}  

2. 연관관계에 대한 고민

연관관계는 JPA의 꽃이라고 할 정도로 정말 간편하고 다양한 기능을 어노테이션으로 편하게 사용할 수 있습니다. 이번 프로젝트에서는 OneToOne 관계는 없었고, 주로 ManyToOne, OneToMany를 이용한 연관관계 설정이 많았습니다. 그렇다면 이 연관관계를 어디까지 연결할 것인가? 여기에 대한 정답은 없다고 생각합니다. 실제로 필요에 따라 연관관계는 어느정도 조정이 가능한데 불필요한 연관관계로 인해 Lazy Loading 이슈가 발생하지 않도록 합니다. 예를 들어, Many에 해당하는 부분이 null이라면 값을 불러오지 못하거나 연결된 데이터가 커서 성능상의 이슈가 있을 수 있습니다.

여기서 ERD를 설계하면서 이런 부분들을 고민해 자주 사용되지 않을 것으로 생각되는 부분의 연관관계인 교육서비스와 서비스 타입 간의 연관관계를 설정하지 않았습니다. 교육서비스와 연관된 엔티티들이 많이 있고 교육서비스를 가지고 올 때, 대부분 서비스 타입의 대분류와 소분류가 필요하지 않았기 때문입니다. (이미 카테고리를 기준으로 교육서비스 리스트를 불러옴)



results matching ""

    No results matching ""