이번 포스팅에서는 HTTP 버전 별 차이에 대해 알아본다.

 

HTTP / 0.9 내용 

더보기

HTTP의 공식 초기 버전으로 TCP/IP 프로토콜을 사용하여 요청 시 서버의 리소스에 대한 경로만 단일 라인으로 요청하는 기능이 제공된다.

(Request)
GET /mypage.html

(Response)
<HTML>
A very simple HTML page
</HTML>

 메서드 또한 리소스에 대한 요청 GET 만 가능하고 응답 역시 단순하게 파일 내용으로만 구성된다.

HTTP / 1.0

HTTP 1.0 버전에서는 0.9의 제한적인 동작를 개선하기 위해 아래와 같은 기능들이 추가되었다.

  • Request Header 
    • 요청과 응답에 HTTP 헤더가 추가되어 메타데이터를 주고 받음 (프로토콜을 더 유연하고 확장성 있도록 개선)
  • Version Field
    • 요청과 시작 부분에 버전 정보가 추가
  • Status Codes
    • 응답 시작 부분에 상태 코드 라인을 추가 (상태 코드 라인을 통해 로컬 캐시를 갱신하거나 사용하는 동작을 수행할수 있음)
  • Content Type
    • HTTP 헤더의 Content-Type 을 통해 HTML 이외 다른 문서 파일도 통신 가능
  • Methods (POST, HEAD)
(Request)
GET /mypage.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

(Response)
200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html
<HTML>
A page with an image
  <IMG SRC="/myimage.gif">
</HTML>

HTTP 1.0

HTTP 1.0 버전의 한계

1.0 버전은 TCP를 사용하여 데이터를 주고 받을 때마다 비효율적으로 TCP 연결, 해제 과정이 이뤄진다.

 

HTTP / 1.1

HTTP 1.1 은 1999년 공개되어 일반적으로 사용되고 있는 범용적인 버전이다. 1.0 버전에서의 단일 커넥션에 대한 비효율적인 통신과정과 더 다양한 기능을 제공하기 위해 아래 기능들이 추가되었다.

  • Host Header
    • 1.1 버전에서 추가된 필수 헤더로 동일한 IP에 대해 대상 서버가 복수 도메인 일 때 구분하기 위해 사용
  • Persistent Connections
    • 1.0 버전에서의 요청 마다 매번 발생하는 불필요한 TCP 연결, 해제 과정 없이 한번의 과정으로 데이터를 주고 받을 수 있음
  • Pipelining
    • 하나의 연결 과정에서 응답을 기다리지 않고 순차적인 요청을 여러번 보낼 수 있음 (hol blocking 발생)
  • Methods (PUT, PATCH, DELETE, CONNECT, TRACE, OPTIONS)

 

(Request)
GET /en-US/docs/Glossary/Simple_header HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/en-US/docs/Glossary/Simple_header

(Response)
200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 20 Jul 2016 10:55:30 GMT
Etag: "547fa7e369ef56031dd3bff2ace9fc0832eb251a"
Keep-Alive: timeout=5, max=1000
Last-Modified: Tue, 19 Jul 2016 00:59:33 GMT
Server: Apache
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding

(content)

Persistent connection

이전 버전에서는 요청마다 tcp 연결 과정이 매번 발생하였는데, 매번 3 way handshake, 4 way handshake 가 발생하는 비용을 절감하기 위해 1.1 버전에서는 요청 헤더에 Connetion: Keep-Alive 값을 담아서 timeout 시간 동안에는 연결을 해제하지 않도록하여 tcp 연결을 유지하는 방법을 사용하였다. (참고로 1.1 버전은 기본으로 persistent connection 지원, 1.0 버전에서도 헤더에 Keep-Alive 값을 추가하면 사용 가능)

HTTP 1.1

Pipelining

클라이언트는 한번 보낸 요청에 대한 응답을 기다리지 않고 순차적으로 여러번 요청을 보낼 수 있다. 그에 따라 서버는 순차적으로 요청에 대한 응답을 보내주어 요청과 응답에 대한 지연시간을 줄일 수 있다.

no pipelining vs pipelining

 

