💡 발단
현재 프로젝트에선 기존에 로그인된 유저 정보를 불러오는 편의메소드를 제작하여 사용했다.
public class SecurityUtils {
public static String getCurrentUserSocialEmail() {
// 유저 정보를 불러오는 부분
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new UserException(StatusCode.NOT_FOUND_USER);
}
// 유저 식별값
return authentication.getName();
}
}
이렇게 만들어둔 메서드를 UserService에서 불러와 DB에서 조회하는 방식이다.
public User getCurrentUser() {
return userRepository.findBySocialEmail(getCurrentUserSocialEmail()).orElseThrow(() -> new UserException(StatusCode.NOT_FOUND_USER));
}
이렇게 두면 유저 정보가 필요한 여러 Service들에서 UserService에 의존해 유저 정보를 간편하게 가져올 수 있다.
// 내가 북마크한 리스트 조회
public PageResponse<LocationCardResponse> getMyBookmarkedLocations() {
User currentUser = userService.getCurrentUser();
Page<Location> resultPage = locationRepository.getBookmarked(currentUser.getId());
return makeLocationCardPageResponse(currentUser, resultPage);
}
유저 정보를 유저 서비스에서 가져온다. 말이 된다!
🤔 그런데... Security에 관련된 로직은 서비스보다는 컨트롤러에서 하는게 맞지 않을까?
❗Spring 계층 구조를 생각해보자
웹 계층이란?
컨트롤러 (@Controller)와 view템플릿 영역.
필터 (@Filter), 인터셉터, @Controller Advice 등의 외부 요청 및 응답에 대한 영역.
Spring Security (Filter)는 웹 계층이다.
JWT 인증과 유저 조회는 필터에서 이뤄지고 있으므로, 그 유저 정보를 다루는 것도 웹 계층이어야 한다.
계층 구조(Layered Architecture)의 핵심은 "관심사 분리"라고 말할 수 있습니다.
각각의 계층은 자신이 담당하는 책임을 지키며 하위 계층에 대해서 의존합니다.
웹 -> 서비스 -> 레포지토리
Service 계층은 현재 유저가 어디에서 왔는지 알 필요가 없어야 한다.
서비스 계층은 그저 비즈니스 로직을 처리하고 레포지토리를 의존하기 위해 필요한 정보들만 받으면 된다.
Web 레이어의 경우 의존의 목적이 명확하다. 웹 레이어는 클라이언트로부터 요청 및 응답에 대한 처리를 해주는 계층이다.
내부 비즈니스 로직은 여기서 처리하지 않는다. 웹이 서비스에 의존해야 하기 때문이다.
그렇다면 서비스는? 웹에서 요청한 게 무엇이든 간에 자신이 해야 할 일(비즈니스 로직)만 처리하면 된다.
❗ 프로그래밍적 관점에서
어떤 함수는 블랙 박스(내부 로직)에 필요한 모든 정보를 인자값으로 명시적으로 받는 것이 좋다.
예를 들면 곱하기 함수가 있다.
multiply(x, y) → x * y
해당 함수는 내부 로직에 필요한 인자값을 모두 받았다. 따라서 프로그래밍적으로 좋은 함수라고 할 수 있다.
반면에 이런 함수 가 있다고 하자.
multiply(x) → { return x * 10; }
함수명과 인자값만 보고는 이 함수가 무슨 일을 하는지 알 수 없다.
내부에서 10을 곱하는지 100을 곱하는지는 직접 이 함수에 들어가서 내용물을 봐야만 알 수 있다.
사실 간단한 함수라면 크게 지장 없겠지만, 나중에 애플리케이션이 복잡해질수록 전체 흐름을 파악하기 어려워진다. (유지보수성)
위 예시에서의 유저 정보가 여기서 10에 해당한다.
당연히 개발자 본인은 한눈에 알 수 있겠지만, 이 함수를 처음 쓰는 사람들에게는 미스테리하게 느껴질 수밖에 없다.
🛠 리팩토링
아까 코드를 다시 보자.
// 내가 북마크한 로케이션 리스트 조회
public PageResponse<LocationCardResponse> getMyBookmarkedLocations() {
// 웹 계층에 의존
User currentUser = userService.getCurrentUser();
// 정작 필요한 것은 user id뿐
Page<Location> resultPage = locationRepository.getBookmarked(currentUser.getId());
return makeLocationCardPageResponse(currentUser, resultPage);
}
리팩토링 결과 :
// 내가 북마크한 로케이션 리스트 조회
public PageResponse<LocationCardResponse> getMyBookmarkedLocations(Long userId) {
Page<Location> resultPage = locationRepository.getBookmarked(userId);
return makeLocationCardPageResponse(userId, resultPage);
}
웹 계층에 의존하지도 않고, 프로그래밍적으로도 명확하게 만들었다.
컨트롤러도 이에 맞게 리팩토링했다.
@GetMapping("/bookmarks/me")
public ApiResponse<PageResponse<LocationCardResponse>> getMyBookmarkedLocations() {
// 같은 웹 계층에 의존
Long currentUserId = userService.getCurrentUser().getId();
PageResponse<LocationCardResponse> pageResponse = locationService.getMyBookmarkedLocations(currentUserId, pageable, cursorId);
return success(pageResponse);
}
그런데 여기서 한번 더 리팩토링할 수 있다.
@AuthenticationPrincipal
찾아보니 컨트롤러 매개변수에 유저 정보를 넣는 방법이 있었다.
@AuthenticationPrincipal : 쓰레드에 저장된 유저 정보인(UserDetails)에 접근할 수 있는 어노테이션. 내부적으로는 Authentication.getPrincipal() 을 호출한다.
리팩토링 후 :
@GetMapping("/bookmarks/me")
public ApiResponse<PageResponse<LocationCardResponse>> getMyBookmarkedLocations(
// 매개변수로 유저 정보 불러옴
@AuthenticationPrincipal PrincipalDetails principalDetails
) {
PageResponse<LocationCardResponse> pageResponse = locationService.getMyBookmarkedLocations(principalDetails.getUser().getId());
return success(pageResponse);
}
모든 API가 유저 정보를 필요로 하는건 아니기 때문에 이렇게 명시하는건 가독성에 도움이 된다.
게다가 어떤 API들은 유저 인증이 되지 않은 상태가 정상인 경우가 있다. (로그인, 회원가입 등)
그렇다면 컨트롤러 메서드에서 “유저 로그인이 필요해. 유저 인증을 거치지 않은 상태라면 이 엔드포인트는 제 기능을 못 할거야”라고 명시하는 것이 좋다.
참고 :
https://any-ting.tistory.com/137
'Back-end > Spring Boot' 카테고리의 다른 글
[Spring Boot] 연관관계 생성 메서드 삽질 (0) | 2023.08.23 |
---|---|
[Spring JPA] 실시간으로 적재되는 데이터와 부모 엔티티 묶어서 가져오기 (0) | 2023.08.10 |
[Spring Boot] 스웨거 springdoc-openapi 적용 (webmvc-ui) (0) | 2023.08.09 |
[Spring Boot] 예외처리 야무지게 하기 (0) | 2023.08.04 |
[Spring Security] JWT 토큰 인증 이후에 유저 엔티티 객체 불러오기 (Authentication 객체가 담기는 곳) (0) | 2023.07.24 |