2020년 8월 4일 화요일

간단하게 OAuth 2.0 사용해보기

간단하게 OAuth 2.0 사용해보기

  • OAuth란?

    • 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준

    • ID와 비밀번호 대신 엑세스 토큰기반으로 사용자를 식별할 수 있다. 토큰은 리소스 서버만 제공한다.

  • 기본 구조

    • Resource Owner : ID와 비밀번호를 이용해 Resource Client에게 권한을 인가하여 엑세스 토큰을 획득하게 될 주체

    • Resource Client : ResourceOwner로부터 사용 인가를 받아, 소유자 대신 엑세스 토큰을 획득하고 해당 토큰 을 통해 Resource Server의 API를 사용하는 주체

    • Resource Server : 보호된 리소스를 관리하며 리소스 클라이언트가 사용할 API를 제공하는 주체. 엑세스 토큰이 유효한지 확인하기 위해 Authorization Server와 통신을 주고 받기도함

    • Authorization Server : 엑세스 토큰과 인가 코드를 관리하는 서버. 엑세스 토큰 검증, 폐기

  • 기본 동작 구조

    • ex) 리소스 오너 : 사용자 / 리소스 클라이언트 : 쇼핑몰 웹(앱) / 리소스 서버, 인가 서버 : 카카오
    • 사용자 -> 쇼핑몰 실행 -> 로그인 -> 카카오 로그인 버튼 클릭
    • 웹 -> 카카오로 인증페이지 요청 -> 카카오 로그인화면 이동
    • 사용자는 카카오 ID,PW 입력(토큰 요청) -> 카카오는 인증페이지를 요청했던 쇼핑몰에 엑세스 토큰 발급
    • 쇼핑몰에선 해당 엑세스 토큰을 가지고 다시 카카오API 호출 -> 정상적인 토큰일 경우 사용자 정보 리턴
  • 내가 적용한 예시

    • 아래의 두 가지 방법 중 하나로 가능하다. 특히 Spring Security가 제공하는 OAuth2ClientAuthenticationProcessingFilter 를 사용하면 토큰받기, 사용자정보 받기 과정을 자동으로 할 수 있다.

    • REST API 직접 호출을 통한 구현

      • 카카오로부터 AccessToken 받기

        public JsonNode getKakaoAccessToken(String code) {
        
        
          final String RequestUrl = "https://kauth.kakao.com/oauth/token";
          final List<NameValuePair> postParams = new ArrayList<NameValuePair>();
        
        
          //포스트 파라미터의 grant_type이라는 명칭에 authorization_code를 추가한다 아래도 동일
          postParams.add(new BasicNameValuePair("grant_type", "authorization_code"));
          postParams.add(new BasicNameValuePair("client_id", kakaoClientId));
          postParams.add(new BasicNameValuePair("redirect_uri", redirectUri));
          postParams.add(new BasicNameValuePair("code", code));
        
        
          final HttpClient client = HttpClientBuilder.create().build();
          final HttpPost post = new HttpPost(RequestUrl);
        
        
          JsonNode returnNode = null;
        
        
          try {
              post.setEntity(new UrlEncodedFormEntity(postParams));
              final HttpResponse response = client.execute(post);
              ObjectMapper mapper = new ObjectMapper();
              returnNode = mapper.readTree(response.getEntity().getContent());
          } catch (UnsupportedEncodingException e) {
              e.printStackTrace();
          } catch (ClientProtocolException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
        
        
          return returnNode;
        }
        
      • 받은 토큰을 통해 사용자 프로필 받기

        public JsonNode getKakaoUserProfile(String access_token) {
        
        
            final String RequestUrl = "https://kapi.kakao.com/v2/user/me";
        
        
            final HttpClient client = HttpClientBuilder.create().build();
            final HttpPost post = new HttpPost(RequestUrl);
        
        
            post.addHeader("Authorization", "Bearer " + access_token);
        
        
            JsonNode returnNode = null;
        
        
            try {
                final HttpResponse response = client.execute(post);
        
        
                ObjectMapper mapper = new ObjectMapper();
                returnNode = mapper.readTree(response.getEntity().getContent());
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        
        
            return returnNode;
        }
        
    • Security 필터 사용

      • SecurityConfig

        @Configuration
        @EnableWebSecurity
        public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        
            private final Filter ssoFilter;
        
        
            public SecurityConfig(Filter ssoFilter) {
                this.ssoFilter = ssoFilter;
            }
        
        
            @Override
            public void configure(WebSecurity web) throws Exception {
                web.ignoring().antMatchers("/resources/**","resources/**");
                web.httpFirewall(new DefaultHttpFirewall());
            }
        
        
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.csrf().disable();
                http.authorizeRequests()
                        .antMatchers("/", "/course/admin/**").hasAuthority("ROLE_ADMIN")
                        .antMatchers("/", "/admin/**").hasAuthority("ROLE_ADMIN")
                        .and()
                            .formLogin()
                            .loginPage("/")
                            .loginProcessingUrl("/loginProcess.ajax")
                            .usernameParameter("id")
                            .passwordParameter("password")
                            .successForwardUrl("/loginCheck.ajax")
                            .permitAll()
                        .and()
                            .logout()
                            .logoutUrl("/logout")
                            .logoutSuccessUrl("/")
                            .deleteCookies("JSESSIONID")
                            .invalidateHttpSession(true)
                        .and()
                            .addFilterBefore(ssoFilter, BasicAuthenticationFilter.class)
                        .httpBasic();
            }
        
        
            @Bean
            public PasswordEncoder passwordEncoder(){
                // 버전차이로 PasswordEncoderFactories.createDelegatingPasswordEncoder(); 는 사용못하는 것 같음
                return new BCryptPasswordEncoder();
            }
        
        
        }
        
        
        
      • OAuthConfig

        @Configuration
        @EnableOAuth2Client
        @PropertySource(value = "classpath:socialInfo.properties")
        public class OAuthConfig {
        
        
            @Autowired
            @Qualifier("oauth2ClientContext")
            OAuth2ClientContext oauth2ClientContext;
        
        
            @Bean
            public Filter ssoFilter() {
                CompositeFilter filter = new CompositeFilter();
                List<Filter> filterList = new ArrayList<>();
        
        
                filterList.add(kakaoSsoFilter());
                filterList.add(googleSsoFilter());
                filter.setFilters(filterList);
        
        
                return filter;
            }
        
        
            public Filter kakaoSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/login/kakao/oauth");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(kakaoClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(kakaoResource().getUserInfoUri(), kakaoClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(kakaoSuccessHandler());
        
        
                return oauth2Filter;
            }
        
        
            public Filter googleSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/google/googleSignInCallback");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(googleResource().getUserInfoUri(), googleClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(successHandler());
        
        
                return oauth2Filter;
            }
        
        
            @Bean
            public AuthenticationSuccessHandler successHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/googleSignIn");
                };
            }
        
        
            @Bean
            public AuthenticationSuccessHandler kakaoSuccessHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/kakaoSignin");
                };
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.client")
            public OAuth2ProtectedResourceDetails googleClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.resource")
            public ResourceServerProperties googleResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.client")
            public OAuth2ProtectedResourceDetails kakaoClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.resource")
            public ResourceServerProperties kakaoResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
                FilterRegistrationBean registration = new FilterRegistrationBean();
                registration.setFilter(filter);
                registration.setOrder(-100);
                return registration;
            }
        }
        