HTTP 1.1 버전의 한계

1. HOL (Head Of Line) Blocking

1.1 버전은 pipelining 을 통해 순차적으로 여러 요청을 서버에 전송할 수 있지만 서버측에서 응답을 순서대로 하나 하나 보내줘야 하기 때문에 가장 처음 보낸 요청에 대한 응답이 느려질 경우 이후 모든 요청에 대해 지연이 발생할 수 있다. 

HTTP hol blocking

HOL Blocking 의 종류는 다음과 같이 두 종류가 존재한다.

- HTTP HOL Blocking

- TCP HOL Blocking

 

2.  중복된 헤더 전송

1.1 버전부터 추가된 다양한 헤더 정보들을 매번 중복해서 주고 받기 때문에 매우 비효율적이다.

 

HTTP / 2.0

HTTP 2.0 은 2015년에 공개되었다. 2.0 버전은 1.1 버전에서 HOL Blocking 과 같은 성능 이슈들을 개선하기 위해 다음의 기능들이 추가되었다.

 

  • Request and Response Multiplexing
    • 한번의 연결(세션)로 동시 여러개의 메시지를 응답 순서없이 주고 받을 수 있다.
  • Stream Prioritization
    • 브라우저에서 리소스 간의 의존관계(우선순위)를 설정할 수 있다.
  • Header Compression
    • 요청과 응답의 헤더 메타데이터를 HPACK 압축 방식을 통해 압축한다.
  • Server Push
    • 클라이언트가 별도의 요청 없이 필요 시 서버에서 클라이언트에게 추가적인 리소스를 보낼 수 있다.

 

HTTP 2.0

HTTP 2.0 버전의 한계

2.0 버전은 이전 버전들의 한계를 개선하였는데 전송 계층인 TCP 프로토콜의 자체적인 한계점으로 인해 tcp hol blocking 을 해결할 수 없다.

 

HTTP / 3.0

HTTP 3.0 은 2020년도에 공개된 버전으로 가장 최근 공개된 공식 버전이다. 기존 HTTP 프로토콜의 전송 계층인 tcp 의 기능적 한계(tcp hol blocking)를 개선하기 위해 UDP 기반의 QUIC(Quick UDP Internet Connections) 를 사용하였다.

HTTP 3.0

 

 

참고 내용

  • https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP
  • https://www.youtube.com/watch?v=a-sBfyiXysI&t=39s
  • https://www.popit.kr/%EB%82%98%EB%A7%8C-%EB%AA%A8%EB%A5%B4%EA%B3%A0-%EC%9E%88%EB%8D%98-http2/
  • https://www.baeldung.com/cs/http-versions
  • https://www.whatap.io/ko/blog/38/

 

 

 

 

 

알고리즘 문제를 풀다 LinkedHashMap 에 대해 확인하던 중, LRU cache 를 구현하는데 적합한 자료구조라는 것을 알게되었다. 아래 내용은 jdk 8 공식 홈페이지의 LinkedHashMap 일부 내용이다.

This kind of map is well-suited to building LRU caches.

LinkedHashMap 의 내부 로직이 어떻게 되길래 LRU cache 구현에 적합한지 궁금해서 찾아보게 되었다. 참고로 LinkedHashMap 은 jdk 1.4 에서 최초 추가되었다.

 

공식 홈페이지의 내용을 계속 읽어보니 removeEldestEntry 메서드를 통해 새로운 데이터가 들어왔을 때 제거 정책을 어떻게 할지 정할 수 있다고 한다.

The removeEldestEntry(Map.Entry) method may be overridden to impose a policy for removing stale mappings automatically when new mappings are added to the map.

자세한 로직을 확인하기 위해 최근 가장 많이 사용하는 버전인 jdk 11 내부를 분석해봤다.

 

LinkedHashMap 은 HashMap 을 상속하고 Map 을 구현한다. 따라서 내부 데이터를 저장하는 구조와 기존적인 데이터 저장, 삭제 같은 메서드는 HashMap 과 동일하다.  (LinkedHashMap 는 그외에 링크드 리스트 구조의 자료 형태를 유지하기 위한 변수와 메서드들이 존재한다.)

 

