728x90


일단 차이점을 찾아보니

  • Stream.toList() return -> 변경할수 없는 List 이고 추가,삭제 불가
  • Collectors.toList() return -> 일반적인 List 이고 추가,삭제 가능
  • Collectors.toUnmodifiableList() return -> 변경할수 없는 List 이고 추가,삭제 불가
  • 성능관련해서 테스트 해보았다.
@BenchmarkMode(Mode.All)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 20, time = 1, batchSize = 10000)
@Measurement(iterations = 20, time = 1, batchSize = 10000)
public class BenchmarkStreamToList {

    @Benchmark
    public List<Integer> streamToList() {
        return IntStream.range(1, 1000).boxed().toList();
    }

    @Benchmark
    public List<Integer> collectorsToList() {
        return IntStream.range(1, 1000).boxed().collect(Collectors.toList());
    }

    @Benchmark
    public List<Integer> streamToList() {
        return IntStream.range(1, 1000).boxed().toList();
    }

}

결과:

Benchmark                                                                                  Mode  Cnt   Score    Error  Units
BenchmarkStreamToList.collectorsToList                                                    thrpt   20  24.422 ±  0.268  ops/s
BenchmarkStreamToList.collectorsToUnmodifiableList                                        thrpt   20  22.784 ±  0.599  ops/s
BenchmarkStreamToList.streamToList                                                        thrpt   20  31.779 ±  1.732  ops/s
BenchmarkStreamToList.collectorsToList                                                     avgt   20   0.045 ±  0.006   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                         avgt   20   0.062 ±  0.035   s/op
BenchmarkStreamToList.streamToList                                                         avgt   20   0.040 ±  0.028   s/op
BenchmarkStreamToList.collectorsToList                                                   sample  445   0.046 ±  0.002   s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.00                            sample        0.039            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.50                            sample        0.041            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.90                            sample        0.057            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.95                            sample        0.073            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.99                            sample        0.102            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.999                           sample        0.150            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.9999                          sample        0.150            s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p1.00                            sample        0.150            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                       sample  460   0.044 ±  0.001   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.00    sample        0.042            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.50    sample        0.044            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.90    sample        0.046            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.95    sample        0.047            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.99    sample        0.051            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.999   sample        0.057            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.9999  sample        0.057            s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p1.00    sample        0.057            s/op
BenchmarkStreamToList.streamToList                                                       sample  655   0.031 ±  0.001   s/op
BenchmarkStreamToList.streamToList:streamToList·p0.00                                    sample        0.030            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.50                                    sample        0.031            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.90                                    sample        0.032            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.95                                    sample        0.033            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.99                                    sample        0.035            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.999                                   sample        0.037            s/op
BenchmarkStreamToList.streamToList:streamToList·p0.9999                                  sample        0.037            s/op
BenchmarkStreamToList.streamToList:streamToList·p1.00                                    sample        0.037            s/op
BenchmarkStreamToList.collectorsToList                                                       ss   20   0.043 ±  0.001   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                           ss   20   0.045 ±  0.004   s/op
BenchmarkStreamToList.streamToList                                                           ss   20   0.031 ±  0.001   s/op

결론: Stream.toList()
성능은 모든 방면에서
Collectors.toList() Collectors.toUnmodifiableList()를 앞지른다!

  • 다음은 조금 더 큰 데이터를 세팅해보자!
@Benchmark
public List<Integer> streamToList() {
  return IntStream.range(1, 10000).boxed().toList();
}

@Benchmark
public List<Integer> collectorsToList() {
  return IntStream.range(1, 10000).boxed().collect(Collectors.toList());
}

@Benchmark
public List<Integer> streamToList() {
  return IntStream.range(1, 10000).boxed().toList();
}

결과:

