🔗 Gitguh Repository
🔗 Github PR
🟩 BasicAuthArgumentResolver에 Service를 넣어서 로그인 인증하는 것은 괜찮을까?
HTTP Header에 Basic Authentication을 받아서 회원을 컨트롤러의 인자로 받게 구현했다.
이때 인자를 받아올 수 있도록 ArgumentResolver를 커스텀했고 Service 계층을 ArgumentResolver에 두게 되었다.
😄 글쓴이 질문 :
ArgumentResolver에 Service가 있어도 될까?
글쓴이 😄 :
중복되는 로직을 제거하기 위해서 ArgumentResolver에서 AuthService를 이용해서 DB에 접근하여 인증 처리를 진행했습니다.
장바구니에서 회원이 상품을 추가, 삭제, 조회하기 위해서는 인증이 되어있어야 했습니다.
만약 CartService에서 각 기능마다 인증 처리를 진행하면 중복되는 부분이 많아보였습니다.
때문에 인증 처리를 공통적으로 할 수 있는 부분은 인증 정보를 변환하는 ArgumentResolver 부분이라고 생각했습니다.
코드는 아래와 같습니다.
하지만 계층이 파괴된 느낌이 들어서 좋은 방법이라고 느껴지지는 않는데 토니는 어떻게 생각하시는지 궁금합니다 ! 🤔
@Component
public class BasicAuthArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
//...
}
@Service
public class AuthService {
private final MemberRepository memberRepository;
public AuthMember login(final String email, final String password) {
final Member findMember = memberRepository.findByEmail(email)
.orElseThrow(() -> new AuthLoginException("이메일 혹은 비밀번호가 틀렸습니다."));
if (!findMember.getPassword().equals(password)) {
throw new AuthLoginException("이메일 혹은 비밀번호가 틀렸습니다.");
}
return new AuthMember(findMember.getId(), findMember.getEmail());
}
💁♂️ 리뷰어 답변 :
1. ArgumentResolver는 어떤 계층일까?,
2. config.auth 패키지에 AuthService와 AuthMember 객체가 있는 이유
리뷰어 💁♂️ :
1. ArgumentResolver는 개념적으로 어떤 계층에 위치한다 생각하시나요?
2. AuthService, AuthMember 객체를 기존 다른 서비스와 도메인 객체처럼 service, domain이 아닌 config.auth라는 패키지 하위에 두신 이유는 무엇인가요?
😄 글쓴이 답변 :
1. ArgumentResolver는 컨트롤러 이전 계층이라고 생각해야 한다.
💡 계층 위치에 대한 생각
계층이라면 Controller 들어오기 이전이라고 생각합니다.
Controller의 메서드를 실제로 호출하기 이전의 작업이기 때문입니다.
💡 ArgumentResolver에 Service 계층이 들어오면서 발생하는 문제점
DB에 접근하기 위해서는 다음과 같이 계층을 하나하나 넘어가야 했습니다.
클라이언트 요청 --> 컨트롤러 --> 서비스 --> 레포지터리
하지만 BasicAuthArgumentResolver 등장으로 위의 흐름이 깨졌습니다.
클라이언트 요청 --> Auth서비스 --> 컨트롤러 --> 서비스 --> 레포지터리
또한 확인되지 않은 불안전한 자원을 이용한다는 점이라고 생각했습니다.
다만 ArgumentResolver는 자원의 타입을 확인하고 변환시키는 역할을 하고 있습니다.
만약 현재 요청의 자원이 불안전하다면 변환시키지 않을 것이니 역할을 제대로 수행했다고 할 수 있습니다.
💡 ArgumentResolver에서 Service에 접근한다는 생각을 정리하면 다음과 같습니다.
- BasicAuthArgumentResolver에 Service를 이용함으로서 기존 흐름이 깨진다.
- BasicAuthArgumentResovler는 요청의 자원이 타입 변환이 가능한지 확인하고 가능하면 변환시켜주는 역할을 제대로 수행하고 있다.
- ArgumentResolver를Controller 계층이라고 생각하면 Service의 메서드를 호출하는데 문제가 없다.
- ArgumentResolver는 Controller의 메서드를 호출하기 이전에 일어나는 일이니 Controller 계층이라 할 수 없으니 기존 계층의 흐름을 파괴한다.
😄 글쓴이 답변 :
2. config 패키지에 위치한 이유는 찾기 편리하기 때문이다.
💡AuthService와 AuthMember는 config.auth 패키지에 있는 것이 자연스럽지 않다.
AuthService와 AuthMember는 config.auth 패키지에 있는 것이 자연스럽지는 않습니다.
다만 config.auth 패키지 내부에 인증과 관련된 클래스가 모여있을때 쉽게 찾을 수 있다는 점을 고려해서 같은 패키지로 두었습니다.
- AuthService는 BasicAuthArgumentResolver에서만 사용됩니다.
- AuthMember는 어디서든 사용될 수 있습니다.
💁♂️ 리뷰어 답변 :
컨트롤러 레이어의 정의에 대한 확장,
1. config 패키지의 의미
2. 로그아웃 기능이 추가될 경우 로직은 어디에 있어야할까?
🚀 컨트롤러 레이어의 정의에 대한 확장
ArgumentResolver는 컨트롤러 레이어라고 보기는 애매한거 같아요.
다만 HTTP에 대한 처리를 돕는 레이어임은 명확합니다.
해당 클래스가 속한 라이브러리를 보면 알 수 있습니다.
spring-web 하위에 속해있는거를 볼 수 있습니다.
다른 클래스의 이름에는 HTTP와 같은 웹 용어를 확인할 수 있네요.
그렇기에 컨트롤러 옆에서 컨트롤러를 지원한다고 생각합니다.
@ControllerAdive와 연관된 ControllerAdviceBean도 같은 라이브러리에 있습니다.
완벽하게 계층을 분리헀다고 하기보다 컨트롤러 레이러를 조금 넓게 잡아서 컨트롤러 객체, 리졸버 객체, 컨트롤러 어드바이스 객체가 다 포함되어 있는거 같아요.
핵심적으로 @RestController와 같은 어노테이션도 이 라이브러리에 포함되어 있습니다.
라이브러리에 포함되었음이 절대적인 기준이 될 수는 없지만 컨트롤러 레이어의 정의에 대한 확장하는데 도움이 될 거 같네요.
🚀 config 패키지는 어떤 의미일까요?
- configuration : 구성, 설정
AuthMember는 구성, 설정일까요?
혹은 장바구니 프로젝트에서 의미있는 도메인인지 생각해보시면 좋을거 같습니다.
🚀 로그아웃 기능 추가 시 로직이 어디에 추가될까요?
로그인시에 같은 아이디로 로그인이 되어있다면 로그아웃 시키는 것과 같은 추가적인 비즈니스 로직이 복잡해질 수 있을거 같아요.
😄 글쓴이 답변 :
1. config 패키지란
2. 로그아웃 기능이 추가될 경우
💡Config, Configuration 에 대해서
Configuration은 무언가의 구성 정보라는 의미입니다.
현재 스프링 프레임워크를 이용하고 있고 대부분의 기능을 사용하기 위해서는 빈으로 등록되어야 합니다.
그리고 빈으로 등록되기 위해서는 정보가 필요합니다.
때문에 config 패키지를 빈 등록 정보를 기준으로 잡았습니다.
현재 config 패키지는 아래 그림과 같습니다.
실제 빈 등록 정보가 있는 클래스는 AuthService, BasicAuthArgumentResolver, BasicAuthInterceptor입니다.
@Service
public class AuthService {}
@Component
public class BasicAuthArgumentResolver {}
@Component
public class BasicAuthInterceptor {}
이제 다음과 같이 패키지를 분리하게 됐습니다.
Exception, DTO, Annotation을 기준으로 분리했습니다.
💡로그아웃 기능이 추가될 경우
기존에 구현에 놓은 BasicAuthArgumentResolver를 이용해도 될거 같지만 다른 요구사항이 많아질수록 역할이 너무 많아질거 같습니다.
문제를 해결하기 위해서는 Interceptor를 이용하는 편이 더 좋다고 느껴졌습니다.
ArgumentResolver는 체이닝이 불가능하지만 Interceptor는 체이닝이 가능하기 때문입니다.
때문에 다음과 같이 로그인 검증, 로그인, 로그아웃 등을 분리해서 구현할 수 있습니다.
💁♂️ 리뷰어 답변 :
패키지를 분리하는 이유
🚀 패키지를 분리하는 이유?
패키지를 분리한다면
접근 제한자를 이용하여 파일 사용처를 제한하는 기능을 활용할 수 있습니다.
또한 클래스를 논리적인 단위로 묶어줄 수도 있습니다.
위 문제에서 레이어별로 auth에 대한 개념이 드러나는 것은 좋을 수 있습니다.
하지만 단점도 있죠.
auth에 대한 로직이 여러 패키지에 퍼지게 됩니다.
이러한 경우는 auth에 대한 로직을 찾기 힘들 수 있습니다.
즉, 프로젝트에 맞는 기준을 세워서 패키지를 분리해야 합니다.
🟩 Interceptor와 ArgumentResolver 동작 시점
💡 Interceptor
URI에 매핑된 인터셉터들이 (컨트롤러 메서드 호출 전/후) 호출되어 조건에 맞는지 검사하게 됩니다.
대표적인 메서드로 preHandle(), postHandle()메서드가 있습니다.
인터셉터가 매핑하는 URI에 맞을 때 HandlerExecutionChain에 넣게 됩니다.
요청한 URI에 맞는 컨트롤러 메서드 정보를 갖고있는 HandlerMethod와
요청한 URI에 맞는 interceptor들을
HandlerExecutionChain이 가지고 있게 됩니다.
이후 DispatcherServlet의 doDispatch() 메서드에서 HandlerAdapter를 통해서 실제 로직을 수행하기 전에 Interceptor의 preHandle() 메서드를 먼저 호출합니다.
이 때 등록된 Interceptor가 false를 반환하면 곧바로 doDispatch() 메서드가 종료됩니다.
💡 ArgumentResolver
컨트롤러 메서드의 파라미터 값으로 변환이 가능한지 확인하고 가능하다면 변환해서 넣어주는 역할을 하고 있습니다.
각 인터셉터의 preHandle() 메서드가 조건에 만족하여 모두 true를 반환헀다면
HandlerAdapter의 handle()메서드가 호출됩니다.
내부로 들어가면 요청받은 자원을 컨트롤러 메서드의 파라미터 타입에 맞게 변환시켜주기 위해서
해당 타입 변환을 지원하는 ArgumentResolver가 있는지 찾아 있는 경우 변환시켜주는 역할을 하고 있습니다.
🟩 테스트 중복 로직은 어느 수준까지 제거해야할까?
회원(Member)과 상품(Product)을 저장하는 테스트에서 중복을 발견했습니다.
@Test
void 회원의_장바구니에_상품을_추가한다() {
// 수정 전 로직 중복 코드
final MemberId memberId = memberRepository.save(MEMBER);
final ProductId productId = productRepository.save(CHICKEN);
// ...
}
@Test
void 회원의_장바구니에_상품을_삭제한다() {
// 수정 전 로직 중복 코드
final MemberId memberId = memberRepository.save(MEMBER);
final ProductId productId = productRepository.save(CHICKEN);
// ...
}
테스트에서의 중복을 제거하기 위해서 @BeforeEach를 사용했습니다.
@BeforeEach
void setUp() {
memberId = memberRepository.save(MEMBER);
productId = productRepository.save(CHICKEN);
}
현재 상황에서는 테스트 클래스 전체에 중복이 발생하는 경우였기에 @BeforeEach로 간단하게 제거할 수 있었습니다.
만약 아래와 같은 상황이라면 코드 중복 제거가 불필요하지 않을까요?
- @BeforeEach에서 미리 데이터를 저장해놓은 것은 낭비일 경우
- 테스트에서 어떤 데이터가 필요한지 파악하기 어려워지는 경우
💡 중복 제거하는 방법
테스트에서 중복을 제거할 수 있는 방법은 다양합니다.
@BeforeEach, private 메서드, Fixtures를 이용해봤을 때 저는 Fixtures가 가장 편리하고 좋다는 생각이 들었습니다.
하나하나 간단하게 알아봅시다!
01. @BeforeEach
테스트 클래스 전체에 중복이 발생하는 경우에 사용하면 좋다.
하지만 각 테스트마다 중복되는 부분에 차이가 있을 수 있어서 사용하기 힘든 경우가 많다.
02. private 메서드로 중복 제거
범용적으로 적용하기 좋은 방법이다.
테스트를 추상화시킬 수 있기 때문에 구체적인 코드보다 이해하기 쉽게 구성할 수있다.
03. Fixtures 이용하기
테스트를 통해서 코드로 전달하지 못하는 개념을 전달할 수있습니다.
코드를 읽는 입장에서 조금 더 명확하게 인지할 수 있습니다.
아래와 같이 상태를 파라미터로 받아서 동적으로 회원과 상품을 생성할 수 있습니다.
public class MemberFixtures {
public static Member 회원을_등록한다(String 회원명, String 이메일) {
return new Member(회원명, 이메일);
}
}
public class ProductFixtures {
public static Product 상품을_등록한다(String 상품명, Integer 가격) {
return new Product(상품명, 가격);
}
}
이렇게 될 경우 현재 테스트에서 중복되는 로직을 Fixtures를 통해서 편리하게 사용할 수 있습니다.
또한 원하는 상태를 테스트에서 직접 넣어 이용할 수 있기에 보다 유연한 테스트도 가능합니다.
@Test
void 회원의_장바구니에_상품을_추가한다() {
// 수정 전 로직 중복 코드
final Member member = MemberFixutres.회원을_등록한다("헤나", "test@test.com");
final Product product = ProductFixtures.상품을_등록한다("치킨", 20_000);
// ...
}
@Test
void 회원의_장바구니에_상품을_삭제한다() {
// 수정 전 로직 중복 코드
final Member member = MemberFixutres.회원을_등록한다("헤나", "test@test.com");
final Product product = ProductFixtures.상품을_등록한다("치킨", 20_000);
// ...
}
저는 Fixtures를 사용할 때 가장 큰 장점은 바로 "테스트 할 부분을 명확하게 표현할 수 있다"는 점입니다.
아래와 같이 이용할 수 있습니다.
Fixture를 통해서 필요한 부분 데이터를 명확하게 알려줄 수 있습니다.
또한 현재 테스트에서 중요한 기능과 구별지을 수 있다는 점에서 가독성을 높일 수 있다는 점이 장점이라고 생각합니다.
@Test
void 회원의_장바구니에_상품을_추가한다() {
// given
Member member = 회원을_등록한다("헤나", "test@test.com");
Cart cart = 회원의_장바구니를_등록한다(member);
Product product = 상품을_등록한다("치킨", 20_000);
// when
cart.addProduct(product);
// then...
}
👊 정리하자면
- ArgumentResolver의 계층
- Controller Layer라고 하기는 애매하지만 HTTP 처리를 돕고 있다.
- config 패키지
- 빈 등록 정보가 들어있는 패키지 (개인적인 생각)
- 패키지 분리는 프로젝트마다 다를 수 있다.
- Interceptor와 ArgumentResolver 동작 시점
- Interceptor
- URI에 매핑되는 인터셉터는 HandlerExecutionChain에 주입된다.
- DispatcherServlet에서 실제 비즈니스 로직을 호출하기 전에 Interceptor의 preHandle() 메서드를 동작시킨다.
- ArgumentResolver
- Interceptor의 preHandle() 메서드가 호출되고 난 후 찾은 HandlerAdapter의 handle() 메서드를 호출하고 내부 로직에서 ArgumentResolver를 통해 컨트롤러의 메서드 파라미터 타입에 맞게 데이터를 변환시킨다.
- Interceptor
- TestFixtures
- 테스트에서 중복되는 데이터(메서드)를 Fixtures를 통해 해결할 수 있다.
- 테스트 할 기능을 구별짓기 편하기 때문에 가독성이 높아진다.
- 생산성이 증가한다.
'👨🚀 우아한테크코스 5기' 카테고리의 다른 글
[20230607] 우아한테크코스 5기 LEVEL2 - 레벨2 및 인터뷰 회고 (0) | 2023.06.23 |
---|---|
[20230521] 우아한테크코스 5기 LEVEL 2 - 지하철 미션, 추가 비용 정책을 데이터베이스에서 가져오려면 (0) | 2023.05.21 |
[20230427] 우아한테크코스 5기 LEVEL 2 - 장바구니 1단계 (4) | 2023.05.02 |
[20230424] 우아한테크코스 5기 LEVEL 2 - 웹 자동차 경주 1단계 & 2단계 회고 (3) | 2023.04.16 |
[20230329] 우아한테크코스 5기 LEVEL 1 - 레벨 인터뷰 회고 (0) | 2023.04.01 |