콘텐츠로 이동

2021 06 23 스프링

2021-06-23 스프링

IoC

  • IoC가 뭔가요? - IoC란 Inversion of Control이라는 뜻으로, 제어의 역전 이라는 뜻입니다. - 여기서 말하는 제어란, 객체 자체가 언제 어떻게 생성되고, 어떤 객체를 사용할지 결정하는 것을 말합니다. - Level1 미션에서 만들었던 여러 미션들이 객체가 언제 만들어지고, 어떤 객체를 사용할지 컴파일 타임에 알 수 있습니다. - Level2에 들어와서 Spring에서는 IoC Container인 Bean Factory가 등장하면서 제어의 역전이 이루어집니다. - 제어권, 즉 객체 생성/소멸 및 사용처에 대한 권한을 상위 템플릿 메서드에게 넘기고, 객체 자신은 필요할 때 호출되어 역할을 다하게 됩니다.
  • IoC는 왜 필요하죠? - 저는 해당 개념이 프레임워크가 도입되면서 필요해졌다고 생각합니다. - Level1에서 진행했던 미션들은 컴파일 타임에 사용될 객체들과 그들의 의존관계를 모두 알고 있었습니다. - 규모가 작아서 일일이 작업이 가능했습니다. - 하지만 Level2를 스프링을 접하고서는 IoC가 필요했음 - 점점 규모가 커진다 - 하나하나 의존관계를 맺어주어 코드를 작성하는 것은 피로함 - 하나하나 객체들을 싱글톤으로 만들어주는 것 피로함 - 즉 객체 생성/소멸/사용 등의 책임을 지는 IoC를 도입하면서 되려 유지/보수/확장에 유리해졌음
  • IoC가 하는 역할은 무엇인가요? - 빈 팩토리가 하는 역할을 말하는 것이 좋을 것 같습니다. - 우선 컴포넌트 스캔 - 어노테이션을 파악해 스프링 프로그램 운영에 필요한 객체들을 빈으로 등록합니다. - 싱글톤으로 빈들 관리 - 등록된 빈들은 싱글톤으로 관리되어 서버 운영시 빈들을 필요 이상으로 재생성하지 않아도 됩니다.
  • IoC는 객체지향적인가요? - 우선 객체지향 == 유지/보수/확장에 유리한 코드로써, 각 객체에 역할을 부여한 프로그래밍 방식 - 제가 생각하는 IoC의 역할은 프로그램 전반에서 사용될 객체들을 관리하는 역할 - 객체들을 관리하는 것 자체가 하나의 역할로 분담할 수 있다고 본다. - 템플릿 메소드 패턴 등이 언급되는 것도 이와 같은 원리라고 생각
  • IoC 컨테이너는 뭘까요? - 스프링에서 IoC 컨테이너는 빈 팩토리, 어플리케이션 컨텍스트 등으로 불립니다. - 스프링 환경에서 사용하는 빈들을 등록/생성/조회/반환/관리 합니다.
  • DI는 뭘까요? - DI는 Dependency Injection으로 의존관계 주입입니다. - DI는 런타임에 각 객체들 사이에 필요한 의존관계들을 맺어주게 됩니다. - 인터페이스에 대해서만 의존관계를 만들자 - 낮은 결합도