따라서 LinkedHashMap 에서 put 을 하게되면 HashMap 내부 putVal 메서드를 통해 데이터가 적재된다. 이때 putVal 메서드 내부 마지막에서 afterNodeInsertion 메서드를 호출한다. 이때 evict 이라는 변수를 매개변수로 전달하게 되는데 일반적으로 true 상태가 디폴트 값이다.

 

afterNodeInsertion 메서드는 LinkedHashMap 에서 다음과 같이 구현되어 있다.

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry 메서드의 기준에 따라 첫번째 데이터를 삭제하게 된다. removeEldestEntry는 오버라이딩하지 않으면 기본적으로 false 를 리턴하게 하여 기존의 데이터를 삭제하지 않도록 설계되어 있다.

따라서 LRU cache 로서 사용하고 싶다면 다음과 같이 조건을 주어서 삭제 정책을 적용해주어야 한다.

class ExpiringCache {
    private Map<String,Entry> map;
    private int MAX_ENTRIES = 200;
    
    ...
    
    ExpiringCache(long millisUntilExpiration) {
        this.millisUntilExpiration = millisUntilExpiration;
        map = new LinkedHashMap<>() {
            protected boolean removeEldestEntry(Map.Entry<String,Entry> eldest) {
              	return size() > MAX_ENTRIES;
            }
      	};
    }
}

자세한 이해를 돕기위해 실제 오픈소스에서 사용중인 예시를 가져왔다. 위 예시는 현재 LinkedHashMap 내부 데이터 크기가 MAX_ENTRIES 보다 크면 가장 첫번째 데이터를 삭제하는 예시로 java.io 의 ExpiringCacha 클래스이다.

 

그런데 아직 LinkedHashMap 가 LRU cache 로서 적합한지 확실하게 알 수 없다 왜냐면 가장 마지막으로 사용된 데이터가 삭제되는지 확인이 필요하기 때문이다. afterNodeInsertion 에서 맵의 첫번째 데이터를 삭제하는 것을 확인하였는데 첫번째 데이터는 어떻게 정해지는지 확인해보겠다.

 

putVal 메서드를 사용하여 데이터를 적재 할 때 신규 key로 데이터를 만드는 상황과 신규 key와 기존 key가 충돌나는 상황이 있다.

 

  • 신규 key로 데이터를 만드는 상황

먼저 데이터가 기존에 없으면 newNode 메서드로 생성을 하는데 이때 LinkedHashMap 에서는 linkNodeLast 라는 메서드를 추가로 호출하여 만들어진 데이터를 tail 값을 통해 리스트 가장 맨 끝으로 연결한다.

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

 

  • 신규 key와 기존 key 충돌 상황

데이터 충돌이 발생하였을 때는 afterNodeAccess 메서드를 추가로 호출한다. 해당 메서드는 매개변수로 들어온 Node 데이터를 리스트 가장 맨 끝으로 연결하고 있다.

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

이런 동작에 따라 LinkedHashMap 자료구조 내부 리스트에서 가장 앞 데이터는 가장 오래된 데이터임을 확인할 수 있다.

 

결과적으로 put 을 했을 때 캐시가 허용 범위를 가득차면 가장 오래된 데이터를 삭제하고 기존 데이터에 대해 갱신이 되었을 때 최신 데이터로 위치를 조정하고 있어 LRU(Least Recently Used) cache 를 구현하는데 적합한 자료구조임을 확인할 수 있다.

 

참고

- https://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashMap.html 

 

 

 

queryDsl 초기 설정 및 테스트 코드 작성

이번 게시글에서는 querydsl 을 springboot 프로젝트에 추가하고 간단한 테스트 코드를 통해 querydsl 사용 방법을 소개한다.

먼저 프로젝트 초기 build.gradle 에 아래의 의존성 코드를 추가해준다.

build.gradle

buildscript{
    ext {
        ...
        querydslPluginVersion = '1.0.10' // 플러그인 버전
    }

    repositories {
        ...
        maven { url "https://plugins.gradle.org/m2/" } // 플러그인 저장소
    }

    dependencies {
        // querydsl 플러그인 의존성 등록
        classpath("gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:${querydslPluginVersion}") 
    }
}