Benchmark                                                                                  Mode  Cnt  Score   Error  Units
BenchmarkStreamToList.collectorsToList                                                    thrpt   20  2.186 ± 0.162  ops/s
BenchmarkStreamToList.collectorsToUnmodifiableList                                        thrpt   20  2.184 ± 0.042  ops/s
BenchmarkStreamToList.streamToList                                                        thrpt   20  3.538 ± 0.058  ops/s
BenchmarkStreamToList.collectorsToList                                                     avgt   20  0.426 ± 0.004   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                         avgt   20  0.469 ± 0.016   s/op
BenchmarkStreamToList.streamToList                                                         avgt   20  0.293 ± 0.008   s/op
BenchmarkStreamToList.collectorsToList                                                   sample   58  0.448 ± 0.049   s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.00                            sample       0.414           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.50                            sample       0.422           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.90                            sample       0.458           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.95                            sample       0.560           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.99                            sample       1.160           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.999                           sample       1.160           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p0.9999                          sample       1.160           s/op
BenchmarkStreamToList.collectorsToList:collectorsToList·p1.00                            sample       1.160           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                       sample   60  0.458 ± 0.004   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.00    sample       0.447           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.50    sample       0.455           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.90    sample       0.471           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.95    sample       0.482           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.99    sample       0.492           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.999   sample       0.492           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p0.9999  sample       0.492           s/op
BenchmarkStreamToList.collectorsToUnmodifiableList:collectorsToUnmodifiableList·p1.00    sample       0.492           s/op
BenchmarkStreamToList.streamToList                                                       sample   78  0.293 ± 0.012   s/op
BenchmarkStreamToList.streamToList:streamToList·p0.00                                    sample       0.277           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.50                                    sample       0.284           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.90                                    sample       0.309           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.95                                    sample       0.377           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.99                                    sample       0.459           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.999                                   sample       0.459           s/op
BenchmarkStreamToList.streamToList:streamToList·p0.9999                                  sample       0.459           s/op
BenchmarkStreamToList.streamToList:streamToList·p1.00                                    sample       0.459           s/op
BenchmarkStreamToList.collectorsToList                                                       ss   20  0.474 ± 0.133   s/op
BenchmarkStreamToList.collectorsToUnmodifiableList                                           ss   20  0.493 ± 0.099   s/op
BenchmarkStreamToList.streamToList                                                           ss   20  0.325 ± 0.056   s/op

결론: 데이터를 크게 셋팅해도 Stream.toList() 성능은 모든 방면에서 Collectors.toList() Collectors.toUnmodifiableList()를 앞지른다!

괜히 테스트 했나 ????

끝!

728x90

SpringBoot version:2.1.6

SpringSecurity version: 5.1.5

MyBatis-Plus version: 3.1.0

JDK version:1.8 (17에서도 잘 돌아감)

 

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yml

# 포트
server:
  port: 8080