참고 및 출처

2020년 8월 3일 월요일

.gitignore 사용하기

.gitignore 사용하기

  • 민감한 정보나, 업데이트하고 싶지 않은 정보는 gitignore를 통해 관리할 수 있다.

    # class 확장자
    *.class
    
    # build 하위 파일
    build/
    
    # 해당 파일 제외
    !gradle/wrapper/gradle-wrapper.jar
    
  • 내가 해놓은 .gitignore

    target/
    !.mvn/wrapper/maven-wrapper.jar
    
    ### GRADLE ###
    .gradle
    /build/
    !gradle/wrapper/gradle-wrapper.jar
    /out
    
    ### STS ###
    .apt_generated
    .classpath
    .factorypath
    .project
    .settings
    .springBeans
    
    ### IntelliJ IDEA ###
    .idea
    *.iws
    *.iml
    *.ipr
    
    ### NetBeans ###
    nbproject/private/
    build/
    nbbuild/
    dist/
    nbdist/
    .nb-gradle/
    /bin/
    
    ### querydsl
    generated
    
    
    
  • 여기서 .gitignore 예시도 제공해준다.

  • 개발 중간에 .gitignore에 추가하였지만, commit 목록에는 표시될 때 !!

    • cached 옵션을 통해 로컬에는 파일을 남기고 원격저장소에 파일을 지울 수 있다. (이미 파일이 원격저장소에 있는 경우 ignore가 안먹히는 것 같다!!)
    • git rm --cached 제외할파일명

2020년 7월 12일 일요일

젠킨스 자동배포 연습해보기

까먹지말자. 메모!


CI/CD?

  • 애플리케이션 개발 단계를 자동화하여 새로운 코드 통합으로 인한 문제를 해결할 수 있다.
  • 빌드, 테스트, 배포 프로세스에 대해 자동화와 모니터링이 가능하다.

CI란? (Continuous Integration)

  • 지속적 통합
  • 개발한 소스를 특정 시점에 통합(빌드, 테스트 등)하는 것이 아닌, 지속적으로 통합을 진행한다. (자동화 프로세스!)
  • 어플리케이션을 변경할 때 자동으로 빌드 및 테스트 가능
  • 유명한 CI 툴은 Jenkins 가 있다.

