기존 MyPageController 코드를 보면
엔드포인트에 {userId}가 있고 직접 쿼리 파라미터 입력을 해서 인증을 하는 식이었는데, @AuthCheck와 시큐리티 AOP인증 방식으로 사용자 검증하는 방식으로 변경을 하였다.
결론: AOP(Aspect-Oriented Programming)와 시큐리티를 활용한 방식으로 코드 수정
🔧🔧변경 전 코드🔧🔧
기존 방식: ({userId}를 넣어서 사용자 체크하는 방식)
기존에는 엔드포인트에서 {userId}를 URL 경로 변수로 받아와서 해당 사용자에 대한 검증을 직접 수행하는 방식이었다.
예를 들어, @GetMapping("/user/{userId}") 같은 방식으로 사용자가 요청할 때, URL에 포함된 userId를 추출해 데이터베이스에서 해당 사용자를 찾고 검증합니다. 이 방식은 다음과 같은 문제를 가질 수 있음
- 코드 중복: 여러 곳에서 같은 사용자 검증 로직을 반복적으로 작성해야 할 수 있다
- 코드 가독성 저하: 검증 로직이 엔드포인트마다 흩어져 있어, 코드 유지보수가 어려워질 수 있다
//닉네임 수정
@PutMapping("/{userId}/nickname")
@Operation(summary = SwaggerDocs.SUMMARY_UPDATE_NICKNAME, description = SwaggerDocs.DESCRIPTION_UPDATE_NICKNAME)
public UserInfoResponse updateNickname(
@PathVariable Long userId,
@RequestParam String nickname
) {
return myPageService.updateNickname(userId, nickname);
}
//프로필 이미지 수정
@PutMapping(value = "/{userId}/profile-image", consumes = "multipart/form-data", produces = "application/json")
@Operation(
summary = SwaggerDocs.SUMMARY_UPDATE_PROFILE_IMAGE,
description = SwaggerDocs.DESCRIPTION_UPDATE_PROFILE_IMAGE,
responses = {
@ApiResponse(responseCode = "200", description = "성공적으로 프로필 사진이 수정되었습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserInfoResponse.class))),
@ApiResponse(responseCode = "400", description = "파일 업로드 실패", content = @Content),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.", content = @Content)
}
)
public UserInfoResponse updateProfileImage(
@PathVariable Long userId,
@RequestPart("profileImage") MultipartFile profileImage
) throws IOException {
return myPageService.updateProfileImage(userId, profileImage);
}
//프로필 이미지 삭제
@DeleteMapping("/{userId}/profile-image")
@Operation(
summary = SwaggerDocs.SUMMARY_DELETE_PROFILE_IMAGE,
description = SwaggerDocs.DESCRIPTION_DELETE_PROFILE_IMAGE,
responses = {
@ApiResponse(responseCode = "200", description = "프로필 사진이 성공적으로 삭제되었습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserInfoResponse.class))),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.", content = @Content)
}
)
public UserInfoResponse deleteProfileImage(
@PathVariable Long userId
) {
return myPageService.deleteProfileImage(userId);
}
// /me 엔드포인트 추가
@Operation(summary = SwaggerDocs.SUMMARY_TOKEN_ME, description = SwaggerDocs.DESCRIPTION_TOKEN_ME)
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> getUserInfo(HttpServletRequest request) {
Map<String, Object> userInfo = authService.getUserInfoFromToken(request);
return ResponseEntity.ok(userInfo);
}
//회원 탈퇴
@DeleteMapping("/user/{userId}")
@Operation(summary = SwaggerDocs.SUMMARY_DELETE_USER, description = SwaggerDocs.DESCRIPTION_DELETE_USER)
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
myPageService.deleteUser(userId);
return ResponseEntity.noContent().build();
}
/*
* 댓글 (community)
*/
@GetMapping("/comments/{userId}")
@Operation(summary = SwaggerDocs.SUMMARY_USER_COMMENTS, description = SwaggerDocs.DESCRIPTION_USER_COMMENTS)
public ResponseEntity<List<MyPageCommunityResponse>> getUserComments(@PathVariable Long userId) {
List<MyPageCommunityResponse> comments = myPageService.getUserComments(userId);
return ResponseEntity.ok(comments);
}
/*
* 관심 종목
*/
//관심 종목 추가
@PostMapping("/{userId}/favorite-stocks")
@Operation(summary = SwaggerDocs.SUMMARY_POST_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_POST_FAVORITE_STOCKS)
public ResponseEntity<Boolean> addFavoriteStock(@PathVariable Long userId,
@RequestBody FavoriteStockRequest request) {
boolean isLiked = myPageService.addFavoriteStock(userId, request.getStockCode());
return ResponseEntity.ok(isLiked); // 찜 유무 반환
}
//관심 종목 상세 좋아요 유무 조회
@GetMapping("/{userId}/favorite-stocks/{stockCode}")
@Operation(summary = SwaggerDocs.SUMMARY_GET_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_GET_FAVORITE_STOCKS)
public ResponseEntity<Boolean> isFavoriteStock(
@PathVariable @Parameter(description = "사용자 ID") Long userId,
@PathVariable @Parameter(description = "찜 여부를 확인할 종목 코드") String stockCode
) {
boolean isLiked = myPageService.isFavoriteStock(userId, stockCode);
return ResponseEntity.ok(isLiked);
}
//관심 종목 조회
@GetMapping("/favorite-stocks/{userId}")
@Operation(summary = SwaggerDocs.SUMMARY_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_FAVORITE_STOCKS)
public ResponseEntity<List<FavoriteStockResponse>> getFavoriteStocks(@PathVariable Long userId) {
List<FavoriteStockResponse> response = myPageService.getFavoriteStocks(userId);
return ResponseEntity.ok(response);
}
//관심 종목 삭제
@DeleteMapping("/favorite-stocks/{userId}/{stockCode}")
@Operation(summary = SwaggerDocs.SUMMARY_DELETE_FAVORITE_STOCK, description = SwaggerDocs.DESCRIPTION_DELETE_FAVORITE_STOCK)
public ResponseEntity<Void> deleteFavoriteStock(@PathVariable Long userId, @PathVariable String stockCode) {
myPageService.deleteFavoriteStock(userId, stockCode);
return ResponseEntity.noContent().build();
}
🔧🔧변경 후 코드🔧🔧
AOP 방식 (@AuthCheck + AOP)
AOP는 관심사의 분리를 통해 코드 중복을 줄이고, 핵심 비즈니스 로직과 공통적인 관심사를 분리하는 프로그래밍 패러다임.
AOP를 사용하면 특정 메서드가 실행될 때마다 공통적인 작업을 자동으로 실행할 수 있다
- @AuthCheck: 사용자 인증/검증을 공통으로 처리하는 애너테이션을 정의하고, 이 애너테이션이 적용된 메서드에 대해 AOP로 인증 검증 로직을 처리하는 방식
- 장점:
- 중복 제거: 사용자 인증 및 검증 코드가 AOP를 통해 공통으로 처리되므로, 여러 엔드포인트에서 반복적으로 코드를 작성할 필요가 없음
- 가독성 향상: 인증 및 검증 코드가 분리되어, 엔드포인트의 핵심 비즈니스 로직에 집중할 수 있음
- 유지보수 용이: 인증 로직을 변경해야 할 경우, AOP 관련 코드만 수정하면 되므로 유지보수가 쉬워짐
===>> AOP란?
AOP는 프로그램의 실행 흐름에 개입하여 특정 작업을 자동으로 수행하게 해주는 개념임. 핵심 비즈니스 로직과는 별도로, 공통적인 관심사(예: 로깅, 보안, 트랜잭션 관리 등)를 처리하는데 유용하다
- 핵심 개념:
- Aspect: 공통된 관심사를 모아놓은 코드입니다. 예를 들어, 인증/권한 체크, 로깅 등이 이에 해당
- Join Point: AOP가 적용될 수 있는 위치입니다. 메서드 실행, 메서드 호출 등이 될 수 있음
- Advice: 실제로 실행되는 공통 관심사 코드입니다. 메서드 호출 전, 후, 예외 발생 시 등 다양한 시점에 실행할 수 있음
- Pointcut: Advice를 적용할 Join Point를 정의하는 표현식
AOP를 사용하면 특정 메서드가 실행되기 전후 또는 예외 발생 시에 인증, 로깅, 트랜잭션 등의 공통 작업을 자동으로 처리할 수 있어 코드가 훨씬 깔끔해지고, 유지보수도 수월해짐
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class MyPageController {
private final MyPageService myPageService;
private final AuthService authService;
private final FavoriteStockRepository favoriteStockRepository;
/*
* 내 정보
*/
@GetMapping("/user")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_USER_INFO, description = SwaggerDocs.DESCRIPTION_USER_INFO)
public ResponseEntity<UserInfoResponse> getUserInfo() {
UserInfoResponse response = myPageService.getUserInfo(SecurityUtil.getCurrentUserId());
if (response != null) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
//닉네임 수정
@PutMapping("/nickname")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_UPDATE_NICKNAME, description = SwaggerDocs.DESCRIPTION_UPDATE_NICKNAME)
public UserInfoResponse updateNickname(@RequestParam String nickname) {
Long userId = SecurityUtil.getCurrentUserId();
return myPageService.updateNickname(userId, nickname);
}
//프로필 이미지 수정
@PutMapping(value = "/profile-image", consumes = "multipart/form-data", produces = "application/json")
@AuthCheck
@Operation(
summary = SwaggerDocs.SUMMARY_UPDATE_PROFILE_IMAGE,
description = SwaggerDocs.DESCRIPTION_UPDATE_PROFILE_IMAGE,
responses = {
@ApiResponse(responseCode = "200", description = "성공적으로 프로필 사진이 수정되었습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserInfoResponse.class))),
@ApiResponse(responseCode = "400", description = "파일 업로드 실패", content = @Content),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.", content = @Content)
}
)
public UserInfoResponse updateProfileImage(@RequestPart("profileImage") MultipartFile profileImage) throws
IOException {
Long userId = SecurityUtil.getCurrentUserId();
return myPageService.updateProfileImage(userId, profileImage);
}
//프로필 이미지 삭제
@DeleteMapping("/profile-image")
@AuthCheck
@Operation(
summary = SwaggerDocs.SUMMARY_DELETE_PROFILE_IMAGE,
description = SwaggerDocs.DESCRIPTION_DELETE_PROFILE_IMAGE,
responses = {
@ApiResponse(responseCode = "200", description = "프로필 사진이 성공적으로 삭제되었습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserInfoResponse.class))),
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.", content = @Content)
}
)
public UserInfoResponse deleteProfileImage() {
Long userId = SecurityUtil.getCurrentUserId();
return myPageService.deleteProfileImage(userId);
}
// /me 엔드포인트 추가
@Operation(summary = SwaggerDocs.SUMMARY_TOKEN_ME, description = SwaggerDocs.DESCRIPTION_TOKEN_ME)
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> getUserInfo(HttpServletRequest request) {
Map<String, Object> userInfo = authService.getUserInfoFromToken(request);
return ResponseEntity.ok(userInfo);
}
//회원 탈퇴
@DeleteMapping("/user")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_DELETE_USER, description = SwaggerDocs.DESCRIPTION_DELETE_USER)
public ResponseEntity<Void> deleteUser() {
Long userId = SecurityUtil.getCurrentUserId();
myPageService.deleteUser(userId);
return ResponseEntity.noContent().build();
}
/*
* 댓글 (community)
*/
@GetMapping("/comments")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_USER_COMMENTS, description = SwaggerDocs.DESCRIPTION_USER_COMMENTS)
public ResponseEntity<List<MyPageCommunityResponse>> getUserComments() {
Long userId = SecurityUtil.getCurrentUserId();
return ResponseEntity.ok(myPageService.getUserComments(userId));
}
/*
* 관심 종목
*/
//관심 종목 추가
@PostMapping("/favorite-stocks")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_POST_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_POST_FAVORITE_STOCKS)
public ResponseEntity<Boolean> addFavoriteStock(@RequestBody FavoriteStockRequest request) {
Long userId = SecurityUtil.getCurrentUserId();
return ResponseEntity.ok(myPageService.addFavoriteStock(userId, request.getStockCode()));
}
//관심 종목 상세 좋아요 유무 조회
@GetMapping("/favorite-stocks/{stockCode}")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_GET_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_GET_FAVORITE_STOCKS)
public ResponseEntity<Boolean> isFavoriteStock(@PathVariable String stockCode) {
Long userId = SecurityUtil.getCurrentUserId();
return ResponseEntity.ok(myPageService.isFavoriteStock(userId, stockCode));
}
//관심 종목 조회
@GetMapping("/favorite-stocks")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_FAVORITE_STOCKS, description = SwaggerDocs.DESCRIPTION_FAVORITE_STOCKS)
public ResponseEntity<List<FavoriteStockResponse>> getFavoriteStocks() {
Long userId = SecurityUtil.getCurrentUserId();
return ResponseEntity.ok(myPageService.getFavoriteStocks(userId));
}
//관심 종목 삭제
@DeleteMapping("/favorite-stocks/{stockCode}")
@AuthCheck
@Operation(summary = SwaggerDocs.SUMMARY_DELETE_FAVORITE_STOCK, description = SwaggerDocs.DESCRIPTION_DELETE_FAVORITE_STOCK)
public ResponseEntity<Void> deleteFavoriteStock(@PathVariable String stockCode) {
Long userId = SecurityUtil.getCurrentUserId();
myPageService.deleteFavoriteStock(userId, stockCode);
return ResponseEntity.noContent().build();
}
}
SecurityUtil.java
public class SecurityUtil {
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
throw new RuntimeException("로그인한 사용자가 아닙니다.");
}
return Long.parseLong(authentication.getName());
}
}
AuthCheckAspect.java
@Aspect
@Component
public class AuthCheckAspect {
@Around("@annotation(com.profitkey.stock.annotation.AuthCheck)")
public Object validateUserId(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
boolean hasUserIdProvider = false;
for (Object arg : args) {
if (arg instanceof UserIdProvider) {
hasUserIdProvider = true;
UserIdProvider request = (UserIdProvider)arg;
Long authId = SecurityUtil.getCurrentUserId();
Long requestUserId = Long.parseLong(request.getUserId());
if (!authId.equals(requestUserId)) {
throw new RuntimeException("사용자 인증 실패: 요청한 사용자 ID가 현재 로그인한 사용자와 일치하지 않습니다.");
}
break;
}
}
// request 없을경우 로그인 정보만 확인
if (!hasUserIdProvider) {
Long authId = SecurityUtil.getCurrentUserId();
if (authId == null) {
throw new RuntimeException("로그인이 필요합니다.");
}
}
return joinPoint.proceed();
}
}
AuthCheck 인터페이스
package com.profitkey.stock.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
}
스웨거에서 테스트 할 때에도

토큰 사용하여 진행하면 된다!
'프로젝트' 카테고리의 다른 글
| (ProfitKey) @Lob 애노테이션: 글자수 제한 해제 (데이터 무결성 오류 해결) (0) | 2025.03.19 |
|---|---|
| (ProfitKey) git revert로 깃허브 충돌 이전으로 돌아가기 (0) | 2025.03.18 |
| (ProfitKey) 컨트롤러 경로 변수 userId 인코딩 문제 해결 (0) | 2025.03.03 |
| (ProfitKey) request와 response (0) | 2025.02.27 |
| (ProfitKey) JPA 기본값 빈 문자열로 설정 (0) | 2025.02.24 |