spring:
  # datasource 설정
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
# jwt 설정
jwt:
  secret: JWTSecret
  tokenHeader: Authorization
  tokenPrefix: Sans-

  expiration: 86400 # 1day ,604800 1 weekly
  # 인증필요없는 url
  antMatchers: /index/**,/login/**,/favicon.ico
# Mybatis-plus 관련 설정
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    db-config:
      id-type: AUTO
      field-strategy: NOT_EMPTY
      db-type: MYSQL
  configuration:
    map-underscore-to-camel-case: true
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

기본적인 Entity,Dao,Service 와 SpringSecurity user Entity,Service 부분은 여기서 생략한다.

Jwt에 사용된 util 작성

@Slf4j
public class JWTTokenUtil {

    private JWTTokenUtil(){}

    public static String createAccessToken(SelfUserEntity selfUserEntity){
        // jwt 생성 부분
        String token = Jwts.builder()
                .setId(selfUserEntity.getUserId()+"")
                .setSubject(selfUserEntity.getUsername())
                // 발급시간
                .setIssuedAt(new Date())
                // 발급자 임의로 수정가능
                .setIssuer("demo-user")
                // 권한설정
                .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities()))
                // 만료시간
                .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration))
                // 싸인알고리즘, yml에 기재된 시큐릿 key값
                .signWith(SignatureAlgorithm.HS512, JWTConfig.secret)
                .compact();
        return token;
    }
}

권한없을시 처리될 부분을 작성한다.

@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler{

    /**
     * 권한없을시 반환하는 부분
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"권한없음!"));
    }
}

사용자 미등록시 처리되는 부분

@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    /**
     * 로그인하지 않는 사용자일때 반환하는 부분
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"로그인되지않음"));
    }
}

로그인실패시 처리될 부분작성

@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {

    /**
     * 로그인 실패시 리턴
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){

        if (exception instanceof UsernameNotFoundException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"존재하지 않는 사용자!"));
        }
        if (exception instanceof LockedException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"동결된 사용자!"));
        }
        if (exception instanceof BadCredentialsException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"패스워드가 틀림."));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"로그인 실패!"));
    }
}

로그인 성공시 처리될 부분작성

@Slf4j
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;

        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "로그인성공!");
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

로그아웃처리 부분

@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "로그인성공!");
        SecurityContextHolder.clearContext();
        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
    }
}

로그인 인증서 핵심로직부분이다.

@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private SelfUserDetailsService selfUserDetailsService;
    @Autowired
    private SysUserService sysUserService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userName = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        SelfUserEntity userInfo = selfUserDetailsService.loadUserByUsername(userName);
        if (userInfo == null) {
            throw new UsernameNotFoundException("사용자존재하지않음");
        }
        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
            throw new BadCredentialsException("패스워드틀림");
        }
        if (userInfo.getStatus().equals("PROHIBIT")){
            throw new LockedException("사용자가동결됨 ");
        }
        Set<GrantedAuthority> authorities = new HashSet<>();
        List<SysRoleEntity> sysRoleEntityList = sysUserService.selectSysRoleByUserId(userInfo.getUserId());
        for (SysRoleEntity sysRoleEntity: sysRoleEntityList){
            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
        }
        userInfo.setAuthorities(authorities);
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

PermissionEvaluator 컨트롤러에 어노테이션으로 체크하는 인증부분 구현 하는 부분이다.

@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private SysUserService sysUserService;


    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();
        Set<String> permissions = new HashSet<>();
        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());
        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getPermission());
        }
        if (permissions.contains(permission.toString())){
            return true;
        }
        return false;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

SpringSecurity 핵심 비지니스로직 구현 부분이다.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 요거 추가하면 controller 쪽에 어노테이션으로 권한 처리 할수 있음.
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;

    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;

    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;

    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;

    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;

    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;


    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        auth.authenticationProvider(userAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
               .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                .anyRequest().authenticated()
                .and()

                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                .successHandler(userLoginSuccessHandler)
                .failureHandler(userLoginFailureHandler)
                .and()

                .logout()
                .logoutUrl("/login/userLogout")
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                .cors()
                .and()
                .csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.headers().cacheControl();
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

JWT 필터도 만들어봐야지

@Slf4j
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // token 헤더정보 가져오기
        String tokenHeader = request.getHeader(JWTConfig.tokenHeader);
        if (null!=tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
            try {
                String token = tokenHeader.replace(JWTConfig.tokenPrefix, "");
                // jwt 파싱
                Claims claims = Jwts.parser()
                        .setSigningKey(JWTConfig.secret)
                        .parseClaimsJws(token)
                        .getBody();
                // user정보
                String username = claims.getSubject();
                String userId=claims.getId();
                if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) {
                    // role 정보
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    String authority = claims.get("authorities").toString();
                    if(!StringUtils.isEmpty(authority)){
                        List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class);
                        for(Map<String,String> role : authorityMap){
                            if(!StringUtils.isEmpty(role)) {
                                authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                            }
                        }
                    }
                    // 파라미터 셋팅
                    SelfUserEntity selfUserEntity = new SelfUserEntity();
                    selfUserEntity.setUsername(claims.getSubject());
                    selfUserEntity.setUserId(Long.parseLong(claims.getId()));
                    selfUserEntity.setAuthorities(authorities);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e){
                log.info("Token 유효기간 만료");
            } catch (Exception e) {
                log.info("Token 유효하지 않는 토큰");
            }
        }
        filterChain.doFilter(request, response);
    }
}

테스트 코드도 작성 , (회원가입이 잘되는지... 물론 회원가입 controller는 따론 구현하지 않아서 일단 이런 식으로 강제로 user를 생성해 본다.)

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootSecurityDemoApplicationTests {


    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private SysUserRoleService sysUserRoleService;



    @Test
    public void contextLoads() {
        // 가입
        SysUserEntity sysUserEntity = new SysUserEntity();
        sysUserEntity.setUsername("user");
        sysUserEntity.setPassword(bCryptPasswordEncoder.encode("123456"));
        // 사용자 status
        sysUserEntity.setStatus("NORMAL");
        sysUserService.save(sysUserEntity);
        // role setting 1:ADMIN 2:USER
        SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();
        sysUserRoleEntity.setRoleId(2L);
        sysUserRoleEntity.setUserId(sysUserEntity.getUserId());
        sysUserRoleService.save(sysUserRoleEntity);
    }

}

postman 으로 조금전에 생성한 사용자로 로그인 해보자

짜쟌~~

user 정보 조회도 해보자

admin 으로 접근을 하니 권한이 없다고 나온다. 물론 admin 권한은 모두 볼수 있어야 되는거 아니야 ? 라고 할수 있는데 해당 demo에는 그런거 따로 설정을 안해서 이다.

테스트 코드로 관리자 admin 이라는 사용자 생성 , role : ADMIN 으로 설정하고 다시 로그인 하여 새로은 admin jwt 을 받고 

/admin/info api를 호출하면 권한이 정상으로 인증되서 정보를 정상조회할수 있다.

일단 잘되니깐 나머지 controller도 만들어서 테스트하면 될것다.

 

인터넷에 찾아보면 너무 많은 방대한 가이드들이 있긴 하지만 한번 이대로 따라 해보는것도 좋을것다.

그리고 Spring Security 5.7.0 이전/이후 버전으로 설정이 조금 다르다.

해당글은 Spring Security 5.7.0 이전 버전으로 진행한것이다.

추후 업그레이드된 Spring Security 5.7.0 이후 버전 설정 방법으로 글을 쓰도록 하겠다.

 

아참 혹시나 필요할것 같아서 소스 코드도 깃에 업로드 합니다.

필요한 분은 클론 받으시고 비록 정답은 아니지만 프로젝트 적용에 조금이나 도움되엇으면 합니다.

https://github.com/luxury515/spring-boot-security-demo

끝!

 

참고문서

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

https://github.com/spring-projects/spring-security/issues/10822

728x90

사용배경:

모 회사에서 페이서비스를 만든다고 하자. 서로다른 고객 서로 다른 페이방식을 사용할것이다. 예컨대 , 네이버 페잇, 카카오페이, 유니온페이 등등.이때 일반적으로 if else 혹은 switch 로 고객타입을 체크하여 그에 맞는 페이방식을 사용하도록 코드를 작성할것이다.

그래서 한번 전략패턴으로 if 문 혹은 switch 문없이 처리해보겠다.

Base인터페이스 하나 만든다.

public interface BasePayment {
 
    /**
     * 기본 인터페이스
     * @param order
     * @return
     */
    PayResult pay(Order order);
 
}