CD란? (Continuous Deploy or Delivery)

  • 지속적 배포
  • 변경사항을 저장소에서 테스트서버나 운영서버에 자동으로 반영한다.

Travis VS Jenkins

Travis
  • 장점

    • 전용 서버가 필요하지 않다. (자체 호스팅)
    • github과 연동이 편하다.
  • 단점

    • 제한된 옵션 제공
    • 느린 속도
    • private 유료

Jenkins
  • 장점

    • 많은 플러그인 제공
  • 단점

    • 별도의 서버가 필요함
    • 젠킨스에 대한 관리 필요

  • 간단히 혼자 해보기는 travis가 재미있을 것 같지만, 학습과 실습이 목적이니 Jenkins를 선택한다!

  • 일단 로컬로 도커를 이용해서 연습해보자.

  • 학습목적이니 일단 내장서버를 이용해서 실행해보자

    Once the spring-boot plugin has been applied to your project it will automatically attempt to rewrite archives to make them executable using the bootRepackage task. You should configure your project to build a jar or war (as appropriate) in the usual way.

    // build.gradle 에 다음 내용을 추가해준다.
    // plugin, 생성할 war명 과 실행할 메인 클래스 명을 넣는다.
    apply plugin: 'war'
    
    
    ...
    
    
    war {
        baseName = 'NoN'
        version =  '0.0.1-SNAPSHOT'
        manifest{
            attributes(
                    'Main-Class': 'com.edu.EduApplication'
            )
        }
    }
    


설치과정 메모


  • 도커에 jenkins 설치하기

    • 명령어로 설치할 수 도 있지만, Docker가 제공해주는 관리도구(Kitematic)로 쉽게 설치할 수 있다.


    • 도커 설치 중..


    • 설치 후 다음 명령어로 어떤 포트로 실행되고있는 지 볼 수 있다.


    • 물론 Kitematic에서도 확인 가능


    • 중간에 패스워드 입력화면은 Kitematic 로그 자세히 보면 패스워드가 있다. 그걸 입력하자.

      • 못봤다면... 도커에서 비밀번호에서 확인해보자

        docker exec -it jenkins /bin/bash
        
        
        cat /var/jenkins_home/secrets/initialAdminPassword

    • localhost:32819 로 접속 (잘 모를때는 추천 플러그인을 설치해보자)


    • 플러그인들이 모두 설치실패한다?


    • 젠킨스 버전 문제라고한다. 높은 버전으로 변경하자.

      docker exec -it -u 0 jenkins bash
      
      
      wget http://updates.jenkins-ci.org/download/war/2.235.1/jenkins.war
      
      
      mv ./jenkins.war /usr/share/jenkins/
      
      
      chown jenkins:jenkins /usr/share/jenkins/jenkins.war
      
      
      도커 젠킨스 재시작

    • 새로운 Item 클릭 


    • 소스관리 탭에서 자신의 프로젝트 git 주소와 Credentials - Add 자신의 git 정보를 입력한다.


    • build 유발 - GitHub hook trigger for GITScm polling 체크


    • 빌드 후 배포를 하기위해 다음 작업을 추가한다. Build - Add build step - Execute Shell - 다음 쉘 추가


  • 현재 젠킨스를 도커에 띄웠으므로 접속주소가 localhost 이다. github에서 요청을 보내야한다. 외부에서 로컬에 접속가능하게 하기 위해 ngrok 을 사용한다.

    • 설치 후 다음 명령어로 사용 가능

      ngrok http 32819
      
      
      // 세션만료 없애기 (기본 8시간), AuthToken 은 ngrok 홈페이지에서 로그인 후 받을 수있다.
      ngrok http 32819 --authtoken={AuthToken}
      
    • 생성된 주소를 사용하면 된다!!


  • Github에 이벤트가 있을 때 젠킨스로 요청을 보내야하므로 Webhooks를 설정해준다.

    • Gibhub - Settings - Webhooks - Add Webhook
    • 젠킨스 주소 뒤에 /github-webhook/ 를 붙여준다. (ngrok에서 생성된 주소 사용하면 됨)

  • README.md 파일에 build 상태 보여주기

    • 젠킨스 플러그인에서 Embeddable Build Status Plugin 다운로드
      • Item - Embeddable Build Status - MarkDown 부분 복사 - github README.md 파일에 입력

      • 다음과 같이 README.md 파일에 표시되는 것을 확인할 수 있다! (빌드상태가 계속 반영된다. 다만, ngrok을 사용하고 있기때문에 주소가 자꾸 바뀐다. 별도의 젠킨스 서버가 필요할 듯하다.)

  • 이제 저장소에 푸시가 있을 때 젠킨스가 자동으로 빌드하는 것을 볼 수 있다. 이를 활용해서 서버에 설치하고 배포작업을 상세화, 단계화하여 적용하면 될 것 같다. (실무에선 어떤 프로세스로 사용할까? ㅠ)


