요약: 이 강연은 오픈소스 활동과 함께한 20년 동안 발표자가 경험한 다양한 삶의 이야기들과 함께, 소프트웨어 개발자들이 시대의 변화를 마주하는 방법에 대한 시각을 나눕니다.
한국에 몇 없는 구글 개발자인 신정규 대표님의 강연을 듣게 되어 영광이었습니다.
소중한 강연을 들을 기회를 마련해 주신 국민대학교 김상철 교수님께 감사드립니다.
강연을 통해 앞으로 프로그래머가 준비해야 할 기술들과 임해야 할 자세, 그리고 오픈소스의 기능과 중요성을 느끼게 해 준 특강이었습니다.
프로그래밍의 의미?
20세기 컴퓨터에서 돌아가는 소프트웨어를 만드는 작업
21세기 프로그래밍이 가능한 전자기기를 통한 유무형의 모든 창작 작업
프로그래밍은 과거보다 더 넓은 의미를 갖게 되었습니다. 더 이상 컴퓨터에서만 돌아가는 소프트웨어가 아닌 모든 창작 작업을 프로그래밍의 의미로 볼 수 있습니다.
프로그래밍의 중요성 또한 커지고 있습니다.
세상은 무엇으로 이루어져 있는가 물어본다면, 대표적으로 정치, 경제, 사회, 문화, 기술, 과학라고 할 수 있습니다.
우리는 현재 기술과 과학이 중요한, 정치, 경제, 사회, 문화의 연결 고리를 프로그래밍이 하고있는 시대에 살고있다고 강조하셨습니다.
이를 통해 프로그래밍은 우리 일상에서 가까워지고 있고 중요하게 다가오고 있다는 것을 확인하였습니다.
“오픈소스 프로그래밍이라 쓰고 인생이라고 읽어봅시다.”
신정규 대표님은 '오픈소스’를 강조하셨습니다.
경제적인 관점에서 희소성이 줄어들수록 가치가 떨어진다는 원리입니다.
그런데 본인의 코드가 멀리 퍼져 누구나 알게된 코드가 되었다고 상상해 봅시다.
그럼 저의 코드는 가치가 떨어졌을까요?
저는 아니라고 대답할 것 같습니다.
나만 알고 있는 코드는 내가 지우면 끝입니다. 어떤 문제가 있고, 더 좋은 방법이 있어도 내가 알지 못하면 알 수 없습니다. 하지만, 오픈소스로 코드를 남긴다면 기록으로 남게 됩니다. 그리고 많은 사람들과 함께 자신의 코드를 개선해갈 수 있어 발전 가능성이 매우 높아집니다.
이는 저에게 향후 개발 프로젝트에 있어 오픈소스로 개발을 해야겠다는 영감을 주었습니다.
우리는 언제나 지나간 후의 그림자를 본다
지금 IT 트렌드 기술이라 하면,
ML/ AI, 오픈소스 하드웨어 플랫폼, 메타버스 '기반’ 기술, aI 기반 전문가 시스템, 머신 주도적 기술 / 과학
스마트 정치/ 경제 등이 있습니다. 지금 이 기술들을 공부하겠다고 내가 그 기술을 습득했을 때 미래는 어쩌면 또 다른 기술이 트렌드가 될 수 있습니다.
"개발자라 한다면 스스로를 코딩하는 사람으로 정의하지 않았으면 좋겠다" "비즈니스 문제를 해결하는 사람으로 생각하는 것이 좋겠다."
배달의민족 김범준 대표의 좋은 개발자란 어떤 사람인지의 대해서 나오는 영상입니다.
엘리베이터를 기다리는 것이 지루하다는 문제가 있다고 생각해 봅시다. 사람들은 '엘리베이터가 늦게 오는 것 같다'라고 말합니다.
이에 대해 어떤 두 회사는 각각 다른 방법으로 문제를 해결합니다. A 회사는 많은 돈을 써서 엘리베이터 속도를 높이는 공사를 합니다. B 회사는 엘레베이터 앞에 거울을 설치합니다.
A 회사는 엘리베이터가 실제로 느리다고 문제를 해석했고, B 회사는 엘리베이터를 기다리는 그 순간의 지루함을 문제로 해석하였습니다. 엘레베이터를 기다리는 것이 지루하다는 문제를 어떻게 바라보느냐에 따라 해결법이 크게 달라집니다.
"풀고자 하는 문제를 정확히 이해하는 것, 여기에 노력의 80%는 들어가야 되지 않나"
어떠한 문제를 정확히 이해하는 것, 그 문제에 대해 해결법을 제시하는 것이 '개발자'다라고 영상에서 말합니다. 즉 '문제 해결력'이 중요하다는 것입니다.
내가 어떠한 일을 한 것이 얼마큼의 비즈니스적 가치가 있는가 코드를 1000줄, 10000줄을 썼다고 해서 그 가치를 증명하는 방법이 아닙니다. 어쩌면 코드 한 줄 안 썼어도 코드 10000줄짜리 일 보다 더 비즈니스적 가치를 지닐 수 있습니다.
하지만 본인의 해결법이 무조건 옳은 해결법이 아니겠죠 구성원과 '소통'하는 것도 중요합니다. "엘리베이터가 늦게 오는 것 같아요? 그럼 거울을 설치하는건 어때요?" "아니지 늦게 오니까 엘레베이터 속도를 빠르게 해야지" 어쩌면 더 좋은 해결법이 나올 수 있기 때문에 의견을 나누는 시간도 무척 중요합니다.
"SOFTWARE IS EATING THE WORLD"
세상의 문제들이 점점 소프트웨어로 해결할 수 있는 시대가 온 것 같습니다.
예를 들어, 배달의 민족 앱을 통해 배달이 안 되는 식당도 배달이 가능하게 되었고, 원하는 시간에 배달이 오도록 할 수 있고, 환경 문제도 해결할 수도 있는 등의 소비자가 겪을 수 있는 문제를 소프트웨어적으로 해결한 사례입니다.
일상에서, 사회적으로, 어디서든 어떤 문제를 발견하면 이를 어떻게 해결할 것인가 고민하는 습관을 가져야겠다고 생각하였습니다. 그렇다면 자연스레 개발자가 되어있지 않을까요
애플리케이션 개발 단계를 자동화하여 애플리케이션을 더욱 짧은 주기로 배포할 수 있습니다. CI/CD는 새로운 코드 통합으로 인해 개발 및 운영팀에 발생하는 문제(일명 "통합 지옥(integration hell)")를 해결하기 위한 솔루션입니다.
"CI"는 개발자를 위한 자동화 프로세스인 지속적인 통합(Continuous Integration)을 의미합니다.지속적인 통합이 제대로 구현되면 애플리케이션 코드의 새로운 변경 사항이 정기적으로 빌드 및 테스트를 거쳐 공유 리포지토리에 병합됩니다. 따라서 여러 명의 개발자가 동시에 애플리케이션 개발과 관련된 코드 작업을 할 경우 서로 충돌하는 문제를 이 방법으로 해결할 수 있습니다.
"CD"는 지속적인 서비스 제공(Continuous Delivery) 및/또는 지속적인 배포(Continuous Deployment)를 의미하며 이 두 용어는 상호 교환하여 사용됩니다. 두 가지 의미 모두 파이프라인의 추가 단계에 대한 자동화를 뜻하지만 때로는 얼마나 많은 자동화가 이루어지고 있는지를 설명하기 위해 별도로 사용되기도 합니다. 지속적인 통합(CI)을 통해 만들어진 결과물을 자동화된 프로세스를 통해 릴리스 과정을 자동화하고, 사용자에게 빠르게 새로운 기능을 제공할 수 있습니다.
Github Actions
github에서 공식적으로 제공하는 CI / CD 툴(개발 워크 플로우 자동화 툴)이라고 보면 됩니다.
Actions탭을 클릭 후 New workflow를 클릭하면 다음과 같은 페이지로 이동합니다.
여러 프레임워크 및 언어를 위한
저는 Spring build tool로 Gradle을 사용하기 때문에 Java with Gradle을 사용하였습니다.
생성되는 파일과 Path는. github/workflows/gradle.yml입니다.
아래는 초기 설정입니다.
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Java CI with Gradle
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: build
name: Workflow의 이름을 나타냅니다. on: Workflow가 실행되는 조건을 지정합니다. 위의 코드에서는 push 이벤트와 pull_request 이벤트가 발생했을 때, master 브랜치가 아닌 모든 브랜치에서 Workflow가 실행되도록 설정되어 있습니다. jobs: 이 Workflow가 수행하는 작업을 정의합니다. 위의 코드에서는 build라는 이름의 작업이 정의되어 있습니다. runs-on: 이 작업이 실행되는 환경을 지정합니다. 위의 코드에서는 ubuntu-latest 환경에서 실행되도록 설정되어 있습니다. env: 이 작업에서 사용하는 환경 변수를 지정합니다. 위의 코드에서는 working-directory 변수를 지정하여 작업 디렉터리를 지정하고 있습니다. services: 이 작업에서 사용하는 서비스를 지정합니다.
postgres:
-image: 실행할 이미지를 지정합니다. 이 경우, PostgreSQL 14 버전 이미지인 postgres:14를 사용합니다. -env: 컨테이너에서 사용할 환경 변수를 지정합니다. - POSTGRES_USER: 데이터베이스의 사용자 이름 - POSTGRES_PASSWORD: 사용자의 비밀번호 - POSTGRES_DB: 데이터베이스의 이름 - POSTGRES_HOST: 호스트 이름 - POSTGRES_PORT: 포트 번호 -ports: 컨테이너와 호스트 간의 포트 포워딩을 설정합니다. 이 경우, PostgreSQL 데이터베이스에서 사용하는 기본 포트인 5432를 호스트의 5432 포트와 연결합니다. -options: 컨테이너를 실행할 때 추가적인 옵션을 설정합니다. 이 경우, PostgreSQL 데이터베이스가 실행되는 동안 컨테이너 상태를 모니터링하기 위한 옵션을 설정합니다. --health-cmd pg_isready: pg_isready 명령을 사용하여 PostgreSQL 데이터베이스의 상태를 모니터링합니다. --health-interval 10s: pg_isready 명령을 실행하는 주기를 10초로 설정합니다. --health-timeout 5s: pg_isready 명령이 실행되는 최대 시간을 5초로 설정합니다. --health-retries 5: pg_isready 명령을 재시도할 횟수를 5회로 설정합니다.
steps: 이 작업에서 수행하는 일련의 단계를 정의합니다. 위의 코드에서는 아래와 같은 단계가 수행됩니다. actions/checkout@v3 단계: Git 저장소에서 코드를 체크아웃합니다. actions/setup-java@v3 단계: JDK 11을 설치하고 환경 변수를 설정합니다. actions/cache@v3 단계: Gradle 캐시를 저장하고 복원합니다.
uses: 이 액션에서 사용할 GitHub Actions를 지정합니다. 이 경우, actions/cache@v3를 사용하고 있습니다.
with: 이 액션에서 사용할 입력값을 지정합니다. 여기서는 path, key, restore-keys를 사용하고 있습니다.
path: 캐시로 사용할 파일 또는 디렉토리를 지정합니다. 이 경우, Gradle 캐시 파일이 저장된 경로인 ~/.gradle/caches와 ~/.gradle/wrapper를 지정합니다.
key: 캐시의 고유 키를 지정합니다. 이 키는 캐시를 검색하거나 저장할 때 사용됩니다. 여기서는 러너의 운영 체제(${{ runner.os }})와 Gradle 설정 파일의 해시값($hashFiles('**/*.gradle*', '**/gradle-wrapper.properties'))을 조합하여 고유 키를 생성합니다.
restore-keys: 캐시를 검색할 때 사용할 키를 지정합니다. 이 경우, 러너의 운영 체제(${{ runner.os }})와 "gradle"을 조합하여 키를 생성합니다.
./gradlew clean build: Gradle 빌드를 실행합니다.
./gradlew test: Gradle 테스트를 실행합니다.
EnricoMi/publish-unit-test-result-action@v2 단계: 테스트 결과를 게시합니다.
여기서 EnricoMi는 자동화된 테스트 실행 후, 테스트 결과를 깃허브에 게시하는 기능을 수행합니다.
Spring Security는 유저에 대한 인증 및 권한처리를 가능하게 해주는 spring 보안 프레임워크입니다.
저는 프로젝트를 진행하면서 @RestControllerAdvice를 사용해
전역적으로 예외 처리를 하도록 하였으나, 기대한 HTTP status code와 에러 메시지와는 달리
403 Fobidden만 응답받을 뿐이었습니다.
이 문제는 User가 로그인을 하지 않은 채, 서비스의 접근할 경우
발생한 예외였습니다. 즉 인증되지 않은 클라이언트가 서버에 요청을 보냈을 때의 발생한 상황이었습니다.
조사해 보니 Spring Security의 Filter Chain으로 발생한 예외는 서블릿 필터 단계에 속하는 부분이기 때문에 @RestControllerAdvice와 같은 어노테이션으로 예외 처리를 불가능하다는 사실을 알았습니다.
그래서 Spring Security가 발생시키는 Exception Handler를 따로 구현해야 할 필요가 있었습니다.
Spring Security에선 AccessDeniedHandler interface와 AuthenticationEntryPoint interface가 존재합니다.
AccessDeniedHandler는 서버에 요청을 할 때 액세스가 가능한지 권한을 체크후 액세스 할 수 없는 요청을 했을 시 동작되고,
AuthenticationEntryPoint는 인증이 되지않은 유저가 요청을 했을 때 동작됩니다.
제가 참여한 프로젝트에선 User의 권한보다 인증의 비중이 더 높아, AuthenticationEntryPoint만 따로 구현해주었습니다.
package com.thesurvey.api.exception;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* Handles authentication errors that occur during the Spring Security filter chain, such as when a
* user attempts to access a secured endpoint without authentication credentials.
*/
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
// method is called by the Spring Security filter chain when an authentication error occurs.
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("권한이 없습니다.");
}
}
public class SecurityConfiguration {
private final AuthenticationProvider authenticationProvider;
private final SessionFilter sessionFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
return http
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers(
"/v2/api-docs/**",
"/v3/api-docs/**",
"/configuration/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/docs/**"
).permitAll()
.antMatchers("/admin/**").hasAuthority("ADMIN")
.antMatchers("/surveys/**").authenticated()
.antMatchers("/users/**").authenticated()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPointHandler())
.and()
...
AuthenticationEntryPoint interface를 구현한 AuthenticationEntryPointHandler 클래스를 만들고
"In the context of REST APIs, when making multiple identical requests has the same effect as making a single request – then that REST API is called idempotent."
Idempotency
수학에서 Idempotency(멱등법칙)이란, 아무리 연산을 여러 번 해도 결과 값이 달라지지 않는 법칙입니다.
예를 들어, 1 * 1 * 1 * 1 * 1 같은 연산입니다.
REST API에서 Idempotency란 성공적으로 수행된 요청이 서버 리소스에 미치는 효과가 해당 요청이 몇 번 실행되었는지에 독립적인 것을 의미합니다.
REST API를 설계할 때 API 소비자가 실수를 할 수 있다는 점을 인식해야 합니다. 클라이언트는 API에 중복 요청을 전송할 수 있습니다. API를 구현할 때 중복 요청이 시스템을 불안정하게 만들지 않도록 해야 합니다.
또한, Idempotency는 효율적인 캐싱 및 최적화 전략을 위한 주요 요소입니다. 캐시 및 콘텐츠 전송 네트워크(CDN)는 멱등한 요청의 결과를 저장하고 제공함으로써 서버 부하를 줄이고 응답 시간을 개선할 수 있습니다.
Example
예를 들어 HTTP 메소드들 중에서 DELETE 요청은 N개의 유사한 DELETE 요청을 실행할 때 첫 번째 요청은 리소스를 삭제하고 응답은 200(OK) 또는 204(No Content)가 될 것입니다.
나머지 N-1개의 요청은 404(찾을 수 없음)를 반환할 것입니다.
분명히 응답은 첫 번째 요청과 다르지만, 서버 측에서는 원본 리소스가 이미 삭제되었으므로 어떤 리소스의 상태 변경도 없습니다.
그러므로 DELETE 요청은 Idempotency 합니다.
HTTP methods supported Idempotency
(여기서 is safe란, 서버의 상태를 수정하지 않는 HTTP 방법은 안전한 방법을 의미합니다.)