프로젝트 핵심 코드
이번 뉴스피드 프로젝트에서 사용했던 핵심 기능과 그에 따른 장점, 단점,그리고 핵심 코드에 대해 설명해보고 합니다.
크게 JWT 인증, 필터, 커스텀 어노테이션, 파사드 패턴 4가지로 정리할 수 있습니다.
해당 코드를 자세하게 보고 싶다면 해당 깃허브 레포지토리로 오시면 됩니다!
https://github.com/TeamSparta19/minsutagram
JWT 인증 기능
핵심 기능:
JWT(Json Web Token)를 사용하여 사용자 인증을 처리합니다. 사용자가 로그인하면 서버는 JWT를 발급하고, 클라이언트는 이를 요청 시 Authorization 헤더에 담아 보내게 됩니다.
장점:
- 무상태(stateless) 인증: 서버가 세션을 유지할 필요가 없고, JWT 자체에 인증 정보가 담겨 있어 클라이언트가 각 요청마다 전달함으로써 인증이 유지됩니다.
- 보안성: 암호화된 토큰을 사용해 보안이 강화되며, 만료 시간 설정을 통해 토큰이 남용되는 것을 방지합니다.
단점:
- 토큰 크기: JWT의 크기가 커질 수 있어 네트워크 비용이 증가할 수 있습니다.
- 토큰 만료 처리: 만료된 토큰을 처리하는 데 어려움이 있을 수 있고, 서버에서 강제 로그아웃이 불가능합니다.
public class JwtUtil {
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분 만료 시간
public String createToken(String email) {
Date now = new Date();
return Jwts.builder()
.setSubject(email)
.setExpiration(new Date(now.getTime() + TOKEN_TIME)) // 만료 시간 설정
.setIssuedAt(now) // 발급 시간
.signWith(key, signatureAlgorithm) // 서명 및 암호화
.compact();
}
public void validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
} catch (ExpiredJwtException e) {
throw new ApiException(ResponseCode.EXPIRE_ACCESS_TOKEN);
}
// Additional validation code...
}
}
코드 설명
필드 (Fields) 부분
- TOKEN_TIME: JWT 토큰의 유효시간을 나타내는 필드로, 60분을 의미합니다. 토큰의 만료시간을 관리하며, 보안성을 위해 일정 시간이 지나면 토큰이 무효화됩니다.
- secretKey: 토큰 서명에 사용되는 비밀키로, 외부 설정 파일에서 가져옵니다. 이 키를 기반으로 JWT 토큰을 생성하고 서명합니다.
- key: secretKey를 변환하여 서명에 사용하는 객체입니다. JWT 서명을 검증하거나 토큰을 발행할 때 사용됩니다.
메서드 (Methods) 부분
- createToken(): 사용자 식별 정보(이메일)를 기반으로 JWT 토큰을 생성하는 메서드입니다. 토큰에는 발급일, 만료일, 서명 등이 포함되며, 이를 통해 사용자의 인증 정보를 안전하게 전달할 수 있습니다.
- validateToken(): 클라이언트가 보낸 JWT 토큰을 검증하는 메서드입니다. 토큰의 서명과 만료 여부를 확인하여 유효성을 검증합니다. 만약 만료되었거나 유효하지 않으면 예외를 발생시킵니다.
JWT 필터 기능
핵심 기능:
필터는 모든 요청을 가로채서 JWT 토큰의 유효성을 검증하고, 필요한 경우 사용자의 인증 정보를 설정합니다.
장점:
- 중앙 집중화된 인증 관리: 필터를 통해 인증 처리를 통합 관리할 수 있어 코드 중복을 방지합니다.
- API 보안 강화: 화이트리스트 URL 외의 모든 요청에 대해 토큰 검증을 수행함으로써 API의 보안을 강화합니다.
단점:
- 복잡한 필터 체인 관리: 필터가 많아질 경우, 체인 관리가 복잡해질 수 있습니다.
- 화이트리스트 관리의 복잡성: 화이트리스트를 추가할수록 관리가 복잡해지고, 보안상의 취약점이 생길 수 있습니다.
public class JwtFilter extends OncePerRequestFilter {
private final List<String> whiteList = List.of("/api/users", "/api/users/login");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String url = request.getRequestURI();
if (whiteList.contains(url)) {
filterChain.doFilter(request, response);
return;
}
String token = request.getHeader(AUTHORIZATION_HEADER);
try {
String jwt = jwtUtil.substringToken(token);
jwtUtil.validateToken(jwt);
Claims claims = jwtUtil.extractClaims(jwt);
request.setAttribute(USER_EMAIL, claims.getSubject());
} catch (ApiException e) {
sendErrorResponse(response, e);
}
filterChain.doFilter(request, response);
}
}
코드 설명
필드 (Fields)
- whiteList: 인증이 필요하지 않은 URL들의 목록입니다. 예를 들어, 로그인이나 회원가입 API는 JWT 인증이 필요하지 않으므로, 이 화이트리스트에 포함됩니다.
메서드 (Methods)
- doFilterInternal(): 모든 요청에 대해 JWT 토큰을 검증하는 필터입니다. 화이트리스트에 포함된 URL은 토큰 검증 없이 통과시키고, 나머지 요청은 토큰을 검증한 후 사용자 정보를 추출하여 요청에 추가합니다.
파사드 패턴 적용 기능 (UserRequest)
핵심 기능:
UserRequest 클래스는 여러 개의 레포지토리를 감싸서 통합된 인터페이스를 제공합니다. 이를 통해 복잡한 비즈니스 로직에서 다양한 레포지토리 호출을 간소화합니다.
장점:
- 간결한 인터페이스 제공: 여러 레포지토리 접근을 단일 클래스에 감춰 코드의 복잡성을 줄이고, 클라이언트 코드에서 깔끔한 인터페이스를 제공합니다.
- 유지보수성 향상: 변경 사항이 생기면 한 곳에서 관리할 수 있어 유지보수가 용이합니다.
단점:
- 클래스가 비대해질 가능성: 기능이 많아질수록 파사드 클래스가 비대해질 수 있습니다.
- 추가적인 의존성: 파사드 클래스가 너무 많은 의존성을 가질 수 있으며, 책임이 모호해질 수 있습니다.
@Component
@RequiredArgsConstructor
public class UserRequest {
private final UserRepository userRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;
public User findUser(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new ApiException(ResponseCode.NOT_FOUND_USER));
}
public void delete(User user) {
postRepository.deleteByUserId(user.getId());
commentRepository.deleteByUserId(user.getId());
userRepository.save(user);
}
}
코드 설명
필드 (Fields)
- userRepository, postRepository, commentRepository, followRepository: 사용자, 게시물, 댓글, 팔로우 관련 데이터를 처리하는 레포지토리 필드입니다. 각 레포지토리를 통해 데이터베이스에서 CRUD 작업을 수행할 수 있습니다.
메서드(Methods):
- findUser(): 이메일을 기반으로 사용자를 찾는 메서드입니다. 사용자가 삭제되지 않았는지(삭제 시간이 null인지)도 확인하며, 없으면 예외를 발생시킵니다.
- delete(): 사용자를 삭제할 때 연관된 게시물, 댓글, 팔로우 관계도 모두 삭제하는 메서드입니다. 이를 통해 데이터의 일관성을 유지합니다.
- existsUser(String email): 사용자가 해당 이메일로 존재하는지 확인하는 메서드이고, UserRepository에서 existsByEmail 메서드를 사용하여 데이터베이스에 해당 이메일로 사용자가 존재하는지 체크합니다. 그리고 반환 값은 boolean 타입으로, 사용자 존재 여부를 나타냅니다.
- saveUser(User user): UserRepository의 save() 메서드를 통해 새로운 사용자를 데이터베이스에 저장하거나, 기존 사용자를 수정하는 역할을 합니다. 사용자를 저장하고, 저장된 사용자 객체를 반환합니다.
@LoginUser 커스텀 어노테이션
핵심 기능:
@LoginUser 어노테이션을 사용하여, 컨트롤러 메서드에서 로그인된 사용자의 정보를 쉽게 주입받을 수 있도록 합니다.
장점:
- 코드 재사용성 증가: 매번 로그인된 사용자를 직접 가져오는 코드의 중복을 없앨 수 있습니다.
- 간결한 코드: 어노테이션을 통해 매번 사용자 정보를 불러오는 과정을 단순화하여 가독성이 증가합니다.
단점:
- 추가적인 학습 필요: 개발자가 커스텀 어노테이션의 동작을 이해해야 하므로 학습이 필요합니다.
- 디버깅의 어려움: 어노테이션을 통해 주입된 데이터에 문제가 생기면, 디버깅이 어려울 수 있습니다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
@LoginUser String email,
@RequestBody UserUpdateDto userUpdateDto
해당 코드에 대한 자세한 내용은 밑에 있는 포스팅에 자세히 설명해놨다.
https://minjooig.tistory.com/120
KPT회고!
Keep - 현재 만족하고 있는 부분
- JWT 인증 시스템의 안정적 구현: 프로젝트에서 JWT를 이용한 인증과 토큰 발급 시스템을 성공적으로 구현하여 보안과 사용자 인증 측면에서 안정적인 환경을 구축했습니다. JwtFilter와 JwtUtil을 사용하여 요청의 유효성을 필터링하고, 불필요한 요청을 차단할 수 있었던 부분이 프로젝트의 핵심 기능 중 하나였습니다.
- Layered Architecture의 명확한 구분: 프로젝트 내에서 계층별 역할을 잘 분리하여 코드의 유지보수성과 확장성을 높였습니다. 컨트롤러, 서비스, 레포지토리 간의 명확한 구분으로 각 계층에서 필요한 역할을 효과적으로 수행할 수 있었습니다.
- 파사드 패턴 사용으로 복잡한 의존성 해결: UserRequest 클래스에서 파사드 패턴을 사용해 복잡한 레포지토리 호출들을 감추고 간결한 인터페이스를 제공한 점은 프로젝트의 복잡성을 줄이는 데 큰 기여를 했습니다.
- Custom Annotation 적용: @LoginUser 같은 커스텀 어노테이션을 도입해 코드 중복을 줄이고, 간결하게 유저 인증 정보를 받아오는 방식이 매우 효과적이었습니다.
Problem - 불편하게 느끼는 부분
- LazyInitializationException 문제: JPA 사용 시 지연 로딩 전략 때문에 LazyInitializationException 문제가 발생하였고, 이를 해결하기 위해 open-in-view=true 설정을 적용했으나, 이로 인해 성능 저하와 데이터 일관성 문제가 발생할 수 있었습니다.
- 성능 최적화 미흡: 일부 기능, 특히 지연 로딩 문제와 N+1 쿼리 문제로 인해 데이터베이스 접근에 있어서 성능 이슈가 발생할 가능성이 있었습니다. 쿼리 최적화나 페치 전략에 대한 추가적인 고려가 필요했습니다.
- 화이트리스트 관리: JwtFilter에서 화이트리스트로 특정 URL을 필터링하는 방식은 간단하지만, 화이트리스트가 커질수록 관리가 어려워질 수 있으며, 보안적으로도 취약점을 만들 수 있습니다.
Try - Problem에 대한 해결책, 당장 실행 가능한 것
- 명시적 페치 전략 도입: open-in-view=true 설정의 단점을 극복하기 위해, 서비스 계층에서 필요한 데이터는 Fetch Join을 통해 명시적으로 로딩하여 성능 저하를 방지하고, 트랜잭션 범위 내에서 안전하게 데이터를 처리하도록 개선할 계획입니다.
- 성능 테스트 및 최적화: 전체적인 성능 테스트를 진행하여, 쿼리 최적화, 인덱스 튜닝, 그리고 캐시 활용 방안을 검토하여 시스템의 성능을 향상시키고자 합니다.
- 화이트리스트 동적 관리: 화이트리스트 관리를 더욱 효율적으로 하기 위해 동적으로 관리할 수 있는 방법을 도입하고, 필요 시에는 보안 정책을 강화할 계획입니다. 예를 들어, 관리 대상 URL이 많아질 경우, 역할 기반의 동적 필터링을 고려할 수 있습니다.
- 테스트 자동화 강화: 테스트 코드 및 CI/CD 파이프라인에 대한 강화가 필요합니다. JWT와 관련된 인증 및 권한 테스트를 추가하고, 시스템의 안정성을 보장할 수 있는 자동화된 테스트를 작성할 계획입니다.
나의 회고
이번 프로젝트에서는 웹 애플리케이션의 핵심 기능인 Post CRUD (생성, 읽기, 업데이트, 삭제)와 페이지네이션, 그리고 팔로우 기능의 연관관계를 다루었습니다. 내가 담당한 부분은 주로 Post 관련 CRUD 기능과 페이지네이션 구현, 그리고 팔로우 기능의 연관관계를 설정하는 것이었습니다. 프로젝트의 주된 목표는 사용자들이 원활하게 게시글을 관리하고, 팔로우 관계를 통해 소통할 수 있는 기능을 제공하는 것이었습니다.
핵심 작업 및 구현
- Post CRUD 및 페이지네이션
- CRUD 기능: Post의 생성, 읽기, 업데이트, 삭제 기능을 구현했습니다. 이 과정에서 RESTful API 설계를 고려하여 엔드포인트를 정의하고, 각 기능에 맞는 서비스 메서드를 구현했습니다.
- 페이지네이션: 대량의 데이터 처리를 효율적으로 하기 위해 페이지네이션을 적용했습니다. Spring Data JPA의 PagingAndSortingRepository를 사용하여 간단하게 페이지네이션을 구현할 수 있었습니다. 이로써 사용자들은 원하는 페이지의 게시글만을 조회할 수 있게 되었습니다.
- 팔로우 기능 연관관계 설정
- 팔로우 기능: 사용자 간의 팔로우 관계를 설정하고 관리하는 기능을 구현했습니다. 이 과정에서 팔로우하는 사용자와 팔로우 당하는 사용자의 관계를 정의하고, 이를 데이터베이스에 저장할 수 있도록 했습니다.
트러블 슈팅 및 해결 방법
- Lazy Loading 문제
- 문제 발생: Lazy Loading 설정 문제로 인해 특정 엔티티의 데이터가 예상대로 로딩되지 않는 이슈가 발생했습니다. 특히, 사용자 정보가 제대로 불러와지지 않는 문제가 있었습니다.
- 해결 방법: @Transactional 어노테이션을 적절히 사용하여 데이터베이스 트랜잭션 범위 내에서 Lazy Loading이 이루어지도록 했습니다. 또한, open-in-view 설정을 통해 뷰에서 Lazy Loading이 가능하도록 하여 문제를 해결했습니다.
- 해결 후 생각: open-in-view 설정은 편리하지만, 성능에 영향을 줄 수 있는 점을 인지하게 되었습니다. 이러한 설정이 데이터베이스 쿼리의 수를 증가시킬 수 있기 때문에, 필요한 경우에만 사용하는 것이 좋겠다고 생각했습니다.
- DTO 남발 문제
- 문제 발생: DTO를 과도하게 사용하면서 일부 DTO가 불필요한 정보를 포함하거나, 다른 DTO와의 연관성 문제로 인해 데이터가 제대로 불러와지지 않는 현상이 발생했습니다.
- 해결 방법: DTO를 사용하여 필요한 정보만을 담도록 리팩토링하고, 복잡한 관계를 단순화했습니다. 또한, 각 DTO의 역할을 명확히 정의하여 정보의 일관성을 유지하도록 했습니다.
- 해결 후 생각: DTO를 사용할 때는 그 목적과 필요성을 명확히 하고, 단순화하여 코드의 유지보수성을 높이는 것이 중요하다는 것을 깨달았습니다.
팀 협업 및 배운 점
- 협업의 유연함: 팀원들과의 협업이 매우 원활했습니다. 각자의 역할을 명확히 하고, 코드 리뷰와 협업 도구를 통해 소통하면서 프로젝트를 진행했습니다. 서로의 코드를 이해하고 피드백을 주고받는 과정에서 많은 것을 배웠습니다.
- 다른 팀원들의 작업에서 배운 점:
- 예외 처리 및 오류 대응: 다른 팀원이 구현한 예외 처리와 오류 대응 방식에서 많은 영감을 얻었습니다. 특히, 예외를 처리하는 방식과 적절한 에러 메시지를 제공하는 방법에 대해 배웠습니다.
- API 설계와 문서화: API 문서화의 중요성을 실감했습니다. 다른 팀원이 문서화한 API 명세를 보며, 사용자와의 소통을 위해 명확한 문서화가 필요하다는 것을 깨달았습니다.
향후 적용 및 개선 계획
- 팀 프로젝트의 적용: 앞으로 팀 프로젝트에서는 DTO의 사용을 보다 신중히 하고, 복잡한 연관관계는 가능한 한 단순화하여 유지보수성을 높이겠습니다. 또한, 트랜잭션 관리와 Lazy Loading 설정을 통해 성능 문제를 예방할 수 있도록 할 것입니다.
- 프로젝트 경험 반영: 이번 프로젝트에서 얻은 경험을 바탕으로, API 설계와 문서화의 중요성을 다시 한 번 강조하고, 코드 리뷰와 협업의 중요성을 더욱 인식하여 향후 프로젝트에 반영할 계획입니다.
'Spring > 미니 프로젝트(뉴스피드)' 카테고리의 다른 글
SNS 팀 프로젝트(1) (4) | 2024.09.02 |
---|---|
Spring boot을 활용한 일정 관리 프로젝트(2) (0) | 2024.08.29 |
일정 관리 시스템 flowchart (0) | 2024.08.16 |
일정 관리 시스템 (0) | 2024.08.16 |