dependencies {
    ...
    // queryDsl 의존성 추가
    compile("com.querydsl:querydsl-core:4.2.1")
    compile("com.querydsl:querydsl-apt:4.2.1")
    compile("com.querydsl:querydsl-jpa:4.2.1")
    compile("com.querydsl:querydsl-collections:4.2.1")
    ...
    annotationProcessor("com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa") // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor("jakarta.persistence:jakarta.persistence-api") // java.lang.NoClassDefFoundError(javax.annotation.Entity) 발생 대응
    annotationProcessor("jakarta.annotation:jakarta.annotation-api") // java.lang.NoClassDefFoundError (javax.annotation.Generated) 발생 대응

}

// querydsl 적용
apply plugin: "com.ewerk.gradle.plugins.querydsl" // Plugin 적용
def querydslSrcDir = 'src/main/generated' // QClass 생성 위치

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

application.properties

실행 과정에서 "The bean 'jpaAuditingHandler', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled." 이런 에러 메시지가 발생하면 application.properties 에 다음 내용을 추가해주면 된다.

spring.main.allow-bean-definition-overriding=true

이후 QClass를 생성하려면 아래의 사진처럼 Gradle Project에서 compileJava를 실행시켜주면 src/main/generated 에 QClass가 생성된다.

Java Config

이제는 querydslconfig 클래스를 생성해준다.

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@EnableJpaAuditing
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

이후 프로젝트 내부 다른 자바 클래스 어디에서도 JPAQueryFactory를 주입 받아 querydsl을 사용할 수 있다.

querydsl 적용하기

실제 프로젝트 내부 User 객체 클래스에 적용하려 한다. 먼저 User 클래스와 UserRepository 를 추가해준다.

...
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    ...
public interface UserRepository extends JpaRepository<User, Long> {

}

이제 querydsl를 지원하는 UserRepositorySupport 클래스를 추가해준다.

package com.jungwoo.book.springboot.domain.user;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

import static com.jungwoo.book.springboot.domain.user.QUser.user;

import java.util.List;

@Repository
public class UserRepositorySupport extends QuerydslRepositorySupport {
    private final JPAQueryFactory queryFactory;

    public UserRepositorySupport(JPAQueryFactory queryFactory) {
        super(User.class);
        this.queryFactory = queryFactory;
    }

    public List<User> findByName(String name) {
        return queryFactory
                .selectFrom(user)
                .where(user.name.eq(name))
                .fetch();
    }

}

이때 findByName 함수에서 user를 인식하지 못하는 데, 아래의 방법을 통해 querydsl에서 QClass로 생성된 user를 인식하도록 해준다.

아까와 마찬가지로 아래의 사진처럼 gradle project 에 compileQuerydsl 을 눌러서 인식하도록 할수 있다.

querydsl 테스트 코드 작성하기

아래의 테스트 코드를 통해 기능이 제대로 되는지 확인할수 있다.

...
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static com.jungwoo.book.springboot.domain.user.Role.USER;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserRepositorySupport userRepositorySupport;

    @After
    public void down() throws Exception {
        userRepository.deleteAllInBatch();
    }

    @Test
    public void querydsl_기본_기능_확인() {
        //given
        String name = "jungwoo";
        String address = "jungwoo@gmail.com";
        String picture = "???";
        Role role = USER;

        userRepository.save(User.builder()
                                .name(name)
                                .email(address)
                                .picture(picture)
                                .role(role)
                                .build());

        //when
        List<User> result = userRepositorySupport.findByName(name);

        //then
        assertThat(result.get(0).getEmail().equals(address));
    }
}

 

ref

 

문제 설명

해당 문제는 주어진 히스토그램에서 가장 큰 직사각형을 구해서 출력하는 문제이다.

문제 풀이

이 문제는 풀이 방법이 다양한데 일반적으로 분할 정복, 스택 그리고 스위핑 알고리즘으로 풀이가 가능하다.
여기서는 스택을 활용하여 문제를 해결하였다.

 

