ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 비밀입니다
    카테고리 없음 2024. 2. 13. 00:18

     

    동작 흐름

     

    - OAuth2 login 

    //OAuth login 
    
    OAuth2LoginAuthenticationFilter -> attemptAuthentication(request,response) 메서드 동작 
    
    ->  AuthenticationManager에 authenticate메서드 호출 
    ->  OAuth2LoginAuthenticationProvider의  authenticate메서드가 다시 호출됨    
    -> 로그인 시도를 통해 발급받은 외부인증서버에 대한 access 토큰 정보, 혹은 유저 정보를 
    OAuth2UserRequest에 담아서 UserService.loadUser()메서드 호출 
    -> 여기서 UserService는 Custom으로 만든 UserService를 의미함! 
    -> 원래 기능은 외부에서 주어진 값 (OAuth2UserRequest)으로 UserService내부에서 해당 유저가
       가입된 유저인지 확인하는 작업이 필요 하지만, SNS+JWT이므로 내부에서 
       토큰을 만들거나, 첫 sns로그인 시 필요한 정보들만 담아서 UserDetails로 반환 
    
    -> 다시 Provider -> Manager -> LoginFilter로 돌아와서 해당 정보를 임시 세션에 저장
    -> 로그인이 완료 되었으니, 다음 처리를 위해 
       success handler 작동시킴(토큰 발급 혹은 회원가입 페이지로 이동시키기)
       
       
       -> 이 흐름에서 우리가 작성해야할 것은 UserService, successHandler뿐!

     

    - JWT 인증 및 인가 

    //OAuth로 회원 등록 혹은 로그인이 완료되었다면, 다음 요청부터는 토큰이 필요함
    // 모든 요청에 대해 토큰을 발급할 클래스와 모든 요청에 대해 토큰을 검증할 필터 필요 
    
    //위 흐름은 로그인 흐름이 아니라, 로그인 완료 후 세션 없이 로그인 유지 흐름임
    
    //원래 form로그인 이라면, UserNameAndPasswordFilter가 처음에 Form으로 유저 정보를 받음
    (OAuth2의 OAuth2LoginAuthenticationFiler와 비슷한 위치라고 보면 됨)
    근데 시큐리티에서 폼 로그인을 끄면, 위 필터가 등록에서 제외됨 따라서 JWT를 검증할 다른 필터를 만들어서 사용
    
    OAuth2 마지막 로그인에서 
    success Handler가 작동하면서
    sns로 회원가입한 유저면, token을 발급하고, 아니면, 회원가입 페이지로 이동시키면 됨
    이때 Redirect시에 sns로그인을 시도한 email과 provider 정보등을 함께 넘김! (필요에 따라 더 넘길 수 있음)
    
    -> 토큰이 발급 된 후 
      -> 유저가 요청을 보냄(토큰을 http request 헤더에 담음) 
      -> jwtTokenAuthFilter 작동 -> 토큰을 검증 
                                    -> 만료된 토큰 -> refresh토큰이 만료 이전
                                                   -> access토큰 재발급 시키고, 요청 통과 
                                                   -> refresh토큰 만료시 
                                                   -> 다시 재로그인 시킴
                                    
                                    -> 조작된 토큰 -> 에러페이지 혹은 메인으로 보내버리기
       
       -> 토큰 검증이 완료되었으면, 임시세션에 로그인한 유저 정보를 담아서 요청을 통과시킴
       -> 이후 컨트롤러 단에서 로직을 처리하면서, 임시세션에 담긴 유저정보를 꺼내서 처리

     

    @EnableWebSecurity
    @Configuration
    @RequiredArgsConstructor
    public class HorokSecurityConfig {
    
    	@EnableWebSecurity
    @Configuration
    @RequiredArgsConstructor
    public class HorokSecurityConfig {
    	
        //테스트 용도
        private final TestHandler testHandler;
        //Service
        private final HorokOAuthUserService horokOAuthUserService;
       
    
       // private final TokenService tokenService;
        private final JwtExceptionFilter jwtExceptionFilter;
        private final JwtTokenAuthFilter jwtTokenAuthFilter;
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    
            //csrf 일단 disable (api서버는 꺼도 된다함)
            http.csrf((auth) -> auth.disable())
                    //form로그인은 안하기때문에 끔 
                    .formLogin((auth) -> auth.disable())
                            .httpBasic((auth)->auth.disable())
                                    //세션은 따로 생성x 시큐리티 임시세션을 이용할 것 따라서 stateless로 바꾸기
                                    .sessionManagement((s)->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    
            http.authorizeHttpRequests((auth) ->
                    //몇가지 경로는 시큐리티 필터 작동없이 통과하도록 바꿈
                    auth.requestMatchers("/","/oauth2/**","/login/**").permitAll()
                            .anyRequest().authenticated()
            ).      
                    //jwt인증필터는 usernamePasswordAuthenticationFiler바로 뒤에 작동하도록 둠
                    addFilterBefore(jwtTokenAuthFilter, UsernamePasswordAuthenticationFilter.class)
                    //토큰 검증 중에 발생하는 Exception을 다루기 위해 등록한 필터
                    .addFilterBefore(jwtExceptionFilter, JwtTokenAuthFilter.class).
                    // oauth2 login관련 
                    oauth2Login((oauth2)->
                    //성공시 동작할 successHandler 등록
                    oauth2.successHandler( testHandler)
                             //custom UserService 등록
                            .userInfoEndpoint((us)->us.userService(horokOAuthUserService))
                            .failureHandler(horokAuthenticationFailureHandler));
    
    	
    
          return  http.build();
    
        }
    
    
    }

     

    **프론트랑 같이 동작하기 위해 추후에 cors 작업을 추가해야함 !! 인도형 참고해서 만들자 

     

    1.  OAuth2 로그인 관련

     

    1.1 OAuth2 UserService

    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class HorokOAuthUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
        private final UserLoginInfoRepository userLoginInfoRepository;
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    
            //기본 OAuth2USerService 생성
            OAuth2UserService<OAuth2UserRequest,OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
           //OAuth2UserSerivce를 사용하여 OAuth2User 정보 가져옴 //userRequest가 외부 인증서버에서 발급받은 토큰개념임
            OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); // oauth2에서 받은 access 토큰에서 유저정보 꺼낸 것
    
            //Naver,kakao같은 정보
            String registrationId = userRequest.getClientRegistration().getRegistrationId();
            //사용자명
            String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
    
            //기본 userService에서 가져온 유저 정보를 기반으로 OAuth2Attribute 객체를 만든다.
            Oauth2Attribute oauth2Attribute = Oauth2Attribute.of(registrationId,userNameAttributeName,oAuth2User.getAttributes());
    
            //파싱한 값들 중 필요 정보를 담은 Map임
            Map<String, Object> memberAttribute = oauth2Attribute.convertToMap();
    
            String email = (String) memberAttribute.get("email");
    
            //등록된 이메일을 기준으로 우리 어플리케이션에 저장된 회원인지 아닌지 판단
            Optional<UserLoginInfo> findloginInfo = userLoginInfoRepository.findByUserLoginEmail(email);
    
            if (findloginInfo.isEmpty()) {
                // 회원이 존재하지 않을경우, memberAttribute의 exist 값을 false로 넣어준다.
                memberAttribute.put("exist", false);
                // 회원의 권한(회원이 존재하지 않으므로 기본권한인 ROLE_USER를 넣어준다), 회원속성, 속성이름을 이용해 DefaultOAuth2User 객체를 생성해 반환한다.
    
                return new DefaultOAuth2User(
                        Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                        memberAttribute, "email");
            }
    
            // 회원이 존재할경우, memberAttribute의 exist 값을 true로 넣어준다.
            memberAttribute.put("exist", true);
            // 회원의 권한과, 회원속성, 속성이름을 이용해 DefaultOAuth2User 객체를 생성해 반환한다.
            return new DefaultOAuth2User(
                    Collections.singleton(new SimpleGrantedAuthority("ROLE_".concat(findloginInfo.get().getUserLoginRole()))),
                    memberAttribute, "email");
        }
    }

     

     //기본 userService에서 가져온 유저 정보를 기반으로 OAuth2Attribute 객체를 만든다.
            Oauth2Attribute oauth2Attribute = Oauth2Attribute.of(registrationId,userNameAttributeName,oAuth2User.getAttributes());

    -> 이를 만드는 이유는 sns마다 넘어오는 정보를 파싱하기 위한 로직이 다르기 때문에 이를 처리할 클래스를 임의로 만든 것 

      String email = (String) memberAttribute.get("email");
       //등록된 이메일을 기준으로 우리 어플리케이션에 저장된 회원인지 아닌지 판단
    Optional<UserLoginInfo> findloginInfo = userLoginInfoRepository.findByUserLoginEmail(email);

    -> 이메일 혹은 나름대로의 값을 통해 Jwt토큰을 발급, 검증 혹은 등록된 유저인지 아닌지 판단할 것이다.

     

     

    1.2 Oauth2Attribute (DAO)

    @ToString
    @Builder(access = AccessLevel.PRIVATE)
    @Getter
    public class Oauth2Attribute {
        private Map<String,Object> attributes; // 사용자 속성 정보를 담는 Map
        private String attributeKey; // 사용자 속성의 키 값
        private String email;
        private String name;
        private String provider;
    
        public static Oauth2Attribute of(String provider,String attributeKey, Map<String,Object> attributes){
            switch (provider){
                case "google":
                    return ofGoogle(provider, attributeKey, attributes);
                case "kakao":
                    return ofKakao(provider,"id", attributes);
                case "naver":
                    return ofNaver(provider, "email", attributes);
                default:
                    throw new RuntimeException();
            }
        }
    
        private static Oauth2Attribute ofGoogle(String provider, String attributeKey,
                                                Map<String, Object> attributes) {
            return Oauth2Attribute.builder()
                    .email((String) attributes.get("email"))
                    .provider(provider)
                    .attributes(attributes)
                    .attributeKey(attributeKey)
                    .build();
        }
    
        private static Oauth2Attribute ofKakao(String provider, String attributeKey,
                                               Map<String, Object> attributes) {
            Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
            Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");
    
            return Oauth2Attribute.builder()
                    .email((String) kakaoAccount.get("email"))
                    .provider(provider)
                    .attributes(kakaoAccount)
                    .attributeKey(attributeKey)
                    .build();
        }
    
        private static Oauth2Attribute ofNaver(String provider, String attributeKey,
                                               Map<String, Object> attributes) {
            Map<String, Object> response = (Map<String, Object>) attributes.get("response");
    
            return Oauth2Attribute.builder()
                    .email((String) response.get("email"))
                    .name((String) response.get("name"))
                    .attributes(response)
                    .provider(provider)
                    .attributeKey(attributeKey)
                    .build();
        }
    
    
        // OAuth2User 객체에 넣어주기 위해서 Map으로 값들을 반환해준다.
        public Map<String, Object> convertToMap() {
            Map<String, Object> map = new HashMap<>();
            map.put("id", attributeKey);
            map.put("key", attributeKey);
            map.put("email", email);
            map.put("provider", provider);
            map.put("name",name);
    
            return map;
        }
    }

     

    -> 나머지는 sns에 따라 파싱하는 것 뿐이고, 파싱된 값을 Map의 형태로 꺼내기 위한 메서드는 아래와 같다.

      // OAuth2User 객체에 넣어주기 위해서 Map으로 값들을 반환해준다.
        public Map<String, Object> convertToMap() {
            Map<String, Object> map = new HashMap<>();
            map.put("id", attributeKey);
            map.put("key", attributeKey);
            map.put("email", email);
            map.put("provider", provider);
            map.put("name",name);
    
            return map;
        }

     

    -> 해당 값은 jwt토큰을 발급하거나, sns로 회원가입이 안된 유저를 회원가입하도록 유도하는데 사용된다.

    -> 따라서 필요에 따라 커스텀하자 

     

    1.3 successHandler 

    @Component
    @Slf4j
    @RequiredArgsConstructor
    public class TestHandler extends SimpleUrlAuthenticationSuccessHandler {
        private final JwtUtil jwtUtil;
        private final UsersRepository usersRepository;
        private final UserLoginInfoRepository userLoginInfoRepository;
        private static final String REDIRECT_URL = "http://localhost:8081/loginSuccess";
        @Override
        @Transactional
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            //자신한테 보내면
            //getRedirectStrategy().sendRedirect(request,response,REDIRECT_URL);\
            //내 컴퓨터로는 절대 못보내고, 다른 컴퓨터로는 보낼 수 있음
            //response.sendRedirect("/loginSuccess");
    
            //따라서 redirect 보내지말고, 그냥 결과 화면만 보고 저장된 걸로 접근해보아야 겠다.
    
    
            //공통로직
            // OAuth2User로 캐스팅하여 인증된 사용자 정보를 가져온다. (로그인시 Oauth2Service를 통해 검증받은 부분)
            OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
    
            log.info("attribute {}",oAuth2User.getAttributes());
            // 사용자 이메일을 가져온다.
            String email = oAuth2User.getAttribute("email");
            // 서비스 제공 플랫폼(GOOGLE, KAKAO, NAVER)이 어디인지 가져온다.
            String provider = oAuth2User.getAttribute("provider");
    
            // CustomOAuth2UserService에서 셋팅한 로그인한 회원 존재 여부를 가져온다.
            boolean isExist = oAuth2User.getAttribute("exist");
            // OAuth2User로 부터 Role을 얻어온다.
            String role = oAuth2User.getAuthorities().stream().
                    findFirst() // 첫번째 Role을 찾아온다.
                    .orElseThrow(IllegalAccessError::new) // 존재하지 않을 시 예외를 던진다.
                    .getAuthority(); // Role을 가져온다.
    		
            //회원가입 완료 유저인지, 새로 회원가입 시켜야하는 유저인지 판단 
            if(isExist){
                // 회원이 존재하면 jwt token 발행을 시작한다.
                GeneratedToken token = jwtUtil.generatedToken(email,role);
                log.info("jwtToken = {}", token.getAccessToken());
    
                //원래는 헤더에 담아서 보내줘야하는데, 그냥 보여주자
                response.setContentType("text/html;charset=utf-8");
                response.setCharacterEncoding("utf-8");
                PrintWriter writer = response.getWriter();
                writer.write(token.toString());
            }else{
               //유저정보 생성해서 저장
                UserLoginInfo userLoginInfo = UserLoginInfo
                        .builder().userLoginEmail(email)
                                .userLoginRole(role)
                                                .build();
                Users users = Users.builder().userNickname("강태바리").userLoginInfo(userLoginInfo)
                        .build();
    
                userLoginInfo.setUser(users);
    
                usersRepository.save(users);
    
                response.setContentType("text/html;charset=utf-8");
                response.setCharacterEncoding("utf-8");
                PrintWriter writer = response.getWriter();
                writer.write("user created");
            }
        }
    }

     

    - UserService에서 return한 OAuthUser를 임시 세션에서 꺼낸다.

    - 토큰을 발급하거나, 회원가입 시키기 위해 필요한 정보를 꺼내서 넣는다. 

    - 여기는 완전히 커스텀 영역이다! 

     

    *주의할점 테스트를 위해 로컬 백엔드 서버로 리다이렉트 시키면 무한 루프를 돈다.

     -> 이유는 백엔드 서버는 oauth2에서 미리 설정한 redirect-uri로 가야하는데 중간에 redirect시킨것과 충돌하는 듯 하다 

     

    1.4 jwt util 

    @Service
    @RequiredArgsConstructor
    @Slf4j
    public class JwtUtil {
    
        private SecretKey secretKey;
        @Value("${spring.jwt.secret}")
        private String sign;
        @PostConstruct
        protected void init(){
           // secretkey = Base64.getEncoder().encodeToString(sign.getBytes());
    
             secretKey = new SecretKeySpec(sign.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
        }
    
        public GeneratedToken generatedToken(String email, String role){
            // refreshToken And accessToken 쌍으로 생성
            String refreshToken = generteRefreshToken(email,role);
            String accessToken = generateAccessToken(email, role);
    
            //token을 Mysql에 저장 리프레쉬 토큰 때문임
    
            return new GeneratedToken(accessToken,refreshToken);
    
        }
    
        public String generteRefreshToken(String email, String role){
            //토큰의 유효 기간을 설정
            long refreshExpiration = 1000L * 60L * 60L * 24L * 7; // 1주
    
            return Jwts.builder()
                    .claim("email",email)
                    .claim("role",role)
                    .issuedAt(new Date(System.currentTimeMillis()))
                    .expiration(new Date(System.currentTimeMillis()+refreshExpiration))
                    .signWith(secretKey)
                    .compact();
        }
    
        public String generateAccessToken(String email,String role){
            long tokenPeriod = 1000L * 60L * 30L; //30분
    
            return Jwts.builder()
                    .claim("email",email)
                    .claim("role",role)
                    .issuedAt(new Date(System.currentTimeMillis()))
                    .expiration(new Date(System.currentTimeMillis()+tokenPeriod))
                    .signWith(secretKey)
                    .compact();
        }
    
    
        public boolean isExpired(String token) throws Exception{
                // 검증시 발생할 수 있는 예외는 필터에서 잡을 것 따라서 여기선
                // 단순히 토큰이 유효한지만 검사
               // 예외 함 봐보자
                    boolean before = Jwts.parser().verifyWith(secretKey)
                            .build()
                            .parseSignedClaims(token)
                            .getPayload()
                            .getExpiration()
                            .before(new Date());
    
                    return before;
    
        }
    
        public String getEmail(String token){
            return Jwts.parser()
                    .verifyWith(secretKey)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload()
                    .get("email",String.class);
        }
    }

     

     

    1.4.1 발급한 토큰을 담아둘 DTO

    @Data
    public class GeneratedToken {
        private String accessToken;
        private String refreshToken;
    
        public GeneratedToken(String accessToken, String refreshToken) {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
        }
    }

     

    - 토큰을 발급하기 이전에 먼저 

    시크릿 값을 넣기 위해 아래와 같이 properties파일이나, 혹은 다른 어딘가에 값을 저장해두자

    spring.jwt.secret=주먹으로 내려친 무슨무슨 코드

     

    jwt라이브러리는 아래 버전을 사용했다.

    //jwt 관련 라이브러리
    	implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    	implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
    	implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

     

    -> jwt를 발급하면서 payoad에 넣을 값과 secret key는 커스텀이다. 

    -> jwt에 payload는 나중에 유저 검증을 위해 사용될 것이다. 

     

     

    2. JWT 로그인 인증 관련 

     

    2.1 로그인 인증필터 

    @RequiredArgsConstructor
    @Slf4j
    @Component
    public class JwtTokenAuthFilter extends OncePerRequestFilter {
    
        private final JwtUtil jwtUtil;
    
        private final UsersRepository usersRepository;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
           //request Header에서 Access Token을 가져온다.
    
            String acctoken = request.getHeader("Authorization");
            log.info(acctoken);
            //모두 허용 URL일 경우 토큰 검사 생략
            if(!StringUtils.hasText(acctoken)){
                doFilter(request,response,filterChain);
                return;
            }
            //AccessToken을 검증, 만료되었을 경우 예외를 발생시킨다.(리프레쉬 토큰 확인해서 재발급)
            try {
    
                jwtUtil.isExpired(acctoken);
    
            }catch (ExpiredJwtException es){
                //토큰이 expired 된 것 처리
                throw new AccessTokenExpiredException("Access Token 만료");
            }
            catch (Exception e) {
                //토큰 조작 처리
    
                //나머지 Eecption
                log.info(e.getMessage());
                throw new RuntimeException(e);
            }
            log.info("여기까지 작동");
            //accessToken의 값이 유효함
    
    
                //email 혹은 특정 값으로 멤버 조회
                //Users byUserNickname = usersRepository.findByUserNickname("예민경영")
                AuthUserDto authUserDto=  AuthUserDto.builder()
                        .userId(1).email("mimi5963").nickname("윤꿀꿀").build();
    
                //SecurityContext에 인증 객체를 등록 (1회용) -> 세션이 Stateless라그럼
                Authentication auth = new UsernamePasswordAuthenticationToken(authUserDto,"", List.of(new SimpleGrantedAuthority("ROLE_USER")));
                SecurityContextHolder.getContext().setAuthentication(auth);
                log.info("여기까지 작동 맘우리");
                //authUserDto가 principal로 셋팅된다.
                //authUserDto를 얻기 위해서는
                // SecurityContextHolder.getContext().~~->..getPrincipal()이다
                // 걍 어노테이션으로 처리하자
    
            filterChain.doFilter(request, response);
        }
    }

     

     

    - jwt에 값을 파싱해서 DB에 유저를 조회하자 이후에 임시 세션에 담을 유저정보를 셋팅한다.

            //email 혹은 특정 값으로 멤버 조회
                //Users byUserNickname = usersRepository.findByUserNickname("예민경영")
                AuthUserDto authUserDto=  AuthUserDto.builder()
                .userId(1).email("mimi5963").nickname("윤꿀꿀").build();

     

    - 커스텀 영역이다.

    - DB에서 조회한 유저정보를 DTO로 바꾼 값을 사용해서 Authentication객체로 만들어서 시큐리티 임시세션에 저장한다. 

      Authentication auth = new UsernamePasswordAuthenticationToken(authUserDto,"", List.of(new SimpleGrantedAuthority("ROLE_USER")));
     SecurityContextHolder.getContext().setAuthentication(auth);

     

    * 세션을 stateless로 설정해두었기 때문에, 하나의 요청에 임시세션이 생성된다.

      따라서 모든 요청을 검증함과 동시에 해당 임시세션에 유저정보를 채워줘야한다. 

     

    2.2 토큰 Exception 처리 필터

     

    @Component
    @RequiredArgsConstructor
    public class JwtExceptionFilter extends OncePerRequestFilter {
    
       private final ObjectMapper objectMapper;
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            try {
                filterChain.doFilter(request,response);
            }catch (AccessTokenExpiredException e){
                response.setStatus(401);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding("UTF-8");
                objectMapper.writeValue(response.getWriter(), StatusResponseDto.builder().status(401).build());
            }catch (Exception e){
                StatusResponseDto build = StatusResponseDto.builder().status(401).msg("Access 토큰이 조작 혹은 해당 애플리케이션에서 발행한 토큰이 아닙니다.").build();
                response.setStatus(401);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding("UTF-8");
                objectMapper.writeValue(response.getWriter(),build);
            }
        }
    }

     

    - 토큰 검증 과정에서 발생한 에러를 처리하기 위한 필터이다. 

     

    -> 추후에 access토큰이 만료되었을 때 다시 발급받기 위한 로직을 위해 적어둔 Exception이다ㅓ.

    public class AccessTokenExpiredException extends RuntimeException{
       public AccessTokenExpiredException(String msg){
            super(msg);
        }
    }

     

    - Exception 상황에서 클라이언트에 응답을 담당할 객체이다.

    - Object Mapper클래스를 통해 아래 Dto에 담긴 결과를 json형식으로 뿌려주기 위해 작성했다.

    @Getter
    @Builder
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class StatusResponseDto {
        private Integer status;
        private Object data;
    
        private String msg;
    
    
        public static StatusResponseDto success(){
            return StatusResponseDto.builder().status(200).build();
        }
    
    
    }
Designed by Tistory.