Mocking

  • Mocking은 뭔가요? - Mock이란 테스트 대역으로써, 실제 객체가 아닌, 원하는 행동 양식을 지정해서 테스트 용으로 쓰이게 되는 객체를 말합니다.
  • Mocking 왜 필요하나요? - 여러가지 클래스들이 의존하면서, 현재 테스트하고자 하는 클래스의 로직이 다른 클래스의 로직에 따라 변화되는 경우가 생겼었다. - Service에 대한 테스트 코드를 작성하면서 가장 많이 Mocking을 사용했습니다. - Service 테스트할 때 findBy~ 나온 결과를 가지고 로직을 처리하는 경우가 있었다. - 이런 경우 Dao의 행동을 정해줘 테스트하고자 하는 로직을 테스트하는 것이 좋았다. - 또한 외부 라이브러리를 활용한 코드같은 경우, 가령 JwtTokenProvider를 Mocking함으로써 Service 자체 - Path 미션 같은 경우 JwtToken으로... - validation true -> LoginUser - validation false -> AnonymousUser
    - 따라서 테스트하고자 하는 로직을 테스트하기 위해, 다른 클래스의 행동 양식을 지정해줬어야 했습니다.
  • Mocking은 어떻게 하나요? - Mocking 하고자 하는 빈을 @MockBean어노테이션을 사용해 덮어 씌웠고, 테스트에서 필요한 행동 양식을 지정해 주었습니다.
    @MockBean
    private JwtTokenProvider jwtTokenProvider;
    
    @Test
    @DisplayName("인가된 사용자가 요청하면, LoginUser를 반환한다.")
    void findMemberByTokenLoginUser() {
        final String token = "token";
        final String testEmail = "test@test";
    
        Mockito.when(jwtTokenProvider.validateToken(token)).thenReturn(true);
        Mockito.when(jwtTokenProvider.getPayload(token)).thenReturn(testEmail);
        Mockito.when(memberDao.findByEmail(testEmail))
                .thenReturn(Optional.of(new Member(1L, testEmail, "test", 20)));
    
        final User user = authService.findMemberByToken(token);
        assertThat(user.getEmail()).isEqualTo(testEmail);
        assertThat(user.getAge()).isEqualTo(20);
        assertThat(user.getId()).isEqualTo(1L);
    }
    

    - 또한 해당 객체 자체를 @Mock 어노테이션을 통해 모킹하여 테스트를 자체적으로 실행할 수 있다고 들었는데 사용해본적은 없다

  • 언제 Mocking을 해봤나요? - Mocking을 처음했던 경우는 Controller를 테스트 할때 Service의 로직을 정의해줘야 했기 때문입니다. - 지금 생각해보면 Controller가 굳이 service의 로직을 호출하여 Validation을 진행할 필요는 없었는데, Session 검증에 대한 로직을 Controller에 만들어줘서 그런일이 있었다. - 어쨋든, 해당 Controller는 Service의 메서드를 if문으로 가지고 있었다. - Session 검증로직 - 이때 잘 Service 클래스를 @MockBean으로 Bean 자체를 mockBean으로 덮어씌웠고, 다음과 같이 when, thenReturn으로 행동을 지정해줌
    Mockito.when(userService.validateUser("test", "test")).thenReturn(false);
    
      - 이렇게 함으로써 Controller에서 검증하려고 하는 로직을 수행할 수 있었음
    
  • Mocking의 장점과 단점에 대해 알려주세요 - 장점 - 우선 간편합니다. - 테스트를 하려고하는 로직에 집중하여 테스트를 작성할 수 있었습니다. - Mocking을 하지 않으면 Given 부분이 너무 많아질 것이란 생각도 들어 해당 비효율을 줄일 수 있었음 - 단점 - 테스트하고자 하는 대상이 아닌 객체를 mocking을 했다는 것 자체가 내부구현을 미리 알고있기 때문에 가능했다고 생각합니다. - 가령 service 테스트를 작성하려했는데, Dao를 Mocking하고, jwtTokenProvider를 Mocking한 경우처럼 말이죠 - 이런 테스트 코드 작성은 변화에 유연하지 못합니다. - Dao가 변경되도 Service 테스트 코드를 변경시켜야하는 불상사가 발생합니다. - 따라서 Service 테스트에서도 그냥 Dao를 주입받아 필요한 정보를 저장해주고 사용하는 형식으로 변경하는 것이 좋아보입니다.
  • MockMvc는 뭔가요? - 참고: https://woowacourse.github.io/javable/post/2020-08-19-rest-assured-vs-mock-mvc/ - MockMvc란 애플리케이션 서버에 배포하지 않고도 스프링 MVC의 동작을 재현할 수 있는 클래스 입니다. - Controller 테스트에서 사용했었습니다.
    @SpringBootTest
    @AutoConfigureMockMvc
    class GameControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private UserService userService;
    
        @Test
        @DisplayName("POST: /signup이 JSON으로 id와 password를 받으면, 정상 작동한다")
        void signup() {
            mockMvc.perform(post("/signup")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"id\" : \"" + "test" + "\"" + ", " +
                            "\"password\" : \"" + "test" + "\"" +
                            "}"))
                    .andExpect(status().isOk());
        }
    }
    

    - MockMvc는 DispatcherServlet에 요청을 보냄 (테스트 용으로 확장된 TestDispatcherServlet) - DispatcherServlet에 요청을 보냈으니, 매핑 정보를 보고 그에 맞는 핸들러 메서드 호출 - 테스트 케이스 메서드는 MockMvc가 반환하는 실행결과 받아 맞는지 검증 - 완벽히 이해하고 쓰진 못한것 같음 - 오히려 통합테스트 마냥 @SpringBootTest를 쓴 것 같아 마음이 아픔

  • RestAssured와는 어떻게 다른가요? - RestAssured는 자바 라이브러리고, REST 어플리케이션의 전구간 테스트를 지원합니다. - 저는 인수테스트를 작성할때 RestAssured를 사용했습니다. - 실제 배포환경과 동일하게, 하나의 클라이언트가 실제 서버에게 HTTP 요청을 보내면서 테스트를 구성합니다. - 따라서 다음과 같은 환경의 테스트가 필요합니다. - 포트도 받고, 빈도 다 띄우고 - 따라서 느림
    @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class AcceptanceTest {
        @LocalServerPort
        int port;
    
        @BeforeEach
        public void setUp() {
            RestAssured.port = port;
        }
    }
    

