๐ŸŒฟ Spring

[Spring Security] OAuth ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธํ•˜๊ธฐ

์—ฐ_์šฐ๋ฆฌ 2022. 2. 2. 20:21
๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ

     

    ์ธ๋„ค์ผ

     

    ์ด์ „๊ธ€

    https://lotuus.tistory.com/79

     

    [Spring Security] OAuth ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธํ•˜๊ธฐ

    ๋ชฉ์ฐจ [์ด์ „ ๊ฒŒ์‹œ๊ธ€] ๊ผญ! ๋ด์ฃผ์„ธ์—ฌ [Spring Security] ๋™์ž‘๋ฐฉ๋ฒ• ๋ฐ Form, OAuth ๋กœ๊ทธ์ธํ•˜๊ธฐ (Feat.Thymeleaf ํƒ€์ž„๋ฆฌํ”„) ๋ชฉ์ฐจ Spring Security๋ž€? Spring์„ ์‚ฌ์šฉํ•  ๋•Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ์ธ์ฆ, ๊ถŒํ•œ ๋ถ€์—ฌ ๋“ฑ์˜ ๋ณด..

    lotuus.tistory.com

     

     

    Spring Security๋Š” ๊ฐ ์œ ๋ช…ํ•œ ์‚ฌ์ดํŠธ๋“ค(๊ตฌ๊ธ€, ๊นƒํ—ˆ๋ธŒ, ํŽ˜์ด์Šค๋ถ, okta)์˜ OAuth2๋ฅผ ๋ฏธ๋ฆฌ ์„ค์ •ํ•ด๋‘์—ˆ๋‹ค.

    public enum CommonOAuth2Provider {
    	GOOGLE {
    		@Override
    		public Builder getBuilder(String registrationId) {
    			ClientRegistration.Builder builder = getBuilder(registrationId,
    					ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
    			builder.scope("openid", "profile", "email");
    			builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
    			builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
    			builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
    			builder.issuerUri("https://accounts.google.com");
    			builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
    			builder.userNameAttributeName(IdTokenClaimNames.SUB);
    			builder.clientName("Google");
    			return builder;
    		}
    	},
        ....
    }

     

    ๊ทธ๋ž˜์„œ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์„ ์—ฐ๋™ํ• ๋•Œ application.properties์— ๊ธฐ์žฌํ•˜๋Š” ๋‚ด์šฉ์ด ํฌ๊ฒŒ ์—†์—ˆ๋‹ค (์ด๋ฏธ ์„ค์ •๋˜์–ด์žˆ์—ˆ๊ธฐ๋•Œ๋ฌธ)

    spring.security.oauth2.client.registration.google.client-id = 
    spring.security.oauth2.client.registration.google.client-secret = 
    spring.security.oauth2.client.registration.google.scope = profile, email

     

     

    ํ•˜์ง€๋งŒ ๋„ค์ด๋ฒ„์˜ ๊ฒฝ์šฐ๋Š” ๊ธฐ๋ณธ Provider๊ฐ€ ์•„๋‹ˆ๊ธฐ๋•Œ๋ฌธ์— application.properties์— ๋ถ€๊ฐ€์ ์ธ ๋‚ด์šฉ์„ ๋“ฑ๋กํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค!

    OAuth2 ์„ค์ • ํŒŒํŠธ์—์„œ ํ™•์ธํ•˜์ž.

     

     

     

     

     

     

    Spring Security - OAuth2 ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธํ•˜๊ธฐ

     

    ๋„ค์ด๋ฒ„ ์„ค์ •

    1. ๋„ค์ด๋ฒ„ ๊ฐœ๋ฐœ์ž ์„ผํ„ฐ ์ ‘์†

     

    NAVER Developers

    ๋„ค์ด๋ฒ„ ์˜คํ”ˆ API๋“ค์„ ํ™œ์šฉํ•ด ๊ฐœ๋ฐœ์ž๋“ค์ด ๋‹ค์–‘ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋„๋ก API ๊ฐ€์ด๋“œ์™€ SDK๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ œ๊ณต์ค‘์ธ ์˜คํ”ˆ API์—๋Š” ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ, ๊ฒ€์ƒ‰, ๋‹จ์ถ•URL, ์บก์ฐจ๋ฅผ ๋น„๋กฏ ๊ธฐ๊ณ„๋ฒˆ์—ญ, ์Œ

    developers.naver.com

     

    2. Application > ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ๋ก

     

    3. ์•ฝ๊ด€๋™์˜, ๊ณ„์ •์ •๋ณด๋“ฑ๋ก ํ›„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋“ฑ๋ก

        > ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฆ„ ์ž‘์„ฑ > ์‚ฌ์šฉ API : ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ ํด๋ฆญ > ํšŒ์›์ด๋ฆ„, ์ด๋ฉ”์ผ์ฃผ์†Œ ํด๋ฆญ > ์„œ๋น„์Šคํ™˜๊ฒฝ PC์›น ํด๋ฆญ 

     

    4. ์„œ๋น„์Šค URL : http://localhost:8080 ์ž…๋ ฅ

       ์ฝœ๋ฐฑ URL : http://localhost:8080/login/oauth2/code/naver ์ž…๋ ฅ ํ›„ ๋“ฑ๋ก

     

    5. ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ์–ตํ•ด๋‘์ž

     

     

     

    OAuth2 ์„ค์ •

    build.gradle : ์ด์ „๊ณผ ๋™์ผํ•จ

     

    application.properties

    # naver
    spring.security.oauth2.client.registration.naver.client-id = 
    spring.security.oauth2.client.registration.naver.client-secret = 
    spring.security.oauth2.client.registration.naver.scope = name, email
    spring.security.oauth2.client.registration.naver.client-name = Naver
    spring.security.oauth2.client.registration.naver.authorization-grant-type = authorization_code
    spring.security.oauth2.client.registration.naver.redirect-uri = http://localhost:8080/login/oauth2/code/naver
    
    spring.security.oauth2.client.provider.naver.authorization-uri = https://nid.naver.com/oauth2.0/authorize
    spring.security.oauth2.client.provider.naver.token-uri = https://nid.naver.com/oauth2.0/token
    spring.security.oauth2.client.provider.naver.user-info-uri = https://openapi.naver.com/v1/nid/me
    spring.security.oauth2.client.provider.naver.user-name-attribute = response
    registration.naver.client-name ์ž๋™ ์ƒ์„ฑ๋˜๋Š” ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์—์„œ ๋…ธ์ถœํ•˜๋Š” ๋“ฑ์— ์‚ฌ์šฉํ•œ๋‹ค(๊ผญ ์žˆ์–ด์•ผํ•˜๋Š”์ง€๋Š” ๋ชจ๋ฅด๊ฒ ๋‹ค..)
    registration.naver.authorization-grant-type OAuth2๋Š” 4๊ฐ€์ง€ Authorization Grant์œ ํ˜•์ด ์žˆ๋‹ค
    ๋„ค์ด๋ฒ„๋Š” ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” authorization_code ๋ฐฉ์‹์„ ์ด์šฉํ•œ๋‹ค (์ฐธ๊ณ )
    registration.naver.redirect-uri ๋„ค์ด๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž ํ™•์ธ ํ›„ ์ •๋ณด๋ฅผ ๋‚ด ํ”„๋กœ์ ํŠธ๋กœ ๋ณด๋‚ด์ฃผ๋Š” ์ฃผ์†Œ
    ๊ตฌ๊ธ€์ฒ˜๋Ÿผ ํŒจํ„ด์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. "{baseUrl}/login/oauth2/code/{registrationId}"
    provider.naver.authorization-uri ์ธ์ฆ์„ ์š”์ฒญํ•˜๋Š” url์„ ์ž‘์„ฑํ•œ๋‹ค (#3.4.2)
    provider.naver.token-uri ํ† ๊ทผ์„ ์š”์ฒญํ•˜๋Š” url์„ ์ž‘์„ฑํ•œ๋‹ค (#3.4.4)
    provider.naver.user-info-uri ํšŒ์› ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” url์„ ์ž‘์„ฑํ•œ๋‹ค (#3.4.5)
    provider.naver.user-name-attribute { "resultCode":~, "message":~, "response": { "email":~, "name":~, ... } }
    ๋„ค์ด๋ฒ„๋Š” ์‚ฌ์šฉ์ž์ •๋ณด๋ฅผ ์ด๋Ÿฐ์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š”๋ฐ,
    spring security์—์„œ๋Š” ํ•˜์œ„ ํ•„๋“œ๋ฅผ ๋ช…์‹œํ•  ์ˆ˜ ์—†๊ณ  ์ตœ์ƒ์œ„ ํ•„๋“œ๋งŒ user_name์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    ๋„ค์ด๋ฒ„์˜ ์ตœ์ƒ์œ„ํ•„๋“œ๋Š” resultCode, message, response์ด๊ธฐ๋•Œ๋ฌธ์— 3๊ฐœ ์ค‘์— ๊ณจ๋ผ์•ผํ•˜๋Š”๋ฐ, ํ•˜์œ„ํ•„๋“œ๊ฐ€ ์กด์žฌํ•˜๋Š” response๋ฅผ user_name์œผ๋กœ ์ง€์ •ํ•œ๋‹ค.
    (#3.4.5)

     

     

     

    OAuth2UserInfo ์ธํ„ฐํŽ˜์ด์Šค์™€ GoogleUserInfo ํด๋ž˜์Šค, NaverUserInfo ํด๋ž˜์Šค

    ๊ธฐ์กด ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์—์„œ๋Š” providerId๊ฐ’์„ "sub"๋กœ ๋ฐ›์•˜์ง€๋งŒ ๋„ค์ด๋ฒ„์˜ ๊ฒฝ์šฐ๋Š” "id"๋กœ ๋ฐ›์•„์•ผํ•œ๋‹ค.

    ๋‹ค๋ฅธ OAuth2 ๋กœ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜๊ฒŒ๋œ๋‹ค๋ฉด ๊ฒฝ์šฐ์— ๋”ฐ๋ผ์„œ providerId๊ฐ’์ด ์•„๋‹Œ email, name ๊ฐ’์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•ด์•ผํ• ์ˆ˜๋„์žˆ๋‹ค. ์ถ”ํ›„ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์œ„ํ•ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ž!

    //๊ตฌ๊ธ€์˜ ๊ฒฝ์šฐ
    String providerId = oAuth2User.getAttribute("sub");
    
    //๋„ค์ด๋ฒ„์˜ ๊ฒฝ์šฐ
    String providerId = oAuth2User.getAttributes().get("response").get("id").toString();
    { "resultCode":~, "message":~, "response": { "id":~, "email":~, "name":~, ... } }

     

    ํŒจํ‚ค์ง€ > auth ํด๋” > userinfo ํด๋” > OAuth2UserInfo.java

    public interface OAuth2UserInfo {
        Map<String, Object> getAttributes();
        String getProviderId();
        String getProvider();
        String getEmail();
        String getName();
    }

     

    ํŒจํ‚ค์ง€ > auth ํด๋” > userinfo ํด๋” > GoogleUserInfo.java

    public class GoogleUserInfo implements OAuth2UserInfo{
        private Map<String, Object> attributes; 
    
        public GoogleUserInfo(Map<String, Object> attributes) {
            this.attributes = attributes;
        }
        
        @Override
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
        @Override
        public String getProviderId() {
            return attributes.get("sub").toString();
        }
    
        @Override
        public String getProvider() {
            return "google";
        }
    
        @Override
        public String getEmail() {
            return attributes.get("email").toString();
        }
    
        @Override
        public String getName() {
            return attributes.get("name").toString();
        }
    }

     

    ํŒจํ‚ค์ง€ > auth ํด๋” > userinfo ํด๋” > NaverUserInfo.java

    public class NaverUserInfo implements OAuth2UserInfo{
        private Map<String, Object> attributes; //OAuth2User.getAttributes();
        private Map<String, Object> attributesResponse;
    
        public NaverUserInfo(Map<String, Object> attributes) {
            this.attributes = (Map<String, Object>) attributes.get("response");
            this.attributesResponse = (Map<String, Object>) attributes.get("response");
        }
        
        @Override
        public Map<String, Object> getAttributes() {
            return attributes;
        }
    
        @Override
        public String getProviderId() {
            return attributesResponse.get("id").toString();
        }
    
        @Override
        public String getProvider() {
            return "naver";
        }
    
        @Override
        public String getEmail() {
            return attributesResponse.get("email").toString();
        }
    
        @Override
        public String getName() {
            return attributesResponse.get("name").toString();
        }
    }

     

     

     

    OAuth2Login loadUserByUsername

    ํŒจํ‚ค์ง€ > auth ํด๋” > PrincipalOauth2UserService.java

    package com.example.demo.oauth;
    
    @Service
    public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    
        @Autowired private UserRepository userRepository;
        @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    
            OAuth2User oAuth2User = super.loadUser(userRequest);
            
            OAuth2UserInfo oAuth2UserInfo = null;	//์ถ”๊ฐ€
            String provider = userRequest.getClientRegistration().getRegistrationId();    
            
            //์ถ”๊ฐ€
            if(provider.equals("google")){
                oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
            }
            else if(provider.equals("naver")){
                oAuth2UserInfo = new NaverUserInfo(oAuth2User.getAttributes());
            }
            
            String providerId = oAuth2UserInfo.getProviderId();	//์ˆ˜์ •
            String username = provider+"_"+providerId;  			
    
            String uuid = UUID.randomUUID().toString().substring(0, 6);
            String password = bCryptPasswordEncoder.encode("ํŒจ์Šค์›Œ๋“œ"+uuid); 
    
            String email = oAuth2UserInfo.getEmail();	//์ˆ˜์ •
            Role role = Role.ROLE_USER;
    
            User byUsername = userRepository.findByUsername(username);
            
            //DB์— ์—†๋Š” ์‚ฌ์šฉ์ž๋ผ๋ฉด ํšŒ์›๊ฐ€์ž…์ฒ˜๋ฆฌ
            if(byUsername == null){
                byUsername = User.oauth2Register()
                        .username(username).password(password).email(email).role(role)
                        .provider(provider).providerId(providerId)
                        .build();
                userRepository.save(byUsername);
            }
    
            return new PrincipalDetails(byUsername, oAuth2UserInfo);	//์ˆ˜์ •
        }
    }

     

     

     

    OAuth2Login Authentication OAuth2User

    ํŒจํ‚ค์ง€ > auth ํด๋” > PrincipalDetails.java

    package com.example.demo.auth;
    
    @Getter 
    @ToString
    public class PrincipalDetails implements UserDetails, OAuth2User {
    
        private User user;
        //private Map<String, Object> attributes;
        private OAuth2UserInfo oAuth2UserInfo;
    
        //UserDetails : Form ๋กœ๊ทธ์ธ ์‹œ ์‚ฌ์šฉ
        public PrincipalDetails(User user) {
            this.user = user;
        }
    
        //OAuth2User : OAuth2 ๋กœ๊ทธ์ธ ์‹œ ์‚ฌ์šฉ
        //public PrincipalDetails(User user, Map<String, Object> attributes) {
        //    //PrincipalOauth2UserService ์ฐธ๊ณ 
        //    this.user = user;
        //    this.attributes = attributes;
        //}
        
        public PrincipalDetails(User user, OAuth2UserInfo oAuth2UserInfo) {
            this.user = user;
            this.oAuth2UserInfo = oAuth2UserInfo;
        }
        
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ํ•ด๋‹น ์œ ์ €์˜ ๊ถŒํ•œ๋ชฉ๋ก ๋ฆฌํ„ด
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
    
            Collection<GrantedAuthority> collect = new ArrayList<>();
            collect.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return user.getRole().toString();
                }
            });
            return collect;
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฆฌํ„ด
         */
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * PK๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค
         */
        @Override
        public String getUsername() {
            return user.getUsername();
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ๊ณ„์ • ๋งŒ๋ฃŒ ์—ฌ๋ถ€
         *  true : ๋งŒ๋ฃŒ์•ˆ๋จ
         *  false : ๋งŒ๋ฃŒ๋จ
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ๊ณ„์ • ์ž ๊น€ ์—ฌ๋ถ€
         *  true : ์ž ๊ธฐ์ง€ ์•Š์Œ
         *  false : ์ž ๊น€
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ๊ณ„์ • ๋น„๋ฐ€๋ฒˆํ˜ธ ๋งŒ๋ฃŒ ์—ฌ๋ถ€
         *  true : ๋งŒ๋ฃŒ ์•ˆ๋จ
         *  false : ๋งŒ๋ฃŒ๋จ
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * UserDetails ๊ตฌํ˜„
         * ๊ณ„์ • ํ™œ์„ฑํ™” ์—ฌ๋ถ€
         *  true : ํ™œ์„ฑํ™”๋จ
         *  false : ํ™œ์„ฑํ™” ์•ˆ๋จ
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    
        /**
         * OAuth2User ๊ตฌํ˜„
         * @return
         */
        @Override
        public Map<String, Object> getAttributes() {
            //return attributes;
            return oAuth2UserInfo.getAttributes();
        }
    
        /**
         * OAuth2User ๊ตฌํ˜„
         * @return
         */
        @Override
        public String getName() {
            //String sub = attributes.get("sub").toString();
            //return sub;
            return oAuth2UserInfo.getProviderId();
        }
    }

     

     

     

    OAuth2Login Domain๊ตฌํ˜„

    ํŒจํ‚ค์ง€ > domain ํด๋” > Role.java : ์ด์ „๊ณผ ๊ฐ™์Œ 

    ํŒจํ‚ค์ง€ > domain ํด๋” > User.java : ์ด์ „๊ณผ ๊ฐ™์Œ

     

     

     

     

    OAuth2Login Controller ๊ตฌํ˜„

    ํŒจํ‚ค์ง€ > controller ํด๋” > UserController.java : ์ด์ „๊ณผ ๊ฐ™์Œ

     

     

     

    OAuth2Login View๊ตฌํ˜„

    resources > templates > join.html : ์ด์ „๊ณผ ๊ฐ™์Œ 

    resources > templates > login.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</title>
    </head>
    <body>
    <h1>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</h1>
    <hr/>
    
    <h2>๋กœ๊ทธ์ธ ์œ ์ € : </h2>
    <p sec:authentication="principal"></p>
    
    <div sec:authorize="isAnonymous()" style="background-color:pink; padding:1em;">
        <form action="/login" method="post" >
            <input type="text" name="username" />
            <input type="password" name="password" />
            <button>๋กœ๊ทธ์ธ</button>
        </form>
        
        <a href="/oauth2/authorization/google">๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ</a>
        <a href="/oauth2/authorization/naver">๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ</a>
        <!-- /oauth2/authorization/{registrationId}์— ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด, 
        ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ provider์˜ authorization-uri๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•œ๋‹ค-->
        <br><br>
    
        <a href="/joinForm">ํšŒ์›๊ฐ€์ž…ํ•˜๊ธฐ</a><br>
    </div>
    
    <div sec:authorize="isAuthenticated()" style="background-color:pink; padding:1em;">
        <a href="/logout">๋กœ๊ทธ์•„์›ƒ</a>
    </div>
    
    <br><br>
    <a href="/user">์œ ์ €</a><br>
    <a href="/manager" sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')">๋งค๋‹ˆ์ €</a><br>
    <a href="/admin" sec:authorize="hasRole('ROLE_ADMIN')">์–ด๋“œ๋ฏผ</a><br>
    
    <br><br>
    <a href="/form/loginInfo">Form ๋กœ๊ทธ์ธ ์ •๋ณด</a>
    <a href="/oauth/loginInfo">OAuth2 ๋กœ๊ทธ์ธ ์ •๋ณด</a>
    <a href="/loginInfo">๋กœ๊ทธ์ธ ์ •๋ณด</a>
    
    </body>
    </html>

     

     

     

     

     

     

     

     

    ๋ฐ˜์‘ํ˜•
    • ๋„ค์ด๋ฒ„ ๋ธ”๋Ÿฌ๊ทธ ๊ณต์œ ํ•˜๊ธฐ
    • ํŽ˜์ด์Šค๋ถ ๊ณต์œ ํ•˜๊ธฐ
    • ํŠธ์œ„ํ„ฐ ๊ณต์œ ํ•˜๊ธฐ
    • ๊ตฌ๊ธ€ ํ”Œ๋Ÿฌ์Šค ๊ณต์œ ํ•˜๊ธฐ
    • ์นด์นด์˜คํ†ก ๊ณต์œ ํ•˜๊ธฐ