참고

2020년 7월 8일 수요일

학교에서 알려주지 않는 17가지 실무기술 책 메모

학교에서 알려주지 않는 17가지 실무기술 책 메모

  • 해커와 화가라는 책을 읽다가 너~무 재미없어서 책을 변경했다. (너무 옛날 이야기같기도 하고, 지루했다.) 
    벌써 6권째 전자책 완독이다! 리디북스 베스트 셀러에 보이길래 목차보고 흥미로워서 구매했다. 
    책 제목처럼 정말 학교에서는 배우지 않은 내용들이지만 업무하면서 어디선가 필요한 내용이었다. 나같은 기초가 부족한 주니어 개발자 혹은 취준생이 읽으면 아주 좋을 것 같다. 

    물론 책에서 해당 내용에 대해 깊게 설명하진 않지만, 해당 기술이 어디서 필요한지, 실무에선 어떻게 사용하는 것이 좋을지, 심화적으로 공부해보면 좋을 것을 소개해주고 있어 가볍게 보기 좋은 것 같다. 
    파이썬을 통해 예제 또한 제공하여 이해를 돕고있다. 
    특히 HTTPS, OAuth 부분은 직접 깊게 찾아보지 않는 이상 잘 정리된 내용을 접하기 힘든데, 쉽고 빠르게 설명해주어서 흥미롭게 읽었다!!

  • 문자열 인코딩

    • UTF - 8

      • 오늘날 가장 많이 사용하는 문자열 인코딩(최소 1바이트, 최대 6바이트, 대부분 4바이트 이내로 처리)
      • 아스키 코드와 호환 가능
      • JSON은 UTF-8 인코딩만 사용한다.
    • MySQL에서 UTF-8과 완벽히 호환되는 문자 집합을 쓰고 싶다면 utf8mb4를 써야한다.

  • 날짜와 시간

    • 서버 개발자라면 ETCD나 주키퍼를 사용해보는 것도 좋다.
  • 정규표현식

    • 특수문자를 찾을 때는 프로그래밍 언어가 한 번, 정규식 표현식이 한 번 문자를 이스케이프해서 해석할 수 있도록 역슬래시 두 개를 사용해야 한다.
    • 정규표현식 검사로 인해 처리속도가 늦어질 수 있다. 성능을 생각한다면 parser 라이브러리를 사용하는 것이 좋다.
  • 범용 고유 식별자

    • UUID는 충돌 가능성이 낮아서 범용적인 고유 식별자로 사용하기 좋다. 하지만 16바이트나 필요하고 UUID만으로는 정보를 식별하기 어렵다.
  • 해시 함수

    • 해시 함수는 임의의 입력값을 고정된 길이의 해시 값으로 변환하는 함수다.

    • 암호학적으로 안전한 해시 함수는 다음의 조건들을 만족해야한다.

      • 해시 값으로 입력 값을 복원하는 방법이 불가능해야 한다.
      • 서로 다른 입력값으로 같거나 비슷한 해시 값을 찾는 방법이 불가능해야 한다.
    • 대규모 사용자를 기반으로 한 서비스를 개발할 때는 SHA-2 해시 함수를 사용하는 대신 멀티 코어를 사용할 수 있는 Blake2b 해시 함수를 사용해야 한다. (SHA-256 또는 SHA-512 해시함수는 비밀번호와 같은 민감한 데이터를 안전하게 저장할 수 있으나, 해시 값을 계산하는 비용이 매우 크다)

    • 거의 모든 경우 클라이언트에서 평문으로 된 비밀번호를 보내고, 서버에서 가능한 빨리 해시 값으로 변환한 후 사용하는 게 이상적이다. 평문으로 된 비밀번호가 유출되는 상황은 높은 수준의 암호화 방식을 적용한 HTTPS를 사용하는 것으로 쉽게 방지할 수 있다.

  • JSON

    • JSON은 숫자, 문자, 참 또는 거짓 등 여러 형태 데이터를 키와 값으로 구조화 된 객체에 담아 처리하는 규격이다.
    • JSON 메시지가 예측할 수 없는 외부 (사용자 입력, HTML 폼요청 등)로부터 온 데이터인 경우에는 앞서 본 예제 코드와 같이 읽기 전에 키가 존재하는지 검사하는 게 좋다.
    • JSON 객체와 배열을 읽을 때는 객체 내부 또는 배열 요소가 정렬됐다는 가정을 하지 않는게 좋다.
    • 실무환경에서 JSON 메시지를 만들 때 null을 사용하지 않는 게 좋다. (그 키가 어떤 데이터를 담고있는지 알수 없다)
    • JSON 규격의 데이터는 읽기 쉽지만, 텍스트 기반이기 때문에 데이터를 표현하는데 비용이 크다는 단점이 있다.
  • YAML

    • JSON과 비슷하지만 YAML만의 고유한 특징으로 차세대 설정 파일 표준 규격으로 영역을 넓히고 있음
    • 주석지원, 앵커, 별칭 기능
    • 앵커는 & 으로 시작하는 식별자, 별칭은 * 로 시작하는 식별자
    • YAML은 텍스트 기반 데이터 규격으로 읽기 쉬우면서 중괄호나 큰 따옴표를 사용하지 않음. 바이너리 규격에 비해 비효적임
  • XML

    • 웹에서 규격화된 데이터를 효율적으로 주고받기 위해 만든 마크업 언어
    • 데이터 직렬화를 위한 규격이 필요하고 반드시 xml을 써야할 이유가 없다면 JSON 또는 바이너리 기반 프로토콜 버퍼를 고려할 것.
    • 애플리케이션 설정 파일을 만들기 위한 규격이 필요하면 YAML을 고려할 것
  • 프로토콜 버퍼

    • 구글에서 만든 데이터 직렬화 규격
    • 바이너리 기반 규격이기 때문에 더 빠르고 효율적으로 데이터 가공, 처리 가능
    • 메시지를 받은 소프트웨어도 데이터를 가공하고 보낸 곳과 같은 메시지 규격을 사용해야만 메시지 복원, 읽기 가능
    • 인터페이스 코드 : 컴파일러가 스키마를 읽어 만들어낸 결과물. 모든 프로그램은 이 코드를 통해서만 데이터 직렬화/역직렬화 가능
    • 프로토콜 버퍼는 메시지만 정의하면 검증, 누락과 관련된 인터페이스를 자동으로 생성하기 때문에 관리의 필요성이 줄어든다. 메시지 크기도 작고 빠름.
  • Base64

    • 바이너리 데이터를 아스키 코드 일부와 일대일로 매칭되는 문자열. 단순 치환하는 인코딩 방식
    • 바이너리 데이터를 문자열 기반 데이터로 취급할 수 있어 많은 곳에서 사용됨.
    • Base64는 XML, JSON, RESTfull API 처럼 문자열을 기반으로 데이터를 주고 받는 환경에서 바이너리 데이터를 취급할 떄 사용.
    • HTTP로 큰 파일을 보내야한다면 HTTP 멀티파트 기능을 살펴보는 게 도움이 된다.
  • zlib

    • zip파일을 압축할 때는 DEFLATE 알고리즘 사용 (INFLATE는 압축 해제)
    • 데이터 크기가 가변적인 환경에서는 모든 데이터를 압축하지 않는 게 좋다.
    • 웹 서비스의 경우 브라우저가 DEFLATE를 지원하지 않을 수 있음. 일반적으로 Nginx와 같은 서버가 알아서 처리하겠지만 웹소켓, HTTP 서버에서 항상 DEFLATE를 사용하지 않게 설정해두는 것이 좋다.
  • HTTP

    • 서버와 클라이언트가 텍스트, 이미지, 동영상 등의 데이터를 주고 받을 때 사용하는 프로토콜
    • 데이터를 안전하게 주고받기 위해 HTTP에 전송계층보안 TLS을 더해 만든 HTTPS를 사용함.
    • HTTP는 요청 메시지를 보내기 직전까지 대상 컴퓨터가 연결 가능한지 메시지를 응답할 수 있는 상태인지 알 수 없다.(stateless 프로토콜)
    • TCP는 HTTP 보다 빠르지만 연결상태를 직접 관리해야해서 로직이 복잡함. (TCP는 실시간 멀티플레이 게임이나, 금융서비스처럼 메시지 처리시간이 로직 처리보다 오래걸리는 경우에만 사용하는 것이 좋음)
    • HTTP요청
      • GET : 웹 브라우저가 서버에 웹 페이지를 요청할 때 사용
      • POST : 클라이언트에서 서버로 데이터가 포함된 요청을 보낼 때 사용
      • DELETE, PUT : DELETE는 데이터 삭제, PUT은 이미 존재하는 데이터의 업데이트 요청을 의미하며 기술적으로는 POST와 큰 차이는 없다.
    • URL과 달리 URI는 특정 문서, 영상 등과 같은 자원의 위치를 가리킬 때 사용하는 용어임. (XML에서도 사용)
    • 로드밸런스 서비스는 사용자가 접속했을 때 부하가 가장 적은 웹 서버로 연결해주고 동작하지 않는 서버를 발견하면 서버 목록에서 자동으로 제외됨.
    • 스티키 세션 : 하나의 브라우저는 하나의 웹 서버에만 연결하게 됨
    • 교차 출처 리소스 공유(CORS) : HTTP 서버의 웹 페이지, 이미지 파일이나 API 등을 특정 호스트로 접속한 웹 브라우저만 사용할 수 있게 제현하는 정책
      • 동일 출처 정책 : 사전에 정의하지 않은 다른 곳에서 웹 페이지, API와 같은 리소스 요청을 차단하는 방어 장치 (다른 웹사이트에서 이미지, 동영상과 같은 리소스를 무단으로 가져가는 상황을 방지할 수 있음 - 다른 도메인간 리소스 공유가 필요한 경우 있기 때문에 CORS가 생김)
    • 웹서버는 요청 처리 외에도 정적 파일 캐시, 로드 밸런스, 압축 및 보안 기능 등 고려해야할 것이 많음
      • HTTP 표준에서 정의하는 기능을 바로 사용할 수있는 웹서버 소프트웨어가 있음
      • Nginx : 아파치 보다 뛰어난 성능과 가벼운 구조로 인기를 끌고 있다.
      • Nginx는 단일 스레드와 이벤트 기반으로 동작하고, 아파치보다 많은 사용자를 처리할 수 있다. (사용 권장)
    • 비동기 통신을 지원하는 표준은 HTTP 2.0과 웹소켓이 있음. HTTP 2.0은 하나의 요청에 대한 응답을 병렬로 보내는 데 초점. 웹소켓은 웹상에서 TCP처럼 데이터를 양방향으로 주고받음
  • RESTful API

    • 서버와 클라이언트가 메시지를 주고 받을 때 가장 많이 사용하는 통신 규격
    • 요청 주소와 메서드(GET,POST 등) JSON 규격을 이용하여 API를 정의하고 읽기 쉬운 형태임
    • 정해진 표준이나 규격이 없기 때문에 커뮤니티나 기업에서 제공하는 관례를 따르자.
    • API를 사용하는 입장에서 호환성을 고려할 수있도록 API 버전을 명시하자.
    • 대상을 어떻게 할 것인지는 메서드로 결정하자. (POST, PUT, PATCH, DELETE 등) (객체의 행동을 요청 주소에 정의하면 메서드 역할이 축소된다. 좋은 디자인이라고 볼 수 없음)
    • GET은 HTTP 표준에 따라 메시지 바디를 사용할 수 없는데, RESTful API에서 요청 주소 경로에 식별자를 넣는 것이 관례이다
    • 조회 시 단일 객체, 복수 객체 조회를 구분하지 않고 같은 형식으로 응답 메시지를 구성하면 API를 만드는 입장, 사용하는 입장 모두 유지보수가 쉬워진디. (재사용 가능)
    • API를 만들 때는 인수받는 방법 과 응답 규격을 최대한 일관성 있게 만들어 모두 재사용이 쉽게 하자
  • HTTPS

    • 안전하게 메시지를 주고 받기 위해 만든 프로토콜. TLS 프로토콜 기반으로 하는 HTTP
    • TLS이 등장하기 전까진 보안 소켓 계층 SSL 을 사용했음. SSL은 많은 취약점으로 더 이상 사용하지 않지만, SSL의 이름을 계속 사용중임
    • HTTPS를 사용하면 서버와 클라이언트가 주고 받는 메시지를 암호화한다. 메시지를 암호화 복호화하는 키는 HTTPS로 메시지를 주고 받는 두 컴퓨터만 안다.
    • HTTPS는 TLS를 기반으로하여 TLS버전에 따라 사용가능한 암호화 목록과 안정성이 달라진다.
    • HTTPS 통신을 하기 위해서는 반드시 인증서가 필요함.
      • 클라이언트는 키를 교환하기 전에 이 서버가 신뢰할 수 있는 서버인지 검증하는 작업이 필요한데, 클라이언트는 이 과정에서 서버가 보내는 인증서를 통해 검증함.
    • HTTPS를 소프트웨어 프레임워크에서 설정하는 것보다 Nginx나 아파치와 같은 웹서버에서 설정하는것이 안전함.
  • OAuth 2.0

    • OAuth는 데이터를 간편하고 안전하게 주고받기 위해 만들어진 표준.
    • ID와 비밀번호 대신 엑세스 토큰 기반으로 사용자 식별. 토큰은 API를 제공하는 리소스 서버만 발급.
    • 엑세스 토큰을 요청할 때 가능하면 유효 기간을 짧게 설정하고 자주 갱신하는 게 좋음.
    • 엑세스 토큰으로 RESTful API를 사용할 때는 반드시 HTTPS를 사용하는 지 확인해야 보안 위협을 방지할 수 있다.
    • JSON Web Token(JWT)을 사용하면 인가 서버를 통해 토큰을 검증하는 대신 인가 정보를 포함해 사용하기 때문에 인가 서버의 부하를 줄일 수 있음.