문제는 간단하다. 가장 큰 직사각형을 구해야 하는데, 직사각형은 보통 밑변 혹은 높이가 클수록 커진다.
그렇기 때문에 밑변이나 높이가 가장 큰 값을 기준으로 비교를 계속해야 한다.

 

따라서 가장 큰 높이나 밑변을 구하기 위해 스택을 활용하는데, 입력받은 히스토그램을 순회하면서 현재 위치를 스택에 푸시한다.
현재 위치는 나중에 밑변을 구하기 위함이고 while을 통해 스택의 top에는 점점 증가하는 히스토그램의 높이 값을 갱신한다.

 

현재 스택의 히스토그램의 top 위치의 높이 값보다 작은 히스토그램 높이 값이 들어오면 현재까지 중복된 밑변과 높이가 있기 때문에 while 내에서 pop을 하면서 히스토그램의 top 위치의 값을 현재까지의 높이 현재 위치 i 값과 top의 위치 값을 뺀 밑변 값에 곱하여 직사각형의 넓이를 구한다.

 

결국에 while을 통해 계속 직사각형의 넓이를 구하여 가장 큰 직사각형의 값을 구할 수 있다.

소스 코드

문제 : [BOJ] 1725. 히스토그램

 

1725번: 히스토그램

첫 행에는 N (1 ≤ N ≤ 100,000) 이 주어진다. N은 히스토그램의 가로 칸의 수이다. 다음 N 행에 걸쳐 각 칸의 높이가 왼쪽에서부터 차례대로 주어진다. 각 칸의 높이는 1,000,000,000보다 작거나 같은

www.acmicpc.net

/*
[boj] 1725. 히스토그램
https://www.acmicpc.net/problem/1725
*/

#include <bits/stdc++.h>
#define io ios_base::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;
typedef long long ll;
int N;
ll ans;
vector<ll> v;
int main() 
{
    io;
    cin >> N;
    v.resize(N+1);
    for(int i=0;i<N;i++) {
        cin >> v[i];
    }
    v[N] = 0;
    stack<ll> s;
    for(int i=0;i<=N;i++) {
        while(!s.empty() && v[s.top()] >= v[i]) {
            ll cur = s.top();
            s.pop();
            int w;
            if(s.empty()) w = i;
            else w = (i - s.top() - 1);
            ans = max(ans, v[cur] * w);
        }
        s.push(i);
    }
    cout<<ans<<"\n";
    return 0;
}

해당 글은 git 초보자가 특정 오픈소스에 pull request(이하 pr) 요청 통해 기여하는 과정을 돕기 위한 가이드라인 문서입니다.

목차

  • Fork 해서 내 저장소에 복사하기
  • 저장소 항상 최신 코드로 동기화하기
  • Fork 한 코드에 Branch 새로 만들어 작업하기
  • 작업한 코드를 원본 저장소에 PR 요청하기

 

Fork 해서 내 저장소에 복사하기

누군가의 오픈소스에 기여하기 위해 먼저 해야할 작업은 원본이 되는 대상 오픈소스 프로젝트를 내 github 저장소로 가져와야 한다. 이후에 내가 추가하고 싶은 내용을 편집하고 변경사항을 원본에 요청할 수 있기 때문이다.

 

원본 프로젝트의 저장소를 들어가보면 오른쪽 상단에 Fork 버튼을 선택하여 내 github 저장소에 복사할 수 있다.

원본 프로젝트 저장소에서 Fork를 하면 자신의 저장소에 새로 복사된 것을 확인할 수 있다.

따라서 이후 작업은 fork 한 저장소에서 진행하면 된다.

 

저장소 항상 최신 코드로 동기화하기

보통 처음 pr를 하거나 pr을 많이 해보지 않았다면 해당 작업을 안 해본 사람들이 많을 것이다. 왜냐면 보통 처음 fork를 한 다음엔 코드가 최신이기 때문에 따로 최신으로 갱신해줄 필요가 없기 때문이다. 하지만 fork를 한 다음 지속적으로 기여를 하다 보면 대다수의 오픈소스들은 계속 누군가에 의해 코드가 변경될 것이다.

 