Spring Test

  • 테스트 코드는 왜 작성해야 하나요? - 빠른 피드백을 위해 필요합니다. - 기능 수정 및 추가에 대해 빠르게 피드백을 받고 대처할 수 있습니다.
  • 어떤 방식으로 테스트 코드를 작성하나요? - 우선 테스트를 추가하면서 이를 통과시키는 프로덕션 코드를 작성합니다. - 이렇게 함으로써 자연스레 프로덕션에서 어떠한 요청과 응답이 오가는지 설계할 수 있습니다.
  • 인수 테스트, 통합 테스트, 단위 테스트에 대해 설명해주세요 - 인수 테스트 - 사용자 시나리오 테스트 - 비즈니스에 초점 - RestAssured & MockMvc - 통합 테스트 - 개발자가 변경할 수 없는 부분까지 묶어 검증 (ex. 라이브러리) - DB 접근 or 전체 코드와 다양한 환경의 동작 확인 - @SpringBootTest - 단위 테스트 - 클래스/메서드 수준의 가장 작은 SW를 실행하여 동작 확인 - Junit
  • 독립적인 테스트를 만들기 위해 어떻게 하셨나요? - 주로 독립적인 테스트를 만들지 못하는 경우는 앞선 테스트에서 DB에 정보를 저장해버린 경우 이에 영향을 받는 것이였음 - 상호 독립적인 테스트를 만들기 위해 총 3가지의 방법을 사용했습니다. - @DirtiesContext - Spring Bean들을 메서드 혹은 클래스 별로 재생성하여 사용하게 됩니다. - 이 과정에서 H2 DB가 초기화 되기 때문에 독립적인 테스트가 가능합니다. - dataSource와 jdbcTemplate의 초기화
    Use this annotation if a test has modified the context — 
    for example, by modifying the state of a singleton bean, 
    modifying the state of an "embedded database", etc. 
    Subsequent tests that request the same context will be supplied a new context.
    
      - `@Transactional`
          - 테스트에 해당 어노테이션을 붙이면 DB에 저장했다가 rollback하게 됨
      - `@Sql("~~.sql")`
          - 테스트-resources에 수행할 SQL 쿼리들을 등록해둠
          - "classpath:~~.sql"을 통해 테스트 수행 전 쿼리문을 실행함
    
  • 본인이 생각했을 때 유지보수하기 좋은 테스트는 무엇인가요? - 유지 보수하기 좋은 테스트는 서로 독립적인 테스트 - 알아보기 쉬운 테스트 - 프로덕션 추가시 고쳐야할 부분이 프로덕션에서 고친 부분만...?

Spring Validation

  • Spring Validation이 뭐죠? - Java의 유효성 검사 표준 기술입니다.
  • Spring Validation이 언제 필요했죠? - RequestBody를 통해 RequestDto를 Jackson으로 매핑하는 경우가 많습니다. - Dto를 생성하는 과정에서 검증이 필요한 경우가 있었습니다. - 가령 꼭 들어와야하는 필드가 공란으로 들어오는 경우 같이요 - 이런 경우에 필요하다
  • 어떤 검증을 어느 Layer에서 어느정도로 해야할까요? - 어떤 어플리케이션을 개발하느냐에 따라 다른 것 같다. - 확장성이 중요한 어플리케이션 같은 경우, 도메인을 탄탄하게 작성하여 프레임워크에 의존적이지 않게 구현해야 한다. - 따라서 상당수의 Validation이 도메인에 들어가는 것이 좋아보인다. - 하지만 도메인의 확장성이 크게 필요하지 않은 경우, 단계별 validation이 좋아보인다. - RequestDto에 BeanValidation을 추가하여 정말 말도 안되는 값들을 쳐낸다 - 최대한 앞단에서 쳐내기 때문에 비용적인 측면에서 유리하다 - 이후 여기서 진행된 Validation은 믿고 뒤에 domain에서 추가적으로 Validation 해준다