2020년 5월 8일 금요일

일 잘하는 사람은 단순하게 합니다 책 메모

일 잘하는 사람은 단순하게 합니다

쉬어가는 책으로 출근 중 지하철에서 간단하게 읽은 책이다. 
책에서는 기획, 보고서, 언어소통, 관계 의 네 가지 영역에서 일 잘하는 방법에 대해 설명하고 있다. 
현재 내가 하고있는 것과는 관련이 없을 수 있지만 분명 도움이 되는 내용이었고, 나중에 활용할 수 있는 내용도 많았다. 
특히 무엇인가를 보고해야할 때나 설명할 때 횡설수설하는 경우가 많았는데, 이 책의 내용을 읽고 내가 했던 말들을 되돌아보는 계기가 되었다.


  • 생각하는 대로 살지 않으면, 사는 대로 생각하게 된디.

  • 기획자는 다음의 세 가지에 꼭 대답할 수 있어야한다.

    • 목표는 무엇인가?
    • 목표를 가로막는 진짜 문제는 무엇인가?
    • 문제를 해결하고 ,원하는 미래를 달성하기 위해 할 수 있는 실현 가능한 최적의 행동은 무엇인가?
  • 단순하게 일하는 사람은 화려한 현황보다는 무엇을, 왜 해야 하는지를 분명히 보여준다.

    • 탄탄한 기획안도 회사 방향과 맞지 않으면 무용지물이다.
  • 일 잘하는 사람은 상대방이 궁금해 하는 내용과 자기가 이야기하고 싶은 내용을 가능한 한 짧게 말하는데 선수입니다.

  • 무엇을 하려고 하는지, 보고서의 핵심은 무엇인지, 무슨 얘기를 하는지, 30초 안에 깔끔히게 설명할 수 있는 습관을 길러야 한다.

  • 기획이란 어떤 대상에 대해 그 대상의 변화를 가져올 목적을 확인하고, 그 목적을 성취하는 데에 가장 적합한 행동을 설계하는 것을 의미한다.

    • 모든 기획은 '왜'부터 시작해야한다.
  • SWOP이나 4P 등의 프레임워크는 고민 과정에서 활용하되 직접적인 언급은 지양하는 것이 좋다.

  • 덩어리를 묶을 때 각 항목끼리는 독립적이어야하고, 항목을 합치면 전체가 되야한다.

  • 좁쌀 서 말 굴리는 것보다 호박 한 개 굴리는 게 낫다

    • 굵직한 기획을 진행해야한다
  • 정보의 홍수 속에서 단순하게 글을 쓰려면 '왜 쓰는지' 처음부터 알고 써야 덜 고생스럽다.

  • 하고 싶은 얘기가 아니라, 듣고 싶어할 얘기를 쓰자

  • 작성자의 설명을 들어야 이해되는 보고서는 실패다.

    • 전체 요약 박스와 소제목별 요약 한 줄은 아무리 심오한 보고서라도 직관적으로 이해할 수 있게 한다.
  • 메시지를 위한 글쓰기에서는 하나의 핵심 키워드를 찾는 일이 관건이다.

    • 스토리들은 모두 핵심 키워드를 향하고 있어야 한다.
  • 모든 사람에게 똑같은 의미를 가진 단어는 없다.

  • '중간보고'는 서로의 의도와 방향을 조절하는 기술이다.

    • 중간보고는 필요하다. 그래야 오해가 있더라도 다시 방향을 맞출 수 있다.
  • 지시할 때 가능한 한 정확하게 설명해주자. 지시하는 사람이 5분 더 쓰면, 실행하는 사람은 하루 이상의 시간을 절약할 수 있다.

  • 두괄식으로 시작해서 30초안에 하고 싶은 얘기를 모두 끝내야한다.

  • A를 물어보면 정확히 A를 대답하자. 비슷한 대답말고.

  • 회사의 공식적인 커뮤니케이션에서는 '매우, 곧, 상당히, 최선을 다해, 심각하게, 신중히' 유의 언어는 쓰지 않는 게 좋다.

    • 오해만 불러일으킬 수 있다.
  • 숫자에 해석을 함께 곁들이면 단순하고 강력한 메시지가 된다.

  • 직장에서 최고의 평판 관리는 '상사를 승진시키는 사람'이다.

  • 생각을 끄고 켜는 연습은 내가 현재에 살도록 도와준다.