만약 원본 프로젝트의 코드를 최신화하지 않으면 코드를 pr 하는 순간, 수 많은 충돌로 인해 멘붕 하게 된다. 😱😱

 

그렇기 때문에 자신의 저장소에 fork 한 프로젝트를 항상 최신화 하는 작업이 매우 중요하다. 👍👍

 

먼저 fork 한 프로젝트를 clone 하여 작업할 공간을 만든다. 그 다음 git remote -v 명령을 통해 현재 원격 저장소로 등록된 목록을 확인해준다.

아직은 origin으로 등록된 내 원격 저장소의 프로젝트 경로만 보인다. 이제 원본 프로젝트를 다음의 명령으로 추가해주자.

 

$ git remote add upstream (원본 프로젝트 url 경로)

 

원본 프로젝트의 url 경로를 추가해주면 다음과 같이 등록된 것을 확인할 수 있다.

이제 fetch 명령을 통해 원본 프로젝트의 최신 코드를 현재 작업 중인 로컬 저장소로 가져온다.

$ git fetch (코드를 가져올 원격 저장소)

$ git fetch (코드를 가져올 원격 저장소)

git fetch 명령어는 코드를 가져올 프로젝트를 위에서 설정한 upstream으로 입력해준다.

명령어가 제대로 실행되었다면 upstream 으로 설정한 원본 프로젝트의 최신 코드를 가져올 수 있다.

그다음 최신 코드로 적용하기 위한 branch에 checkout 한 뒤 나의 로컬 저장소에 merge 하면 최신 코드가 적용이 된다.

$ git merge (원격 저장소/브랜치) (코드를 합칠 현재 나의 로컬 저장소의 브랜치)

마지막으로 merge 완료한 최종 코드를 나의 원격 저장소에 push 하여 나의 github 저장소의 코드도 최신으로 갱신할 수 있다.

 

Fork 한 코드에 Branch 새로 만들어 작업하기

이제 개발 준비는 다 되었다. 실제 오픈소스에 기여하기 위한 작업을 위해 나만의 작업 branch를 만들어 주자.

위에서 fork 한 나의 원격 저장소의 코드를 clone을 통해 가져온 다음 작업하기 위한 branch를 새로 만들어서 작업을 진행하도록 하자.

branch 명은 자신이 작업할 기능 혹은 내용에 대해 추측할 수 있는 이름으로 정하면 나중에 어떤 내용이 수정된 건지 예측이 가능하여 관리하기에 좋다.

 

작업한 코드를 원본 저장소에 PR 요청하기

이후 생성한 branch에서 작업을 한 다음 자신의 origin 저장소에 코드를 push 하면 자신의 github 저장소에 다음과 같이 이벤트가 발생한다.

그러면 이제 compare & pull request 버튼을 눌러 원본 프로젝트에 pr 요청을 할 수 있다.

항상 pr 요청하기 전에 신중하게 자신이 pr 요청하려는 commit 내역이 제대로 되었는지 꼭 한 번 더 확인하는 습관을 갖도록 하자

pr 요청을 하면 아래처럼 pr 메시지와 자신의 코드를 merge 할 원본 프로젝트의 branch를 선택할 수 있다.

pr 메시지는 되도록 변경 사항에 대해 구체적으로 작성해주자

 

pr 메시지 작성과 merge할 branch를 선택한 뒤에 Create pull request를 눌러 pr을 생성하면 아래와 같이 pr 이 만들어진 것을 확인할 수 있다. 🥳

 

 

처음 오픈소스에 기여하는 것은 쉽지 않다. 각각의 오픈소스마다 자신들만의 규칙이 있고 기여를 하려면 해당 코드에 대해 분석도 필요하고 깃을 처음 다루는 사람이라면 간단한 pr 요청조차도 하기 어렵다. 하지만 따로 연습 저장소를 만들어 연습을 하고 반복된 기여 작업을 하다 보면 어느 순간 쉽게 깃을 다루고 해당 오픈소스에 나의 코드를 많이 기여할 거라 생각한다.

 

다양한 코드를 많이 접해보고 꾸준히 기여할 수 있도록 노력하자. 👍👍

 

+ Recent posts