Spring Web MVC flow

  • 클라이언트로 부터 요청이 들어오면 어떻게 처리되어 응답하는지 아는대로 알려주세요 1. 클라이언트는 HTTP 요청을 서버에 보냄 2. 웹 서버(아파치, 엔진엑스)는 HTTP 요청을 받아 WAS로 보냄 3. WAS에서 돌고 있는 Tomcat이 HTTP 요청을 받음 4. WAS 안에서 Dispatcher Servlet이 모든 요청을 받음 5. Handler Mapping을 통해 해당 요청이 어떤 Controller를 위한 것인지 핸들러 정보 획득 6. Handler Adapter를 통해 해당 핸들러의 실행 메서드를 호출해줌 - 핸들러 어댑터가 있어야 다양한 리턴값에 따른 요구사항을 맞출 수 있음 7. Handler Interceptor를 통해 Controller에 들어오는 HttpRequest를 가로채 필요한 처리 수행 8. Argument Resolver를 통해 Controller에 들어오는 HttpRequest를 가로채 Controller에 필요한 매개변수를 생성해서 넣어줌 9. Controller - Service - Repository - DB 순으로 로직 처리 10. Controller는 View Name 혹은 Java 객체를 반환 11. Handler Interceptor를 통해 필요한 Post Handle 수행 12. View Resolver를 통해 String 이라면 필요한 HTML 파일 처리를, Java 객체라면 MappingJackson2JsonView를 통한 JSON 처리를 진행 13. 응답을 반환
  • ArgumentResolver와 Interceptor를 각각 어떤 역할로 사용하나요? - 저는 다음과 같이 ArgumentResolver와 Interceptor를 사용하였습니다. - Interceptor: 토큰에 대한 validation을 진행 - ArgumentResolver: 토큰의 정보를 추출하여 Controller에 필요한 매개변수를 생성해 전달
  • Interceptor와 ArgumentResolver를 어떻게 구현하나요? - 우선 HandlerInterceptor를 구현한 사용자Interceptor를 생성 - 이후 preHandle 메서드를 오버라이드 하여 필요한 인가 처리 - HandlerMethodArgumentResolver를 구현한 사용자PrincipalArgumentResolver를 생성 - 이후 SupportsParameter, resolveArgument 를 오버라이딩하여 어떠한 어노테이션이 붙은 매개변수를 채워줄지 선정 - WebMvcConfigurer를 구현한 UserPrincipalConfig를 생성하여 - 만들어준 Interceptor와 ArgumentResolver를 등록시켜줌
    public class AuthInterceptor implements HandlerInterceptor {
        private final AuthService authService;
    
        public AuthInterceptor(AuthService authService) {
            this.authService = authService;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            if ("OPTIONS".equals(request.getMethod()) || "GET".equals(request.getMethod())) {
                return true;
            }
    
            final String token = AuthorizationExtractor.extract(request);
            if (Objects.isNull(token) || !authService.validateToken(token)) {
                throw new AuthorizationException();
            }
            return true;
        }
    }
    
    public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
        private final AuthService authService;
    
        public AuthenticationPrincipalArgumentResolver(AuthService authService) {
            this.authService = authService;
        }
    
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
        }
    
        @Override
        public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                    NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
            String credentials = AuthorizationExtractor.extract(webRequest.getNativeRequest(HttpServletRequest.class));
            return authService.findMemberByToken(credentials);
        }
    }
    
    @Configuration
    public class AuthenticationPrincipalConfig implements WebMvcConfigurer {
        private final AuthService authService;
    
        public AuthenticationPrincipalConfig(AuthService authService) {
            this.authService = authService;
        }
    
        @Override
        public void addArgumentResolvers(List argumentResolvers) {
            argumentResolvers.add(createAuthenticationPrincipalArgumentResolver());
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new AuthInterceptor(authService))
                    .addPathPatterns("/**")
                    .excludePathPatterns("/login/token")
                    .excludePathPatterns("/members")
                    .excludePathPatterns("/members/email-check");
        }
    
        @Bean
        public AuthenticationPrincipalArgumentResolver createAuthenticationPrincipalArgumentResolver() {
            return new AuthenticationPrincipalArgumentResolver(authService);
        }
    }
    
  • Controller에서도 할 수 있는 로직을 굳이 ArgumentResolver에서 해야하나요? - 물론 가능하지만 토큰을 까고 세션을 까서 필요한 정보를 추출하는 과정 자체가 중복 로직 - 아규먼트 리졸버는 해당 과정 중복을 거둬낼 수 있음 - 따라서 ArgumentResolver로 처리가 가능한 부분들은 하는 게 좋다고 봄
  • 토큰, 세션등의 검증을 하기위해 Service 객체를 ArgumentResolver가 들고있는거 어떻게 생각하나요? - 저는 괜찮다고 생각합니다. - 토큰과 세션등의 검증은 비즈니스 로직이라고 할 수 있습니다. - 따라서 서비스 클래스에 결집시켜 놓는것이 유지보수에 유리하다고 생각합니다.