Order 정보 클래스를 만든다.

@Data
public class Order {
 
    /**
     * 금액
     */
    private int amount;
 
    /**
     * 페이유형
     */
    private String paymentType;
 
}

Return 타입클래스도 만든다.

@Data
@AllArgsConstructor
public class PayResult {
 
    /**
     * 처리결과
     */
    private String result;
 
}

이제 Base 인터페이스 기준으로 각종? 구현 클래스를 한번 만들어 보자. (카카오 페이, 네이버 페이 등등 )

@Service("NaverPay")
public class NaverPay implements BasePayment {
 
    @Override
    public PayResult pay(Order order) {
        return new PayResult("네이버 페이 결제 성공!");
    }
 
}
@Service("KakaoPay")
public class KakaoPay implements BasePayment {
 
    @Override
    public PayResult pay(Order order) {
        return new PayResult("카카오페이 결제 성공!");
    }
 
}

뭐 유니온 페이도 한번 만들어본자 ^^

@Service("UnionPay")
public class UnionPay implements IPayment {
 
    @Override
    public PayResult pay(Order order) {
        return new PayResult("인련페이 지불성공");
    }
 
}

이제 컨트롤러를 하나 만들어보자

@RestController
public class PayService {
 
    @Autowired
    private ApplicationContext applicationContext;
 
