TIL - 0531

스프링부트

HTTP 세션 데이터 쉽게 가져오기 - 커스텀 파라미터 만들기

  • HandleMethodArgumentResolver를 사용함
    • 파라미터를 커스텀하는 역할, 두개의 메소드를 오버라이드 한 후 스프링 MVC에 리졸버를 등록하고, 실제로 적용할 부분에 선언해서 사용함
    • Interceptor와 컨트롤러 로직 사이에 존재하는 것으로 생각 : 코드 중복을 줄일 수 있음(같은 파라미터를 가지고 기능 구현을 한다면)

어노테이션 생성하기 : 커스텀 파라미터로 사용할 파라미터라는 표시(설정)

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
    public boolean required() default true;
}

커스텀 파라미터 만들기 - HandleMethodArgumentResolver

public class LoginUserHandlerMethodArgumentResolver implements HandleMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user = HttpSessionUtils.getUserFromSession(webRequest);
        if (!user.isGuestUser()) {
            return user;
        }

        LoginUser loginUser = parameter.getParameterAnnotation(LoginUser.class);
        if (loginUser.required()) {
            throw new UnAuthorizedException("You're required Login!");
        }
        return user;
    }
}
  • supportsParameter : 리졸버가 해당 클래스에 적용되는지 확인 - LoginUser(어노테이션)의 적용범위가 ElementType.PARAMETER인지 확인함
  • resolveArgument : 파라미터를 커스텀 하는 메소드, 리턴으로 지정하는 객체가 파라미터로 사용됨

커스텀 파라미터 MVC에 적용 : 실제로 사용하기위해선 설정사항에 넣어야함

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    .
    .
    .
    
    @Bean
    public LoginUserHandlerMethodArgumentResolver loginUserArgumentResolver() {
        return new LoginUserHandlerMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserArgumentResolver());
    }
}

  • @Bean 적용 메소드 : 리졸버 스프링 빈을 생성하는 메소드
  • addArgumentResolvers : 생성한 리졸버(스프링빈)를 리졸버 리스트에 추가함 - 실행하기위해선 추가해줘야하나봄

커스텀 파라미터 사용하기

@Controller
public class UserController {
   
   .
   .
   .
   
   @PutMapping("/{id}")
   public String update(@LoginUser User user, @PathVariable Long id, User updateInfo) {
       ...
   }
}
  • @LoginUser : 치환한 어노테이션명
  • User : 커스텀 파라미터의 리턴 타입, resolveArgument에서 리턴한 오브젝트의 타입

Null Object 패턴 처리와 Optional 처리

  • 두 방법은 모두 안전한 NullPointerException 처리를 위한 방법
  • 두 방법의 차이라면 Null을 래핑하는 오브젝트 역할이 많다면, 많게된다면 Null Object 패턴을 사용하는 것이 좋다고 생각됨
    • 그렇지않다면 일일이 Optional에서 객체가 존재한다면 get()을 통해서 뺴온 후 작업을 해야함 : 코드를 지저분하게 만듦(따로 로직을 만들 필요가 없음), 새로운 null check 패턴을 배워보니 신기!
public class User {
    private static GuestUser guestUser = new GuestUser();
    
    public boolean isQuestUser() {
        return false;
    }
    
    
    private static class GuestUser extends User {
        
        @Override
        public boolean isQuestUser() {
             return true;
        }
    }       
}