2020년 4월 19일 일요일

예외처리 핸들러 테스트

예외처리 핸들러 테스트

  • 토이프로젝트의 예외처리를 어떻게 할까 고민하다 검색과 백기선님 웹MVC강의에 예외처리 부분을 복습하였다. (회사소스에 존재하는 ExceptionHandler에는 로그만 남기도록 되어있었다.)
  • MVC에서 요청을 처리하다가 에러가 발생하거나 자바에서 지원하는 예외가 발생했을 때 정의한 핸들러로 그 예외를 어떻게 처리할지?, 어떤응답을 만들지를 정의할 수 있다.
      @ExceptionHandler
      public String RuntimeExceptionHandler(RuntimeException e, Model model){
        model.addAttribute("message", "runtime error");
        return "error"
      }
    
    • 예외처리를 핸들러를 전역 컨트롤러에 선언할 수 있다. (모든 컨트롤러에 적용됨)
      // 모든 컨트롤러 적용
      @ControllerAdvice("com.edu.controller")
      public class CommonExceptionHandler {
        ...
      }
    
    
      // 다수의 특정 컨트롤러 지정 가능
      @ControllerAdvice(assignableTypes = {EventController.class, EventApi.class})
      public class CommonExceptionHandler {
        ...
      }
    
    
      // 패키지 지정 가능
      @ControllerAdvice("com.edu.controller")
      public class CommonExceptionHandler {
        ...
      }
    
  • @RestControllerAdvice 는 @ResponseBody 가 붙은거 라고 생각하면될듯!