    /**
     * 컨트롤러
     * @param amount
     * @param paymentType
     * @return
     */
    @RequestMapping("/pay")
    public PayResult pay(@RequestParam("amount") int amount,
                    @RequestParam("paymentType") String paymentType) {
        Order order = new Order();
        order.setAmount(amount);
        order.setPaymentType(paymentType);
 
        // paymentType으로 bean을 컨테이너로 부터 가져올수 있지
        IPayment payment = applicationContext.getBean(order.getPaymentType(), BasePayment.class);
 
        // 지불시작
        PayResult payResult = payment.pay(order);
 
        return payResult;
    }
 
}

끝!

728x90

프로젝트 관리하다가 외주업체에 맞겼는데 if..else로 도배를 해놓았던것이다.
음 개발자입문할때 누군가가 그러더라 "개발자는 if문,for문만 알아도 개발을 할수 있다" 고. 지금와서 생각해보니 틀린말은 아니다.
그런데 우리는 사람이다. 사람은 항상 생각하고 불편함을 해결하고 복잡한것보다 쉬운것을 추구한다.
그래서 나도 오늘 좀 생각없이 개발하는 아재 개발자들한테 if..else를 줄일수 있는 방법을 말해보려고 한다.
이미 알고 있는 고수분들은 그냥 잘난척하는 내앞을 지나가 주라!
일단 ...이런 코드를 많이 봤지?

@RestController
@PostMappting("/create")
public CommonResult testController(@RequestBody TestDto testDto ){
	
    if(StringUtils.isEmpty(testDto.getName()){
    	return CommonResult.validateFailed("이름이 비었음!");
    }
    if(testDto.getSort() == null || testDto.getSort() < 0){
    	return CommonResult.validateFailed("0 보다 작을수 없음!");
    }
    int count = testService.create(testDto);
    if(count ==1){
    	return CommonResult.success(testDto);
    }
}

Springboot 에서 Hibernate Validator library를 이용하여 검증을 하면 편하다.
1. library 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. 객체를 만든다.

@Data
public class Student {

    @NotBlank(message = "이름은 필수잆력")
    private String name;
    @NotNull(message = "나이는 필수잆력")
    @Range(min = 1,max =50, message = "나이 범위1-50")
    private Integer age;
    @NotEmpty(message = "성적은 필수잆력")
    private List<Double> scores;
}

3. post 요청 controller 를 만든다.

@RestController
@RequestMapping("/student")
public class StudentController {

    @PostMapping("/add")
    public Rest<?> addStudent(@Valid @RequestBody Student student) {
        return RestBody.okData(student);
    }

요청결과:

POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json

{
    "name": "felord.cn",
    "age": 77,
    "scores": [
        55
    ]
}

4. get 요청 controller 를 만든다.

@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
    return RestBody.okData(student);
}

요청결과:

GET /student/get?name=blake.com&age=12 HTTP/1.1
Host: localhost:8888

5. 커스텀 어노테이션
주위 깊게 보았으면 발견했을듯! 바로 Student 객체에 @NotNull + @Range 어노테이션을 겹쳐서 사용했다.

@NotNull(message = "나이필수 입력")
@Range(min = 1,max =50, message = "나이범위 1-50")
private Integer age;

원인은 @Range 가 공백값에 대한 제약을 할수 없다는것, 값이 있을때만 제약대상이다. 이처럼 어노테이션을 여러개 사용해야 하는경우가 가끔있는데 이럴때 이런 어노테이션들을 하나로 묶는것도 방법이긴 하다.

import org.hibernate.validator.constraints.Range;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;


@Constraint(
        validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
    // message 필수
    String message() default "나이는 필수입력,범위는 1-50 ";

    // 선택
    Class<?>[] groups() default {};

    // 선택
    Class<? extends Payload>[] payload() default {};
}

혹은 상태와 같은 불변의 뭔가를 따로 관리해야 된다고 하면 enum클래스를 만들어서 사용하면 좋다.

public enum Colors {

    RED, YELLOW, BLUE

}

public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
    private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();

    @Override
    public void initialize(Color constraintAnnotation) {
        Colors[] value = constraintAnnotation.value();
        List<String> list = Arrays.stream(value)
                .map(Enum::name)
                .collect(Collectors.toList());
        COLOR_CONSTRAINTS.addAll(list);

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return COLOR_CONSTRAINTS.contains(value);
    }
}