다양한 Jdbc 구현체의 활용

  • 어떤 JDBC 구현체를 사용해봤나요? - DB 접근을 위해 JdbcTemplate, SimpleJdbcInsert, NamedParameterJdbcTemplate을 사용해봤습니다.
  • JdbcTemplate은 어떻게 생성되나요? - JdbcTemplate은 DataSource를 필요로 합니다. - DataSource는 properties, yml 파일에 적인 설정대로 빈이 생성되게 됩니다. - JdbcTemplate도 빈이기에, 필요한 DataSource를 주입받아 생성됩니다.
  • NamedParamJdbcTemplate은 뭔가요?
    public NamedParameterJdbcTemplate(DataSource dataSource) {
        Assert.notNull(dataSource, "DataSource must not be null");
        this.classicJdbcTemplate = new JdbcTemplate(dataSource);
    }
    

    - 다음과 같이 생성 - JdbcTemplate으로는 ?를 사용하여 사용자가 넣을 데이터를 처리 - 순서가 강제되고, 가독성을 떨어뜨리고, 휴먼 에러 발생 가능성이 올라감 - 이를 방지시켜줌 - NamedParamJdbcTemplate을 통해서, - 들어가야할 변수에 ":변수" 로 표현이 가능 - 해당 변수들을 ParameterSource Map으로 처리 가능 - 객체 자체를 매핑시켜 저장할 수 있다고 하는데, 미션에서 적용해 본적은 없다

  • SimpleJdbcInsert는 뭔가요?
    public StationDao(NamedParameterJdbcTemplate namedParameterJdbcTemplate, DataSource dataSource) {
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
        this.insertAction = new SimpleJdbcInsert(dataSource)
                .withTableName("STATION")
                .usingGeneratedKeyColumns("id");
    }
    //(insert into {TABLE_NAME} value ~~) -> id가 keyColumn
    

    - 데이터를 간단하게 저장하기 위해 만들어진 구현체 - BeanPropertySqlParameterSource를 통해 객체에서 필드를 뽑아 파라미터로 지정하여 저장할 수 있음 - executeAndReturnKey를 통해 칼럼 id 가져오기도 가능 - SQL 굳이 안써줘도 됨

    public Station insert(Station station) {
        SqlParameterSource params = new BeanPropertySqlParameterSource(station);
        Long id = insertAction.executeAndReturnKey(params).longValue();
        return new Station(id, station.getName());
    }
    

  • 셋 중에 뭘 언제 쓸건가요? - 강점을 살릴 수 있도록 혼용한다. - 우선 NamedParameterJdbcTemplate 같은 경우, JdbcTemplate보다 추가적인 장점들을 제공했다 - 명시적으로 SQL 변수들을 지정할 수 있음 - 따라서 NamedParameter을 쓰는게 유지보수에 좋아보인다. - SimpleJdbcInsert 같은 경우 id 값을 한번에 가져올 수 있다. - 이를 통해 넣고, 조회하는 DB 2번찌르는 불상사나, - keyHolder를 통해 이렇게 돌아돌아 가는 경우 안해도 됨
        public Station save(String stationName) {
            String sql = "INSERT INTO STATION (name) values (?)";
            KeyHolder keyHolder = new GeneratedKeyHolder();
    
            jdbcTemplate.update(con -> {
                PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
                ps.setString(1, stationName);
                return ps;
            }, keyHolder);
    
            final long stationId = Objects.requireNonNull(keyHolder.getKey()).longValue();
            return new Station(stationId, stationName);
        }
    

    - NamedParameterJdbcTemplate과 SimpleJdbcInsert 혼용할 생각

Annotation

  • 어노테이션이 뭔가요? - 어노테이션이란 메타데이터로써 프로그램의 일부는 아니면서 프로그램에 대한 데이터를 제공 - 작성한 코드에 대해 추가적인 정보를 제공하며, - 런타임에 해당 코드에 필요한 추가적인 처리가 가능함
  • 스프링에서 어노테이션 참 많이 쓰는데 왜 쓰는거에요? - 저는 어노테이션을 씀으로써 "비즈니스 로직에 영향을 주지 않은 채"로 필요한 처리를 진행할 수 있다고 생각합니다. - 추가적인 시스템 설정등을 어노테이션만 붙임으로써 역할을 부여하고, 클래스 안에는 탄탄하게 비즈니스 로직 작성에만 개발자가 집중할 수 있습니다.