내가 테스트해본 것

아쉬운 점 : 그냥 잘못된 값을 던지고 해당 메서드에서 exception이 발생하였을 때 처리하는 테스트를 하였는데, 테스트 시 예외를 강제로 줘서 테스트를 하고 싶었는데 못하였음 ㅠㅠ(의미가 없으려나??)
  • CommonExceptionHandler.java
@ControllerAdvice
public class CommonExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(CommonExceptionHandler.class);

    public static final String DEFAULT_ERROR_VIEW = "exception";

    @ExceptionHandler(value = RuntimeException.class)
    public ModelAndView defaultRuntimeErrorHandler(HttpServletRequest req, RuntimeException e) {
        logger.debug("RuntimeException: " + e);
        logger.debug("url: " + req.getRequestURL());

        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);

        return mav;

    }
}
  • LectureControllerTest.java
public class LectureControllerTest extends BaseControllerTest {

/* 전역 Exception Handler 설정
CommonExceptionHandler 에서 처리하기 위한 셋팅: standaloneSetup, setControllerAdvice 지정

지정 파라미터를 다른 값으로 변경하면 CommonExceptionHandler에 안들어온다.
하지만 아예 이 부분을 제거해도 CommonExceptionHandler에 옴.
 */
    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders
                .standaloneSetup(lectureController)
                .setControllerAdvice(new CommonExceptionHandler())
                .build();
    }

  ...


    @Test
    @Description("Exception 발생 테스트")
    public void exceptionTest() throws Exception{

        // Given
        String userId = "admin2";

        // When (실패함. 어떻게 사용하는 지 더 찾아볼 것)
        //when(lectureController.checkedLecture(anyString(),anyString(),anyString())).thenThrow(new Exception("테스트입니다."));

        // Then
        mockMvc.perform(post("/lecture/checkedLecture")
                .param("userId", userId)

        ).andDo(print())
                .andExpect(status().is2xxSuccessful())
                .andExpect(view().name("exception"))
                .andExpect(model().attributeExists("exception"))
        ;

    }
}