Mockito 활용해서 테스트 진행하기

  • RestTemplate가 end to end 테스트였다면, 서버 로직에 대한 단위 테스트를 하려면 Mock Test를 함 : Mockito 사용
    • 두 테스트의 케이스는 같지만 테스트 하고자 하는 바가 다름
  • Mock 객체 DI를 활용한 테스트 : mock 개념이 없었다면 서버 프로그램의 단위 테스트를 위해서 환경 세팅을 다 해두고 브라우저로 테스트하는 것처럼 했어야했음
    • Mock 객체 사용이 가능한 이유는 스프링이 객체지향적인 프레임워크라서 : 해당 타입 혹은 하위 타입의 인스턴스라면 DI할 수 있기때문(표준 인터페이스를 만들어두고 인터페이스를 구현) - 같은 메소드로 같은 맥락의 기능을 사용하지만 각각의 인스턴스에 따라 다르게 동작
    • Mock 테스트는 실제 서블릿 컨테이너 환경을 구성하지않고 테스트하기때문에 빠른 테스트가 가능함
@RunWith(MockitoJUnitRunner.class)
public class QnaServiceTest {

    @Mock
    private QuestionRepository questionRepo;

    @InjectMocks
    private QnaService qnaService;

    @Test
    public void create() {
        Question question = new Question("test", "test");
        when(qnaService.create(new User(), question)).thenReturn(question);
    }
    
    .
    .
    .
    .
    .
    생략
}
  • RunWith(MockitoJUnitRunner.class) : 테스트를 Mockito를 통해서 하겠다는 설정
  • @Mock : mock 객체 생성하겠다는 설정
  • @InjectMocks : 해당 객체의 디펜던시(위임 대상)로 @Mock으로 생성한 객체를 인젝션하겠다는 설정
  • when ~ thenReturn : Mockito에서 제공하는 테스트 관련 메소드, 직관적임, 테스트 프레임워크 사용과 관련해서는 너무 많으니깐 생략! 쓰면서 배우자~~!
    • 성공 케이스보다 중요한건 뭐다? 실패 케이스
    • 해당 부분에 대한 로직 처리가 되어있는지 확인하는 것이 핵심

자바

VO와 DTO

  • VO(Value Object) : 말그대로 값을 저장하는 객체, 값이라하면 변수와는 반대되는 개념, immutable object, 한번 초기화 이후에는 상태값이 변경될 수 없음
    • String은 불변객체 : 내부적으로 값을 변경하면 새로운 객체를 만들어서 리턴함
    • 아래의 예제는 Car를 가변객체를 불변객체로 만들려면 어떤 작업을 해야하는지 볼 수 있는 코드임 : 불변객체를 만들면 힙 메모리 효율이 떨어지겠지만 불변객체가 필요할 떄가 있음, 가변 객체와 불변객체 둘 중 어떤 것을 사용할지 상황에 따라 선택하기 - 어찌되었건 이런 선택지가 있다는 점을 알게됨
public class Car {
    private int position;
    private MoveStrategy strategy;
    
    public Car(MoveStrategy strategy, int position) {
        this.strategy = strategy;
        this.position = position;
    }


    // mutable
    public void move(int randomVal) {
        if (strategy.isMovable(randomwVal) {
            position++;
        }
    }    
    
    // immutable
    public Car move(int randomVal) {
        if (strategy.isMovable(randomwVal) {
            return new Car(strategy, position++);
        }
        return this;
    }
}

  • DTO : DTO의 역할은 도메인 객체를 그대로 전달하지않고 당시의 상태를 프리징시키는 역할(비즈니스 로직과 상태값을 따로 떼어냄 - 로직을 더이상 수행하지못하도록)
    • VO와는 달리 set 메소드가 있어서 변경은 할 수 잇음 : mutable
  • 주의할 점 : setter가 없다고 하더라도 조심해야할 점은 인스턴스 변수(상태)가 레퍼런스 타입이라면 getter 메소드로 get하여 해당 객체를 변경시킬 수 있음
    • 콜렉션 프레임워크를 인스턴스 변수로 사용한다면 조심해야함
    • Collections.unmodifiableList() 와 같이 읽기전용 콜렉션으로 변경시켜 get하게끔 해야함
public class Question {
    
    private List<Answer> answers = new ArrayList<>();
    
    public List<Answer> getAnswers() {
        return Collections.unmodifiableList(answers);
    }
}