@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
 
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
    // 에러 메시지
    String message() default "규격에 맞는 않음.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    // 유형
    Colors[] value();
}

제약이 잘 걸리는지 함 테스트 해보자!

@Data
public class Param {
    @Color({Colors.BLUE,Colors.YELLOW})
   private String color;
}

요청하기

GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888

결과:
BindException 발생한다!
CAY 는 존재하지 않기때문. 그래서 BLUE, YELLOW, RED를 넣으면 리턴이 정상이다.


트러블슈팅:

@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
    return RestBody.okData(color);
}

// 혹은

@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
    return RestBody.okData(color);
}

// 혹은

@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
    return RestBody.okData(student);
}

이처럼 클래스아닌 method 에 적용하연 제약기능이 작동하지 않는다. 구글링해보면 클래스에 적용해야 되지 메서드에 적용하면 안된다는것을 할수 있다. (직접 검색해 보시길!)
마지막으로
어노테이션을 겹쳐서 사용시 제약이 걸리지 않는 문제 도 있음!

@Data
 
public class Student {

    @NotBlank(message = "이름 필수입력")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "성적 필수입력")
    private List<Double> scores;
    @NotNull(message = "학교 공백일수 없음")
    private School school;
}


@Data
public class School {
    @NotBlank(message = "이름 공백일수 없음")
    private String name;
    @Min(value = 0,message ="나이는 0보다 커야 됨." )
    private Integer age;
}

이런 구조일때 get 요청시 문제 없으니, post 요청시 School 에 대한 제약기능이 작동안된다.
이때 우리는 @Valid 어노테이션을 붙여주면 된다.

@Data
public class Student {
    
    @NotBlank(message = "이름 필수입력")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "성적 필수입력")
    private List<Double> scores;
    @Valid
    @NotNull(message = "학교 공백일수 없음")
    private School school;
}

참고:

// message는 오류 메시지를 정의하는 곳

// null만 허용
@Null(message="")

// null 허용 X,  "", " "는 허용
@NotNull(message="")

 // null, "" 허용 X,  " "는 허용
@NotEmpty(message="")

// null, "", " " 허용 X
@NotBlank  (message="")

// 데이터의 사이즈(최소 길이, 최대 길이) 설정
@Size(min=, max= ,message="")  

// 정규식을 이용해서 검사
@Pattern(regexp = ,message="")  

// value 이하의 값만 허용
@Max(value = ,message="")

// value 이상의 값만 허용
@Min(value = ,message="") 

// 값을 양수만 허용
@Positive(message="")

// 값을 양수와 0만 허용
@PositiveOrZero(message="")  

// 값을 음수만 허용
@Negative(message="")   

// 값을 음수와 0만 허용
@NegativeOrZero(message="")  

// 현재보다 미래의 날짜만 허용
@Future(message="")   

// 현재보다 과거의 날짜만 허용
@Past(message="")   

// True일 때만 허용(null 체크 X)
@AssertTrue(message="")

// False일 때만 허용(null 체크 X)
@AssertFalse(message="")

끝!

+ Recent posts