Error, Exception 참고

  • Error : 개발자가 미리 예측하여 처리할 수 없는 경우 (JVM 자원 부족 등의 문제?)
    • 관례적으로 Error에 대해서는 Custom Class를 만들지 않는다
  • Exception : 개발자가 구현한 로직에서 발생하기 때문에 미리 예측하여 처리할 수 있음
    • RuntimeException
      • 처리를 강제하지 않음(동작하다 exception 발생)
      • 하위 Exception: NullPoint, IllegalArgument, IndexOutOfBound, System)
      • Custom UnChecked Exception 을 만들 경우 RuntimeException 클래스를 상속하여 만든다.
    • CheckedException
      • 반드시 예외처리를 해야함(처리안하면 컴파일 오류)
      • RuntimeException을 제외한 모든 클래스
      • Custom checked Exception을 만들 경우 Exception 클래스를 상속하여 만든다.
  • 예외처리 팁
    • 아무것도안하는 에러처리는 피하기 : catch 영역에 아무 동작도 안하면 예외 발생 시 추적하기 힘들어짐. 처리를 못한다면 로그라도 남길 것
    • exception.printStackTrace()는 쓰지 말 것 : 에러가 발생한 원인에 대한 Stack Trace를 추적하여 개발자에게 디버깅할 수 있는 힌트를 제공해주지만, 운영 시 성능을 생각한다면 지양해야한다. java reflection을 사용하여 trace를 추적하기 때문에 오버헤드가 발생한다고한다.
    • 반복문 내에서 Checked Exception에 대한 처리는 지양할 것 : 반복문 내에서 Checked Exception에 대한 예외처리 구문이 들어가게 되면 성능은 2배 3배 떨어지게 된다.