프로젝트 리팩터링 - SOLID원칙

꾸준하게 달리기~·2023년 7월 17일
0
post-thumbnail

들어가기 앞서 문제점

보통 우리가 Spring Security + Oauth2를 사용해서 로그인을 하면,
세가지를 생각할 수 있다.

  • 외부 API에서 OAuth2 방식을 통해 정보 얻어오기
  • 해당 정보를 통해 나의 콘텐츠로 가공해 저장하기
  • 성공 핸들러에서 직접 저장하는것이 아니라 서비스로직 호출하여 저장 실행하기

그런데, 프로젝트를 하며
두번째 단계, 즉 나의 객체로 만들어(캐스팅) 저장하기 부분에서 며칠동안 고전했다.
해당 문제를 팀원들과 이야기하고 찾아보고 했지만, 프로젝트 완성 시간은 다가오고 해결되지는 않았고, 마음은 급해져서 두번째 단계와 세번째 단계를 합쳐서 실행하도록 로직을 만들어버렸다.
즉, SOLID 원칙의 S를 어기고 말았다

SOLID 원칙이란? : https://www.nextree.co.kr/p6960/

그렇게 틀려먹은 코드를 보면 아래와 같다.
가장 큰 문제점은 OAuth2UserSuccessHandler, 즉 성공 핸들러 클래스에서 저장 로직을 수행하는 것이다.

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2UserSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils customAuthorityUtils;
    private final MemberRepository memberRepository;
    private final MailService mailService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        log.info("OAuth2 Login 성공!");
        OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
        String email = String.valueOf(oauthUser.getAttributes().get("email"));


        //CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
        //oAuth2User.setEmail(email);
        //oAuth2User.setRoles(customAuthorityUtils.createRoles(email));

        String accessToken = delegateAccessToken(oauthUser);
        //String refreshToken = delegateRefreshToken(oAuth2User);

        String redirectURI = "http://ourecostory.s3-website.ap-northeast-2.amazonaws.com/";
        Optional<Member> optionalMember = memberRepository.findByEmail(email);

=================================문제의 코드 부분==============================
        if(optionalMember.isPresent()) {
            Member member = optionalMember.get();
            log.info("## 리다이렉트 -> {}", redirectURI);
            log.info("## 토큰: {}", accessToken);
            response.setHeader("Authentication", "Bearer_" + accessToken);
            response.setHeader("memberId", String.valueOf(member.getMemberId()));
            response.setHeader("role", String.valueOf(member.getRoles()));

            getRedirectStrategy().sendRedirect(request, response, createURI(accessToken, member.getMemberId(), member.getRoles()).toString());
        }
        else {
            log.info("##해당 멤버 저장 시작");
            Member member1 = new Member(email, email.substring(0, email.indexOf("@")));
            List<String> roles = customAuthorityUtils.createRoles(email);
            member1.setRoles(roles);
            memberRepository.save(member1);
            log.info("##해당 멤버 저장 완료");
            log.info("## 리다이렉트 -> {}", redirectURI);
            log.info("## 토큰: {}", accessToken);

            mailService.sendEmail(email, "반가워요!", "정말 반갑습니다!");
            log.info("메일 전송 완료!");
            getRedirectStrategy().sendRedirect(request, response, createURI(accessToken, member1.getMemberId(), member1.getRoles()).toString());
        }
        
=================================문제의 코드 부분==============================



    }

    private URI createURI(String accessToken, long memberId, List<String> roles) {
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("access_token", accessToken);
        queryParams.add("memberId", String.valueOf(memberId));
        queryParams.add("Role", String.valueOf(roles));

        return UriComponentsBuilder.newInstance()
                .scheme("http")
                //.scheme("https")
                .host("ourecostory.s3-website.ap-northeast-2.amazonaws.com/")
                .queryParams(queryParams).build().toUri();
    }

    private String delegateAccessToken(OAuth2User oAuth2User) {
        String email = String.valueOf(oAuth2User.getAttributes().get("email"));
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", email);
        claims.put("roles", customAuthorityUtils.createRoles(email));

        String subject = email;
        Date expiration = jwtTokenizer.getTokenExpiration(
                jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration,
                base64EncodedSecretKey);

        return accessToken;
    }





프로젝트를 마치고

뭔가 마음속에 캥겼다.
음... 이런식으로 하면 임시방편으로 실행되는것이고,
SOLID원칙의 S, 하나의 클래스는 하나의 책임만 져야 한다 원칙에도 어긋나기 때문에 이건 꼭 고쳐봐야겠다! 라고 생각했다.

해당 문제가 생긴 이유

문제는 아래의 코드에서 발생했다.

CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

에러 코드는 다음과 같았다.

class org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser cannot be cast to
class com.main.server.auth.googleoauth.CustomOAuth2User

즉,
내가 외부 API에서 받아오는 DefaultOAuth2User를 상속받아 내 입맛대로 고쳐서 사용하기 위해
직접 만든 CustomOAuth2User 클래스가 있다.

@Getter
@Setter
public class CustomOAuth2User extends DefaultOAuth2User {

    private String email;
    private List<String> roles;


    public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
                            Map<String, Object> attributes, String nameAttributeKey,
                            String email, List<String> roles) {
        super(authorities, attributes, nameAttributeKey);
        this.email = email;
        this.roles = roles;
    }
}

오어스 로그인 성공해서 외부에서 받아온 principal(authentication.getPrincipal();)
도무지 DefaultOAuth2User 클래스로 보지 않고
DefaultOidcUser 클래스로 보기 때문에
(CustomOAuth2User) authentication.getPrincipal(); 에서
캐스팅 할 수 없다는 에러가 나온다는 내용이었다.

즉,
DefaultOAuth2User, DefaultOidcUser
둘 클래스 관계를 살펴보았다.
구글링을 통해 알아보니,
서로 다른 클래스이며, 직접적인 상속 관계나 상하위 클래스의 관계는 없다.
따라서 두 클래스 중 어느 것이 상위 클래스인지라는 개념은 적용되지 않는다.

상하관계는 아니더라도, 그런데 분명히 연관이 있으니 두개를 착각한다고 판단했다.


찾아보던 와중, 스코프에 openId를 포함하면, DefaultOidcUser 클래스로 캐스팅할 수 있다. 라는 글을 읽고,
openId 객체를 포함하면 DefaultOidcUser가 되는구나! 라고 판단했다.

(https://stackoverflow.com/questions/68105980/defaultoidcuser-cannot-be-cast-to-class-customoauth2user)

음.. 그럼 가지고있는 필드변수들이 달라서
해당 내용을 캐스팅하지 못하는구나! 라는 생각이 들었다.

문득 이전에 외부 API를 받아오며 아무것도 설정해주지 않은 scope가 떠올랐다.

그럼 스코프를 지정해주지 않아서,
내가 원하지 않던 openId 까지 가져오는구나.
그럼 이제 해당 openId를 가져오지 않게 지정해야겠다!
라는 생각을 하고,
야믈파일에 해당 내용을 추가해주었다.

이 이후로는 더이상 DefaultOidcUser 클래스로 인식하지 않았고,
DefaultOAuth2User으로 인식할 수 있었다.




이후 코드 수정

이전엔 해당 오류때문에 받아온 api를 내 db에 저장하는 일을 해주는 service 클래스를 SecurityConfig 클래스에서 사용하지 않았다.

즉, SecurityConfiguration 클래스의 filterchain 매서드의 엔드포인트에
service 클래스를 달지 않고,
oop 원칙에 어긋나게 성공 핸들러에서 db에 저장까지 실행했지만,

이제 당당하게 비즈니스로직이 포함된 service클래스를
filterchain의 endpoint에 추가해줄 수 있었다.

 .oauth2Login().loginPage("/oauth2/authorization/google")
 .successHandler(new OAuth2UserSuccessHandler(jwtTokenizer, customAuthorityUtils, memberRepository, mailService))
 .failureHandler(new OAuth2UserFailureHandler());
 //.userInfoEndpoint().userService(customOAuth2UserService);

위의 코드를(변경 전) 보면 리팩터링 이전엔 SecurityConfig 클래스에서
외부 API를 통해 저장을 성공하면,
성공 핸들러에서 저장하는 로직을 사용했다. (위의 이슈때문에), 캡슐화를 높게 설정했어야 했지만 어긋난것에서,
아래와 같이 service 로직에서 잘 저장할 수 있도록 하였다.

또한 위의 OAuth2UserSuccessHandler 생성자를 보면 알 수 있는데, 
이전에는 저장을 위해 memberRepository를 DI했었다.


------------------------변경 후-------------------------

 .oauth2Login().loginPage("/oauth2/authorization/google")
 .successHandler(new OAuth2UserSuccessHandler(jwtTokenizer, customAuthorityUtils))
 .failureHandler(new OAuth2UserFailureHandler())
 .userInfoEndpoint().userService(customOAuth2UserService);




코드는
https://github.com/ingeon2/webService_project-refactoring/tree/devBE_%EC%9D%B4%EC%9D%B8%EA%B1%B4/server/src/main/java/com/main/server/global/auth

profile
반갑습니다~! 좋은하루 보내세요 :)

0개의 댓글