// 에드센스


1. 구현하고자 하는 것

 

인증은 Firebase에서,

인가는 Spring Security로,

구성하려고 한다.

이후 Controller에서 @PreAuthorize를 통해서 요청을 제한하고자 한다.

즉, Firebase에서 발급한 JWT를 Spring Security에서 사용하기.

 

인증 - 유저가 누구인지 확인하는 절차

인가 - 유저가 요청한 요청(request)을 실행할 수 있는지를 확인하는 절차

 

 

 

 


2. 현재까지 구현된 것

nodejs로 구현한 회원가입 로직에 firebase-admin를 사용하여 Firebase에 유저 생성 및 권한등록을 해주었다.

 

 

import * as admin from 'firebase-admin';

export const createFirebaseUser = (email: string, password: string, name: string) => {
  return admin.auth().createUser({
    email: email,
    password: password,
    displayName: name,
    disabled: false,
  });
};

firebase-admin 라이브러리로 firebase에 새로운 user를 등록해 주었고

 

 

import * as admin from 'firebase-admin';

export const setRole = (uid: string, role: string) => {
  let claims = {
      group: role,
  };
  admin.auth().setCustomUserClaims(uid, claims)
};

firebase-admin 라이브러리로 { group: role } 이라는 customUserClaim을 생성해주었다.

이것 자체로 권한으로써의 역할을 하진 않고 그저 사용자 설정 클레임일 뿐이다.

 

여기까지가 현재 진행 상태였고,

우리의 목표는 Spring에서 이 클레임을 사용자의 ROLE로 인식되게 하는 것이다.

 

 

 

 

 

 


3. JwtFilter 구현

    @PostMapping("")
    @PreAuthorize("hasAnyRole('admin', 'partner')")
    fun postEvent(@Valid @RequestBody req: PostEventRequest): ResponseEntity<Any> {
        println("postEvent")
        return ResponseEntity.status(201).body(object {
            val data = eventService.createOneEvent(req)
        })
    }

이렇게 컨트롤러에 @PreAuthorize 어노테이션을 통해서 사용자의 접근을 걸러낼 수 있어야한다.

저 컨트롤러로 들어가는 request에서 권한을 어떻게 추출하여 걸러낼 수 있을까?

 

  1. Firebase JWT에서 group을 추출하여 role을 얻어낸다.
  2. 이 role을 UserDetails 객체를 만들고 넣어준다.
  3. UserDetails 객체를 SecurityContextHolder에 등록해준다.

이 로직을 Filter로 구현하여 Filter를 등록해주자.

 

참고로, @PreAuthorize 안에서 다양한 표현식을 사용할 수 있다.

https://docs.spring.io/spring-security/site/docs/3.0.x/reference/el-access.html#el-common-built-in

 

15. Expression-Based Access Control

There are some built-in expressions which are specific to method security, which we have already seen in use above. The filterTarget and returnValue values are simple enough, but the use of the hasPermission() expression warrants a closer look. The Permiss

docs.spring.io

 

 

1. Firebase JWT로부터 Custom Claim 추출하기

val accessToken = request.getHeader("Authorization").split(" ")[1]

JWT 토큰이 Header에 Authorization: Bearer {jwt token} 형식으로 요청되어지기에 공백을 기준으로 뒷 덩어리를 가져온다.

 

 

 

2. JWT Token 검증하기

val decodedToken: FirebaseToken = FirebaseAuth.getInstance().verifyIdToken(accessToken)
val uid: String = decodedToken.uid
val role: String = decodedToken.claims["group"] as String

Firebase Admin를 통해서 verify한다.

시간이 만료되었거나 잘못된 토큰일 경우 FirebaseAuthException이나 IllegalArgumentException을 뱉는다.

 

 

 

3. SecurityContextHolder에 UserDetails 객체를 생성하여 등록하기

val userDetails: UserDetails =
    User.builder().username(uid).authorities(SimpleGrantedAuthority(role)).roles(role).password("").build()
val authentication = UsernamePasswordAuthenticationToken(
    userDetails, null, userDetails.authorities
)
SecurityContextHolder.getContext().authentication = authentication

 

 

위 3단계를 합치고 예외 몇 가지를 지정해준 JwtFilter 코드는 다음과 같다.

class JwtFilter: OncePerRequestFilter() {

    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        try {
            val accessToken =
                request.getHeader("Authorization").split(" ")[1]
            val decodedToken: FirebaseToken = FirebaseAuth.getInstance().verifyIdToken(accessToken)
            val uid: String = decodedToken.uid
            val role: String = decodedToken.claims["group"] as String
            val userDetails: UserDetails =
                User.builder().username(uid).authorities(SimpleGrantedAuthority(role)).roles(role).password("").build()
            val authentication = UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.authorities
            )
            SecurityContextHolder.getContext().authentication = authentication
        } catch (e: FirebaseAuthException) {
            response.status = HttpStatus.SC_UNAUTHORIZED
            response.contentType = "application/json"
            response.writer.write("{\"code\":\"INVALID_TOKEN\", \"message\":\"" + e.message + "\"}")
            return
        } catch (e: IllegalArgumentException) {
            response.status = HttpStatus.SC_UNAUTHORIZED
            response.contentType = "application/json"
            response.writer.write("{\"code\":\"INVALID_TOKEN\", \"message\":\"" + e.message + "\"}")
            return
        } catch (e: NoSuchElementException) {
            response.status = HttpStatus.SC_UNAUTHORIZED
            response.contentType = "application/json"
            response.writer.write("{\"code\":\"USER_NOT_FOUND\"}")
            return
        } catch (e: NullPointerException) {
            response.status = HttpStatus.SC_BAD_REQUEST
            response.contentType = "application/json"
            response.writer.write("{\"code\":\"AUTHORIZATION_HEADER_NOT_FOUND\"}")
            return
        } catch (e: Exception) {
            e.printStackTrace()
            return
        }
        filterChain.doFilter(request, response)
    }
}

 

 

 

 


4. Filter 등록

이렇게 구현한 Filter를 Spring Security의 Filter Chain에 등록해준다.

 

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
class WebSecurityConfig: WebSecurityConfigurerAdapter(){

    @Throws(java.lang.Exception::class)
    override fun configure(web: WebSecurity) {
        web.ignoring()
            .antMatchers("/")
            .antMatchers("/ping")
    }

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http
            .cors().disable()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/v1/**").authenticated().and()
            .addFilterBefore(
                JwtFilter(),
                UsernamePasswordAuthenticationFilter::class.java
            )
            .exceptionHandling()
            .authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
    }
}

 

@PreAuthorize가 동작하지 않으면 다음을 추가해주자

@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)

 


5. 동작확인

만료된 JWT Token으로 접근을 시도하면 401을 뱉으며 잘 쳐내는 것을 볼 수 있다.

 

권한에 맞지 않는 api에 접근을 시도하면 403을 뱉으며 잘 쳐내는 것을 볼 수 있다.

 

 

 

 

 

 

참고:

spring data jpa에서

 

No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer

 

에러가 발생했다.

 

간단하게 해석해보자면 serializer가 없다는 것인데,

serialize, 즉 직렬화란 간단히 말해서 Object를 연속된 String 데이터나 연속된 Bytes 데이터로 바꾸는 것을 의미한다.

역직렬화는 그 반대겠지?

 

Object는 메모리상에서 존재하고 추상적이다.

String 데이터나 Bytes 데이터는 드라이브에 저장될 수 있고 통신선을 통해 전송될 수 있다.

https://youtu.be/qrQZOPZmt0w

 

 

아무튼 다시 나는 스프링부트 + 코틀린을 사용하여 개발을 하고있었고,

JPA로 table을 findAll을 통해서 목록을 뽑아오는 기능을 구현하고있었다.

에러는 이 부분에서 발생했다.

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    var noticeCategory: NoticeCategory,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    var noticeBoard: NoticeBoard

 

JPA로 조회한 데이터는 메모리 상에 존재하는 Object다

이 Object를 Json 형태로 반환하기위해 직렬화를 해주어야하는데 

NoticeCategory와 NoticeBoard 컬럼이 LAZY로딩으로 설정되어있다.

 

이렇게 되면 조회한 Object를 serialize하려는 순간 실제 Object가 아닌

프록시로 감싸져있는 hibernateLazyInitializer를 serialize하게되므로 에러가 발생한다

 

 

 

해결

3가지 방법정도가 있다. 난 2번 방법을 사용했다.

1. @JsonIgnore로 반환시키지 않는다

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    @JsonIgnore
    var noticeCategory: NoticeCategory,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id")
    @JsonIgnore
    var noticeBoard: NoticeBoard

 

 

2. LAZY로딩 대신 EAGER로딩으로 설정해주면 된다

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "category_id")
    var noticeCategory: NoticeCategory,

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "board_id")
    var noticeBoard: NoticeBoard

 

3. application 파일에 spring.jackson.serialization.fail-on-empty-beans=false 설정해주면 된다

 

 

 

 

+

220328 추가사항

프록시 컬럼이 있는 엔티티를 그대로 반환하면 위에 보이듯 hibernateLazyInitializer를 직렬화하려다가 에러가났는데

ObjectMapper로 Entity -> dto로 변환을 시켜주니 LAZY로딩이 정상적으로 동작했다.

 

참고:

 

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring()
    	.antMatchers("/")
        .antMatchers("/ping");
}

@Override
public void configure(HttpSecurity http) throws Exception {
     http.csrf().disable()
        .authorizeRequests()
        .antMatchers("/v1/**").authenticated();
}

개발하며 위처럼 파라미터가 다른 두 configure를 재정의해서 사용할 일이 있었다.

 

 

HttpSecurity 패턴은 보안처리

WebSecurity 패턴은 보안예외처리 (정적리소스나 health check)

 

WebSecurity 패턴은 HttpSecurity패턴이 정의되어야 사용할 수 있다.

 

 

 

configure (WebSecurity)

  • antMatchers에 파라미터로 넘겨주는 endpoints는 Spring Security Filter Chain을 거치지 않기 때문에 '인증' , '인가' 서비스가 모두 적용되지 않는다.
  • 또한 Security Context를 설정하지 않고, Security Features(Secure headers, CSRF protecting 등)가 사용되지 않는다.
  • Cross-Site-Scripting(XSS), content-sniffing에 대한 endpoints 보호가 제공되지 않는다.
  • 일반적으로 로그인 페이지, public 페이지 등 인증, 인가 서비스가 필요하지 않은 endpoint에 사용한다.

 

configure (HttpSecurity)

  • antMatchers에 있는 endpoint에 대한 '인증'을 무시한다.
  • Security Filter Chain에서 요청에 접근할 수 있기 때문에(요청이 security filter chain 거침) 인증, 인가 서비스와 Secure headers, CSRF protection 등 같은 Security Features 또한 사용된다.
  • 취약점에 대한 보안이 필요할 경우 HttpSecurity 설정을 사용해야 한다.

 

 

 

 

참고:

더보기

참 오랜만에 글을 쓴다.... 분발하자


1. CEX란?

  • Centralized Exchange로 "중앙화거래소"를 의미한다
  • 우리가 많이 사용하는 업비트, 빗썸, 코인원과 같은 거래소

 

 

CEX의 장점

  • 암호화폐에 대해 잘 몰라도 쉽게 접근할 수있는 낮은 진입장벽
  • 거래 처리속도가 빠르다
  • 개인키를 거래소가 알아서 관리해준다
  • 거래량이 많다

 

 

CEX의 단점

  • 중앙 서버가 공격 당아혐 우리의 자산이 위험하다
  • 가두리 펌핑

 

 

 

CEX(중앙화 거래소)에서 코인을 사면 어떤 일이 벌어질까? 실제로 트랜잭션이 발생할까?

코인을 나의 소유로 만든다는 것은 블록체인 네트워크 상에서 나의 거래 트랜잭션이 기록되고 이것이 기록된 블록이 생성됨을 의미한다.

나는 그간 아무생각없이 업비트에서 코인을 사고 팔아만 왔다. 그럼 코인을 사고 팔때마다 네트워크상에 나의 거래 기록이 기록될까?

 

결과는 "아니다"

조금이라도 생각해보면 당연한 결과지만 그간 아무 생각없이 침팬치마냥 매수/매도 버튼만 눌러왔기에 신선한 충격을 받았다.

코인별로 블록이 생성되는 시간도 다 다르고 소모되는 비용도 천차만별이기에 업비트에서 일어나는 모든 거래를 트랜잭션으로 기록하는 것은 너무 낭비이며 비효율이다.

 

22년 1월 27일의 도지코인

하지만 이런 중앙화 거래소에서도 실제 트랜잭션이 일어나는 순간은 있다.

바로 외부 지갑으로(에서) 이동할 때 실제 트랜잭션이 발생한다.

 

중앙거래소에서의 코인 거래는 "원화 포인트와 가상자산 포인트간의 매매 or 교환 행위"로 이해하는 것이 일반적이라고 한다.

 

 

 

업비트에서 비트코인을 사고 외부 거래소 지갑(이를테면 바이낸스)로 옮기는 작업을 예시로 들어보겠다

1. 우리가 업비트에 8천만원을 입금한다. 8천만원은 업비트의 운영 법인으로 이전되고 우리는 업비트에 8천만원어치의 반환채권을 가지게 된 셈이다. 간단하게 8천만 포인트라고 해도 될 것 같다. 우리는 업비트에서 이 8천만 포인트로 비트코인 10개를 구입한다.

 

 

2. 우리가 8천만 포인트로 10비트코인을 구입했다는 기록은 업비트의 중앙서버에 기록된다.

우측의 거래소 BTC지갑은 업비트가 가지고있는 실제 비트코인이 있는 지갑이다. 이 지갑의 비트코인 수량에는 아무런 변화가 없음에 주목하라.

 

 

 

3. 우리는 이제 이 10비트코인을 나의 바이낸스 지갑으로 옮기고 싶어한다. 그러면 업비트의 비트코인 공용 지갑에서 업비트 개인 비트코인 지갑으로 전송이 일어나며 이 이동은 트랜잭션으로 기록된다.(트랜잭션 1회)

 

 

 

4. 이어서 업비트 개인 비트코인 지갑에서 외부 주소로 이동이 발생하고 이 또한 트랜잭션이 발생한다.(트랜잭션 2회)

그림은 하드웨어 개인 BTC 지갑이라고 나와있지만 아무튼 거래소 외부로의 이동임이 핵심이다.

 

 

이렇게 그림으로 살펴봤을때는 실제 트랜잭션이 총 2번 발생했다.

정말 그럴까?

마침 바이낸스 -> 업비트로 트론을 전송할 일이 있었기에 트론스캔을 통해 트랜잭션을 확인해 보았다.

 

 

1. 먼저 나의 업비트 트론지갑 주소는 다음과 같다. TTU~~~이다

 

 

2. 트론스캔을 통해 확인해보면 TAz -> TTU로 트랜잭션이 발생했음을 확인할 수 있다.

TTU는 나의 업비트 트론 지갑 주소이면 TAz는 대충 바이낸스쪽 나의 트론 지갑 주소임을 알 수 있다.

TTU -> TRQ로의 이동도 확인할 수 있는데 대충 나의 트론 개인지갑 -> 업비트 트론 공통지갑으로의 이동임을 추측해볼 수 있다.

 

바이낸스 -> 업비트 개인지갑으로 이동이 일어나고 2시간 뒤에 업비트 개인지갑 -> 업비트 공용지갑으로 트랜잭션이 일어남을 확인할 수 있다. 업비트측에서 유저 개인지갑 -> 공용지갑으로의 이동은 외부지갑에서의 이동과 상관 없이 발생하는가보다(뇌피셜)

 

 

 

 


2. DEX

  • Decentralized Exchange, 즉 탈중앙화 거래소다
  • 업비트, 빗썸처럼 중개자가 없다
  • 모든것은 스마트 컨트랙을 통해 P2P로 진행된다
  • 중앙 거래소의 기능을 스마트 컨트랙으로 대체한 것
  • 모든 거래가 자동화 되어있고 모든 거래는 블록체인 상에서 진행된다
  • 유니스왑, 스시스왑, 팬케이크 스왑 등이 있다

 

 

DEX 장점

  • 보안적으로 CEX보다 낫다.
  • 개인정보가 요구되지 않는다
  • 중앙거래소에 상장되지 않은 다양한 비상장 코인들이 있다

 

 

DEX 단점

  • 유동성이 필요하다
  • 실시간 거래 반영이 늦다
  • 고객센터가 없다 -> 모든 일을 내가 책임져야 한다
  • 중앙거래소보다 진입장벽이 높다

 

 

 

CEX, DEX 한 줄 비교

  • 집을 팔 때
  • CEX는 집을 공인중개사에게 완전히 넘기고 공인중개사가 파는 것
  • DEX는 공인중개사가 집을 살 사람만 찾아주고 내가 파는 것

CEX
DEX

 

 

 

 

 

 


AMM

  • DEX는 CEX처럼 오더북 기반으로 거래가 체결되지 않는다
  • AMM 프로그램 사용

AMM이란, 매수자와 매도자간의 거래를 이어주는 것을 탈중앙화 + 자동화 한 프로그램

 

 

AMM은 4가지 요소로 구성된다

  • 토큰 페어
  • 트레이더
  • 가격 결정 알고리즘
  • 유동성 공급자

 

 

유동성 공급자

  • 두가지 토큰에 대한 유동성을 풀에 공급하는 주체
  • 특정 토큰의 유동성이 없다면 거래를 할 수 없다(가격이 천정부지로 상승한다)

 

유동성 공급자에 대한 간단한 비유

-메-

우리가 흔히 아는 RPG게임에서 상점 시스템 상점주인으로부터 아이템을 사거나, 팔거나가 가능한 형태이다.

하지만 스카이림이라는 게임에서는 조금 다르다고 한다(이 게임을 해보진 않았다)

 

 

특이하게 상점주인이 가진 골드의 수량이 표시된다

메이플스토리에서는 우리가 아이템을 얼마나 많이 팔던 상점주인은 전부 사준다. 즉 상점 주인의 골드가 무한대였지만

스카이림은 상점주인이 가지고있는 골드가 제한되어 있기에 우리가 너무 비싼 아이템을 팔 수 없다.

 

 

이때 상점주인이 유동성 공급자의 역할을 한다고 볼 수 있다

  • 상점 주인은 아이템-골드 간의 거래 기회를 준다(유동성 공급자는 A토큰-B토큰간의 거래 기회를 준다)
  • 상인의 골드가 없다면 아이템을 팔 수 없다

 

 

 

가격결정 알고리즘

의 한 예시로 CPMM(Constant Product Market Maker)를 살펴보자

 

AMM 가격 결정 알고리즘의 한 종류

두 토큰 수량의 곱이 일정하게 유지되도록 가격을 경정하는 알고리즘

x와 y는 풀에 공급된 토큰의 수량이다

이더리움-오미세고 풀을 예시로 들어보자

 

 

풀에 10이더리움과 500오미세고가 들어있다(Exchange State1)

x * y = 5000이다

단순하게 생각했을때 1이더리움은 50오미세고의 가치를 가지는 것으로 보이기에 트레이더가 1이더리움을 풀에 넣는다면 50오미세고를 받게 될 것으로 예상된다

 

하지만 1이더리움을 넣는 순간 풀에 11개의 이더리움이 존재하므로 x * y = 5000이 유지되려면 오미세고는 454.5454...개가 되어야 한다. 따라서 트레이더는 500 - 454.5개의 오미세고를 받게된다. (약 45개) 

예상한 수량보다 적게 받은 이 상황을 슬리피지라고 부른다

 

 

 

 

참고:

더보기

https://www.coindeskkorea.com/news/articleView.html?idxno=70117

https://steemit.com/coinkorea/@bbkang/vs

 

중앙화거래소 vs 탈중앙화거래소 — Steemit

<중앙화 거래소의 문제점> 1.거래가 거래소 서버의 데이터베이스에서 이루어짐 2.거래소가 모든 기록과 거래(입출금, 매매)를 통제 3.개인지갑의 돈=거래소의 돈 (소유와 거래의 분리??) 현재 암호

steemit.com

https://bitkr.com/defi-kr/decentralized-exchanges/

 

탈중앙화 거래소(DEX)란 무엇이며 어떻게 운영되나요? | BitKR

탈중앙화 거래소는 DeFi 공간에서 '차세대 대물'로 선전되고 있습니다. 여기에서 작동 방식에 대한 모든 걸 읽어보세요.

bitkr.com

https://www.hankyung.com/economy/article/202110069443i

 

[김태린] 탈중앙화 거래소 떡상시대, DEX가 미래인가?

[출처: 셔터스톡] [스존의 존생각] 직장인 코인 투자자들에게는 눈을 붙이기도 힘든 나날의 연속이다. 재택근무가 조금씩 해제되기 시작했다. 연말정산 철도 다가왔다. ‘현금 빼고 다 간다’는

www.joongang.co.kr

https://medium.com/grabityorg/%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90%EC%A7%80%EA%B0%91-%EA%B0%9C%EC%9D%B8%EC%A7%80%EA%B0%91-vs-%EA%B1%B0%EB%9E%98%EC%86%8C%EC%A7%80%EA%B0%91-feat-%ED%94%8C%EB%9E%98%EB%8B%9B%EC%9B%94%EB%A0%9B-b7ddaf394815

 

[암호화폐지갑] 개인지갑 VS 거래소지갑 (feat. 플래닛월렛)

GBT Protocol 커뮤니티 여러분 안녕하

medium.com

https://steemit.com/blockchain/@sidonyia/qnp3p

 

[암호화화폐] 거래소에서의 코인 거래 원리 (장기 미접속 청산주의) — Steemit

거래소에서 코인을 원화로 구입하여 거래할때의 원리를 알아보고자 합니다. 우리가 거래소에서 코인을 구매할때 실제 코인을 구매하는것이 아닌 거래소에서 만든 자체 정보로만 거래를 하고

steemit.com

https://academy.binance.com/ko/articles/what-is-an-automated-market-maker-amm

 

자동화된 시장 메이커(AMM)란 무엇인가요? | Binance Academy

자동화된 시장 메이커는 탈중앙 금융(DeFi) 내의 스마트 콘트랙트이며, 누구나 유동성 풀에 암호화폐를 예치하고 시장을 형성할 수 있게 합니다.

academy.binance.com

https://medium.com/@aiden.p/uniswap-series-1-%EC%9C%A0%EB%8B%88%EC%8A%A4%EC%99%91-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-e321446623c7

 

[UNISWAP SERIES] 1. 유니스왑 이해하기

유니스왑과 AMM을 쉽게 이해해보자. Reviewed by 정현, 오영택

medium.com

 

블록체인에 대해 전반적인 그림을 그리기 위해 조사하며 얻은 얕은 지식입니다.


0. 코인말고 블록체인

암호화폐에 대한 글은 아니고 카카오의 블록체인 프로젝트 클레이튼으로써 클레이튼 네트워크 전반에 대해 얕게 조사해 본것을 정리하고자 한다.

이번 포스팅은 클레이튼 공식 문서에 있는 소개 동영상을 보고 정리한 내용이다.

https://ko.docs.klaytn.com/bapp/tutorials/bapp-on-baobab-video-lecture/1.-introduction

 

1. 소개 - Klaytn Docs KO

Baobab에서 BApp 개발하기(동영상 강의)

ko.docs.klaytn.com

 

 

 

 


1. 다른 블록체인 플랫폼들의 약점

  • Scalability(확장성)
  • Finality(확정성)
  • Fork(분기발생)

 

 

 

1. Scalability(확장성)

"얼마나 많은 일을 신속하게 처리할 수 있는지"

 

를 의미한다.

이때 TPS와 Block Interval의 개념이 나온다.

TPS란 Transaction Per Second로, 초당 몇개의 트랜잭션(거래)인지를 의미하고.

Block Interval은 블록 생성 주기를 의미한다.

 

  • 비트코인은 보통 7 TPS
  • 이더리움은 보통 15~20 TPS

반면,

  • VISA카드는 1700 TPS를 가진다.

 

또 Block Interval을 보면

  • 비트코인은 10분
  • 이더리움은 15~20초다.

 

다시말해

이더리움이 20 TPS에 15초의 인터벌을 가진다면 20 * 15 = 300 transactions라는 것을 알 수 있다.

이더리움으로 우리가 송금을 한다면, 이더리움 블록이 생성이 되어야 거래가 성사되기에 15초의 대기시간이 필요하다. 물론 비트코인의 10분에 비해서 이더리움의 15초는 많이 개선된 것이긴 하지만 여전히 너무 느리다.

 

왜 이렇게 느리지?

기존 평범한 중앙 서버 방식에서는 큰 request가 들어올 경우 많은 서버들이 request를 나누어 병렬적으로 처리를 하기에 서버의 개수를 증가시키면 처리 속도를 늘릴 수 있었다.

하지만 블록체인 네트워크에서는 노드의 수가 아무리 많다해도 일을 분산해서 처리하는 것이 아닌 모든 노드가 모든 일을 한다. 즉, 전체 네트워크에서 가장 낮은 퍼포먼스의 노드를 기준으로 전체 네트워크의 속도가 낮아지는 하향평준화가 발생한다.

 

하지만,

킹레이튼은 3000 TPS와 Block Interval 1초를 가진다.

 

 

 

2. Finality(확정성)

FInal이란 트랜잭션이 블록에 기록되고 그 블록을 더 이상 수정할 수 없는 상태를 의미한다.

Finality는 블록이 더 이상 변경될 수 없는 상태까지 소요되는 시간을 의미한다고 할 수 있다.

 

이전 포스팅에서 51%공격과 컨펌의 개념에 대해 살짝 다뤘는데 그 내용인 듯 하다.

  • 비트코인은 블록생성(채굴)까지 10분이 걸리고, 60분의 Finality 시간이 걸린다.(10분씩 6번의 검증)
  • 이더리움은 블록생성(채굴)까지 15초갸 걸리고, 6분의 Finality 시간이 걸린다.(15초씩 25번의 검증)

 

즉, 내가 만약 비트코인으로 송금을 했고, 10분이 지나서 나의 거래기록이 저장된 블록이 생성되어 체인에 붙었다고 해도,

그 거래를 확정하려면 그 뒤로 5개의 블록이 더 붙어야 한다는 것이다. 그 정도로 많은 블록이 추가로 붙은 체인이어야만 메인 체인으로 인정을 해 줄수 있기 때문이다.

거꾸로 말하면 비트코인은 60분동안은 내 거래내역이 무효화 될 수 있다는 뜻.

 

하지만,

갓레이튼은 Finality까지 1초면 충분하다.

 

 

 

3. fork(분기발생)

비트코인과 이더리움은 PoW 알고리즘으로 블록 생성 노드를 결정한다.

매우 복잡한 연산을 노드들끼리 경쟁적으로 하며 먼저 문제를 푼 노드가 블록을 생성하고, 주위 노드들로 자신이 문제를 풀었다고 전파를하는 원리인데,

 

만약 두개의 노드가 기가막힌 타이밍으로 동시에 문제를 푼다면?

그러면 블록체인에 분기가 발생한다.

 

하나의 예시 상황을 보자

  1. 내가 비트코인을 친구에게 전송했다.
  2. 내가 전송한 거래 기록은 트랜잭션 풀에 저장된다.
  3. 채굴 노드들은 트랜잭션 풀에서 거래 기록을 모으고 문제를 풀며 블록을 생성하기 위해 애쓴다.
  4. 이때 기막힌 타이밍에 A노드와 B노드가 동시에 문제를 풀어냈다.
  5. 따라서 A노드와 B노드는 동시에 자신이 문제를 풀었다는 사실을 주위 노드들에게 전파한다.
  6. 주위 노드들은 A노드에게 전파받은 노드는 A노드가 생성한 블록을 체인에 추가하고 다시 문제를 푼다.(B노드의 경우도 마찬가지) 이때 A노드와 B노드가 동시에 생성한 블록의 내용은 동일하다. 동일한 트랜잭션을 모았으니까.
  7. 그러면 현재 A노드가 생성한 #6-1블록이 체인 마지막에 붙은 줄 알고있는 노드가 있을 것이고, B노드가 생성한 #7블록이 체인 마지막에 붙은 줄 알고있는 노드가 있을 것이다.
  8. 이 상태로 노드들은 다시 열심히 채굴을 하고 이번에는 어떤 C노드가 문제를 풀어내어 블록 생성 권한을 가졌다고 해보자.
  9.  C노드는 B노드가 생성했던 #7블록을 마지막으로 하는 체인을 알고있는 노드라면 #7블록 뒤에 자신이 생성한 #8 블록을 붙인다. 그리고 이 사실은 모든 노드들에게 전파된다.
  10. #6-1 블록을 갖고있던 노드들은 #7블록이 없기에 자신들의 체인을 포기하고 #6 - #7 - #8 체인의 형태로 다시 채굴을 시작한다.

 

이런 흐름으로 PoW가 작동하는데 여기서 51%공격이 발생할 수 있다.

 

황레이튼은 이런 위험이 있는 PoW 방식 대신 IBFT 알고리즘을 사용하여 문제를 해결했다.

 

https://steemit.com/kr/@kanghamin/istanbul-byzantine-fault-tolerance

 

Istanbul Byzantine Fault Tolerance/이스탄불 비잔티움 장애 허용 — Steemit

Istanbul Byzantine Fault Tolerance(=IBFT)에서는 3단계로 합의과정이 이루어져있다. Pre-Prepare Prepare Commit 시스템은 잘못된 노드(나쁜 노드)가 F라고 가정했을때 총 노드수가 3F+1이상이면 돌아갈 수 있다. N =

steemit.com

 

 

 

 


2. 클레이튼의 합의 과정

참여 노드 수를 제한하여 성능을 높힌 BFT 알고리즘은 덜 분산적이고 투명성이 저하된다는 단점이 있다.

이 단점을 개선한 방식이 IBFT다. 이거에 대해선 따로 다시 공부를 해야한다...

 

IBFT의 경우 1명의 제안자(Proposal)다수의 위원회(Commit/Validator)로 구성된다.

제안자는 거버넌스 카운실(Governance Council) 노드들 중 랜덤으로 선정된 하나의 노드이며 이번 라운드는 자신이 선정다는 사실을 자신의 공개키를 통해 암호화하여 위원회 노드들에게 알린(전파한)다. (클레이튼의 블록 생성 주기를 라운드라고 한다.)

위원회 노드들의 경우도 자신이 합의 노드라는 것을 합의 노드들에게 공개키를 통해 암호화하여 알린다.

제안자는 합의 노드의 2/3의 서명을 받을 경우 블록을 생성하고 다른 위원회 노드들에게 전파한다.

 

 

 

 


3. 클레이튼 네트워크의 구조

CNN : consensus node network = 합의 노드 / 제한 노드

PNN : proxy node network = 블록을 앤드포인트 노드에게 전달하는 노드

ENN : Endpoint node network = 기타 노드 / 누구나 참여가능 노드

CN/PN/ENBootnode : 새로운 참가자의 노드를 등록하게 도와주는 노드

CNN + PNN : Core cell

출처: https://jeongbincom.tistory.com/88 [하나셋 - 프로그래머로써 살아남기]

CN노드들간의 합의 끝에 블록이 생성되고 CNN에서 PNN으로, PNN에서 EN들에게 전파된다.

 

 

왜 CN -> EN의 다이렉트 통신을 안하고 CN -> PN -> EN처럼 중간에 PN을 둘까?

위에 언급하였듯 블록체인 네트워크에서 커다란 request가 들어온다면 이 request를 여러 노드들이 분산하여 처리하는 것이 아닌 각각의 노드들이 모든 request를 처리해야한다. 따라서 네트워크의 성능은 그 네트워크에서 가장 낮은 성능의 노드에 맞춰진다.

 

즉, CN 노드들의 성능이 매우 중요한데, CN 노드들은 합의하는 것에만 최선을 다해도 모자란 판인데 EN들과 통신하는 것에 자원을 낭비해선 안된다. 따라서 CN 하나에 여러개의 EN을 연결하는 구조 대신, CN 하나와 여러개의 EN 사이에 PN을 두어 EN과 통신하는 측면에서 성능을 높힌것이다.

 

참고로 CN 노드가 되려면 

  • Physical core가 40개 이상
  • 256GB RAM
  • 14TB 이상 저장공간
  • 10G 네트워크

의 조건이 필요하다고 한다.

 

 

 

 


4. 서비스 체인

메인넷과 연결돼있고, 독립적으로 운영되는 체인이다.

  • Bapp이 특별한 노드 환경에서 수행되어야 할 때
  • 프라이빗 블록체인과 같이 보안 수준 맞춤형으로 설정해야 할 때
  • 많은 처리량을 요구하는 서비스라서 메인넷에서 사용하기엔 비용 부담이 될 때

서비스 체인을 사용한다.

 

메인체인과 서비스체인간 통신은 되지만 제한된 조건 하에서 통신한다.

요약하면 서비스 체인은 독립된 서비스 공간을 구축해서 필요할때 메인넷의 신뢰를 고정 시키는 것

참고로 클레이튼의 서비스 체인에서는 트랜잭션 가스비를 안받도록 설정할 수 있다.

 

 

 

 


5. 이더리움 VS 클레이튼

이더리움

  • 단일 네트워크
  • PoW방식이고, 어떤 노드가 블록을 생성하게 될지 모름
  • 따라서 최신 블록을 전파받기 위해서 노드는 다른 모든 노드들고 최대한 연결돼 있어야한다.

 

클레이튼

  • 2개의 레이어를 가지는 네트워크
  • 매 라운드마다 합의 노드들 중  하나가 뽑혀서 블록을 생성한다.
  • 이더리움과 달리 합의 노드들 중에서 블록이 생성되기에 어떤 노드에서 생성될지 거의 알 수 있다.
  • 따라서 최신 블록을 전파받기 위해서는 합의 노드들에게만 연결해 있으면 된다.
  • 개발자가 Bapp을 만들고 엔드포인트 노드에 접속한다. EN -> PN -> CN
  • 혹은 개발자의 Bapp 자체를 엔드포인트로 활용하여 네트워크에 접근할 수도 있다.
  • 메인넷과 부분적으로 소통하며 독립된 서비스 공간을 구축할 수 있는 서비스 체인이 있다.

 

 

 

 

 

참고:

더보기

https://media.fastcampus.co.kr/insight/why-blockchain-is-hard/

블록체인에 대해 전반적인 그림을 그리기 위해 조사하며 얻은 얕은 지식입니다.


1. DID란?

  • 탈중앙화 신원증명(Decentralized Identity)이다.
  • 개인 정보를 사용자 개인이 보관하여(개인 소유의 단말기 등에) 개인 정보 인증 시 필요한 정보들만 골라서 인증을 진행할 수 있도록 해주는 전자신원 증명기술이다.
  • 기존에는 기업이 사용자의 모든 개인 정보를 중앙집중형으로 관리하는 형태였지만, DID는 자신의 개인정보를 자신이 직접 관리하는 분산형태이다.
  • 사용자들의 개인 정보는 사용자들의 개인 단말기에 저장되고, 블록체인에 존재하는 DID 문서라는 것을 통해서 개인 정보를 제출하여 인증받을 수 있다.

 

 

 


2. 왜 DID?

기존 인증 시스템만으로도 세상은 잘 돌아갔다. 굳이 이런 어려운 기술을 사용하는 이유는 뭘까?

 

1. 개인정보 관리의 위험

https://www.thedailypost.kr/news/articleView.html?idxno=80609

 

 

 

"유저 데이터를 소유하는 것은 자산에서 부채가 되고있다"

-비탈릭 부테린-

 

 

이런 이슈를 DID는 해결해줄 수 있다.

  • 개인의 입장에선, 자신의 정보를 직접 자신이 직접 관리하기에 나의 개인 정보를 기업에 의존하지 않아도 되고,
  • 기업의 입장에선, 직접 관리했다가 유출사고가 발생할 수 있는 리스크를 줄일 수 있다.

그러면 DID 기술을 어떻게 구현되는지 살펴보자.

 

 

 

 


3. DID의 구성 요소

1. DID 아이디

DID 아이디는 DID 문서의 위치를 나타낸다. 콜론을 통해서 3등분으로 구분되는데 의미는 다음과 같다.

  • did: DID 스키마를 따를 것임을 의미
  • example: DID 메소드의 이름을 의미, DID 메소드란 특정 분산 원장 또는 네트워크에서 DID와 관련된 DID 문서들을 생성, 읽기, 갱신, 그리고 비활성화 하는 메커니즘이다. DID 메소드들은 여기에서 확인해볼 수 있다.
  • 123456789abcdefghi: DID 메소드 안의 고유 아이디를 의미한다.

 

 

2. DID 문서

DID문서는 DID아이디의 제어권, 소유권을 증명할 수 있는 공개키와 인증정보를 가지고있다. 블록체인에 올라가있으며 여기에는 민감한 개인정보는 포함돼 있지 않다.(블록체인에 보관하지 않아도 되지만 대부분 블록체인에 보관한다고 한다.)

  • id: 이 DID문서가 설명하고 있는 아이디를 의미한다.
  • publicKey: 아이디와 관련된 공개키 리스트.
  • authentication: 이 아이디의 소유권 증명을 위한 정보.
  • service: 이 아이디와 상호작용이 가능한 서비스들을 의미한다.

 

 

3. VC(Verifiable Credential)

블록체인에 올라가서 모두에게 보여지는 DID문서에는 당연히 사용자의 개인정보가 포함되면 안된다. 사용자의 민감한 정보들은 사용자가 소지하는 단말기(휴대전화 등)에 저장되며 이를 VC라고 한다. 

  • VC에는 이름, 나이, 주소, 백신접종여부등의 정보가 들어있다.
  • VC는 개인이 단말기의 지갑 애플리케이션과 같은 안전한 영역에서 보관한다.
  • 자신이 가진 모든 VC들을 인증을 위해 제출해야 하는것이 아닌, 원하는 VC들만 골라서 제출할 수 있다. 즉, 담배를 구입할 때 신분증을 제출하면 나의 이름과 주민등록번호와 주소지 등이 노출되지만, 성인여부에 대한 VC만 골라서 제출하면 다른 민감한 정보의 노출을 막을 수 있다.
  • VC의 단위는 Claim이라고 한다.

 

 

 

 


4. 인증 과정

모든 종류의 DID가 다음과 같은 인증 절차를 가지는 것은 아니기에 거시적인 관점에서만 DID발급, 사용, 검증으로 나누어서 살펴보도록 하자.

 

 

1. 발급하는 법

신원정보발행자가 어떤 유저의 클레임을 발급해주는 과정이다.

  1. 사용자는 신원정보발행자(이슈어)에게 클레임(인증하고자 하는 항목, 예를들어 백신접종여부) 발급을 요청한다.
  2. 공인 인증된 이슈어는 사용자의 클레임을 생성하고 클레임에 이슈어의 개인키로 서명을 한다.
  3. 그 후 이슈어는 블록체인에 존재하는 사용자의 DID문서에 이슈어의 공개키를 등록한다.
  4. 발급된 클레임을 사용자에게 반환한다.

이제 사용자는 어떤 권한에 대해 인증할 수 있는 하나의 클레임을 얻은 것이다.(성인 여부, 백신 접종여부 등)

 

 

 

2. 사용하는 법

사용자가 발급받은 클레임을 통해서 자신의 정보를 인증하는 과정이다.

  1. 사용자는 발급받은 클레임에 자신의 개인키로 서명을 한다. (클레임은 현재 사용자의 개인키로 1회, 이슈어의 개인키로 1회, 총 두번의 개인키 서명이 된 상태)
  2. 사용자의 공개키를 블록체인에 존재하는 자신의 DID 문서에 등록한다. (혹은 클레임과 함께 공개키도 직접 보낸다)
  3. 검증자에게 클레임을 제출한다.

여기서 검증자는 백신패스를 확인하는 음식점 사장님이라고 가정해보자.

 

 

 

3. 검증하는 법

  1. 음식점 사장님은 사용자의 클레임을 받아서 검증을 진행한다.
  2. 검증은 클레임에 걸린 두번의 개인키 서명을 풀어야한다(복호화)
  3. 먼저 사용자의 DID문서를 조회하여 사용자의 공개키를 획득하고(혹은 직접 받은 공개키를 획득하고),
  4. 사용자가 전달한 클레임에서 사용자가 개인키로 서명한 것을 복호화한다.
  5. 사용자의 개인키로 된 서명이 풀렸다는 것은 -> 인증 정보가 해당 사용자의 것이 맞다는 것을 의미한다.
  6. 다음으로, 사용자의 DID문서에 존재하는 이슈어의 공개키를 통해서 이슈어의 개인키로 서명한 것을 복호화한다.(이제 모든 서명이 풀렸다!)
  7. 이슈어의 공개키로 복호화가 되었다는 것은, 해당 이슈어가 발급한 클레임이 맞다는 것이고, 이는 사용자의 인증 정보가 신뢰할 수 있음을 의미한다.

 

 

 

 

 

 

참고:

더보기

https://media.fastcampus.co.kr/insight/why-blockchain-is-hard/

블록체인에 대해 전반적인 그림을 그리기 위해 조사하며 얻은 얕은 지식입니다.


1. 채굴이란?

채굴은 새로운 블록을 생성하는 것을 의미한다.

블록은 채굴 노드가 생성한다.

저번 포스팅에서 말했 듯, 트랜잭션들은 검증노드로부터 네트워크 여러곳으로 전파된다.

채굴노드는 네트워크에 공유되고 있는 트랜잭션들을 모아서 블록 만들기를 시도한다.

 

 

비트코인은 블록 한개당 1MB의 크기이다. 물론 모든 코인이 동일하지는 않고 어떤 블록체인을 사용하느냐에 따라 다르다.

 

 

비트코인은 블록 한개가 생성되는데 10분이 걸린다. 즉, 나의 거래내역이 블록에 포함되어 체인에 붙기까지 10분이 걸린다는 것이다. 또 컨펌의 과정을 위해서 추가로 6개의 블록이 이어져야한다.(비트코인의 경우)

컨펌이란 나의 트랜잭션이 포함된 블록이 블록체인의 메인 체인에 들어있다고 확정하는 것이다.

 

 

 

 


2. 어떤 채굴노드가 블록을 생성하는가?

채굴 노드들은 블록을 생성하기 위한 경쟁을 서로 한다.

모든 노드들이 블록을 생성하면 체인은 너무 많은 분기(갈래)가 발생할 것이다.

이 경쟁은 수학문제를 푸는 것이며, 정확히는 특정 해쉬값이 나오게 하는 임의의 논스(nonce)값을 먼저 찾는 경쟁이다.

이 경쟁에서 승리하면 블록을 생성할 수 있고 이 노드에게는 보상이 주어진다.

논스값을 찾는 것은 CPU보다는 GPU를 사용하는 것이 훨씬 빠르기에 그래픽카드를 사용한 채굴이 많은 것이다.

아무튼 이런 고생을 PoW 알고리즘이라고 하며 모든 블록체인이 이 방법을 사용하는 것은 아니다.

여러가지 채굴 알고리즘이 존재하는데, 다음 포스팅에서 공부해보겠다.

 

 

 

 


3. 동시에 블록이 만들어진다면?

경쟁을 하였음에도 동시에 블록을 생성하여 분기가 발생한다면?

 

6번 블록 이후 두개의 갈래로 나눠졌다.

이때 6-1블록과 7블록 중 어떤 블록쪽으로 계속 이어나가야 할까? 

이때 위에 언급한 확정(컨펌)의 개념이 나온다.

 

PoW알고리즘은 

 

"네트워크에 더 많은 기여를 한 사용자는 신뢰할 수 있는 사용자다"

 

라는 전제를 하고있다.

따라서 최초로 분기가 발생하고 그 뒤로 더 많은 블록들이 붙은 쪽이 더 많은 기여를 받은 분기이고 이 쪽을 메인 체인으로 인정한다. 블록들을 더 붙혀보고 많이 붙은 분기를 선택하는 이 과정이 위에서 언급한 컨펌(확정)의 과정이다.

인정받지 못한 분기의 블록들은 고아블록(스테일블록)이 된다.

 

 

 

 


4. 51%공격

블록을 수정하거나 조작하는 것은 불가능할까?

채굴하는 과정에서 GPU는 많은 전력을 소모한다. 논스값을 찾는 경쟁을 하기 때문인데,

왜 채굴하는데 이렇게 많은 전력을 쓰도록 설계한 것일까?

그냥 다음 블록을 생성할 노드를 랜덤으로 정하면 안되는 것일까?

 

이유는 블록체인 네트워크를 해킹으로부터 보호하기 위함이다.

블록 1개를 만드는데 많은 고생(채굴)을 했으니 이것을 수정하기 위해서는 그 이상의 고생(컴퓨팅 파워)를 하도록 만든 것이다. 또 블록 하나를 조작하면 그 블록의 해시값이 바뀌고 이로인해 연결된 블록들의 해시값도 바뀌기 때문에 사실상 블록체인 네트워크 전체 노드들의 고생보다 더 큰 고생이 필요하다.

 

하지만 그렇다해도 방법은 존재한다.

51% 공격이라고 불리는 수단인데, 과정은 다음과 같다.

 

 

 

1. A가 B에게 30코인을 줬다는 거래 내역이 101번 블록에 기록됐다.

 

 

2. A는 전체 네트워크의 51%이상의 컴퓨팅 파워를 지니고 있다고 가정한다.

3. A는 자기의 거래내역을 없애버린 새로운 블록을 만들고 그 뒤로 컨펌을 위한 블록들을 더 빠르게 생성한다.(자신이 가진 51% 이상의 압도적인 컴퓨팅 파워를 사용해서)

 

 

4. 이렇게 되면 A가 조작한 블록이 속한 분기가 메인 체인으로 인정된다. 

고로 A가 B에게 코인을 보낸 사실은 사라진다.

 

 

 

 


5. 51% 공격 대응

사실 이 공격은 공격자가 손해를 보는 모순을 가지고있다.

 

첫째로,

공격자가 이득을 취하기 위해서는 해당 암호화폐의 가치가 올라야한다.

하지만 이런 공격으로인해 해당 암호화폐에 대한 신뢰가 떨어지게 되고, 가치도 떨어진다.

그렇다면 51% 소유자가 가장 큰 손해를 보는 상황이 돼 버린다.

실제로 2014년 특정 마이닝풀이 일시적으로 코인의 해시파워가 51%를 초과하는 일이 발생했을 때, 가격이 폭락했다. 즉, 이론적으로 가능하나 경제적인 이유로 불가능한 공격이라는 것이다.

 

둘째로,

블록체인 네트워크의 참여자가 많을수록 51%의 해시파워를 확보하는 것은 매우 높은 비용이 필요하다.

따라서 해킹을 통해 얻는 수익보다 적자가 난다는 것이다.

하지만 참여자가 적은 네트워크는 51%의 파워를 확보하고 유지할 수도 있기 때문에 이 부분에서는 대안이 필요하다.

이것도 다음 포스팅에서 공부해보겠다.

 

 

 

 

 

참고:

더보기


1. Github Actions를 구성하는 요소

너무 잘 정리하신분의 글을 퍼왔다.

 

  • 1) Workflow
    • 여러 Job으로 구성되고, Event에 의해 트리거될 수 있는 자동화된 프로세스
    • 최상위 개념
    • Workflow 파일은 YAML으로 작성되고, Github Repository의 .github/workflows 폴더 아래에 저장됨
  • 2) Event
    • Workflow를 Trigger(실행)하는 특정 활동이나 규칙
    • 예를 들어 다음과 같은 상황을 사용할 수 있음
      • 특정 브랜치로 Push하거나
      • 특정 브랜치로 Pull Request하거나
      • 특정 시간대에 반복(Cron)
      • Webhook을 사용해 외부 이벤트를 통해 실행
    • 자세한 내용은 Events that trigger workflows 참고
  • 3) Job
    • Job은 여러 Step으로 구성되고, 가상 환경의 인스턴스에서 실행됨
    • 다른 Job에 의존 관계를 가질 수 있고, 독립적으로 병렬 실행도 가능함
  • 4) Step
    • Task들의 집합으로, 커맨드를 날리거나 action을 실행할 수 있음
  • 5) Action
    • Workflow의 가장 작은 블럭(smallest portable building block)
    • Job을 만들기 위해 Step들을 연결할 수 있음
    • 재사용이 가능한 컴포넌트
    • 개인적으로 만든 Action을 사용할 수도 있고, Marketplace에 있는 공용 Action을 사용할 수도 있음
  • 6) Runner
    • Gitbub Action Runner 어플리케이션이 설치된 머신으로, Workflow가 실행될 인스턴스
    • Github에서 호스팅해주는 Github-hosted runner와 직접 호스팅하는 Self-hosted runner로 나뉨
    • Github-hosted runner는 Azure의 Standard_DS2_v2로 vCPU 2, 메모리 7GB, 임시 스토리지 14GB

 

참고:

 

 

 

 


2. Beanstalk으로 배포하기

프로젝트 최상위에 .github/worfklows 폴더를 만들고 그 안에 .yml 스크립트로 깃헙 액션의 동작을 지정할 수 있다.

yml 파일 대부분은 처음 보더라도 대충 어떤 의미인지 알 수 있다.

 

name: Backend CI/CD

on:
  push:
    branches: [ develop ]

jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      # Download AWS CLI 2
      - name: Install AWS CLI 2
        run: |
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          which aws
          sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
          
      # Configure AWS credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # npm install for ci
      - run: npm ci

      # Build
      - run: npm run build

      # Unit test
      - run: npm run test:unit

  deploy:
    name: BeanStalk Deploy
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: ['14.x']
    needs: build
    steps:
      - uses: actions/checkout@v2

      # Initialize Node.js
      - name: Install Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      # Install project dependencies
      - name: Install dependencies
        run: npm install

      # Download AWS CLI 2
      - name: Install AWS CLI 2
        run: |
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          which aws
          sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
      # Configure AWS credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # Build
      - name: Run build
        run: npm run build

      # Make upload zip file
      - name: Generate deployment package
        run: zip -r deploy.zip . -x '*.git*' './aws/*' './node_modules/*' './dist/*' awscliv2.zip

      # Deploy to Elastic Beanstalk
      - name: Deploy to EB
        uses: einaregilsson/beanstalk-deploy@v18
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: ${{ secrets.APPLICATION_NAME }}
          environment_name: ${{ secrets.ENVIRONMENT_NAME }}
          region: ${{ secrets.AWS_REGION }}
          version_label: ${{github.SHA}}

위에서부터 살피며 내려가자

 

 

 

 

on

jobs를 실행시킬 트리거를 지정한다. 위의 코드처럼하면 develop 브랜치에 push가 발생했을때 github actions가 실행된다는 의미다.

다음처럼 여러개의 트리거를 지정할 수 있다.

  on:
    push:
      branches: [ master ]
    pull_request:
      branches: [ master ]

 

 

 

 

jobs

jobs는 Build와 Deploy로 구성된다. 그리고 각각 name을 지정한 것을 볼 수 있다.

 

 

 

 

Configure AWS Credentials

      # Configure AWS credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

코드 내부에 aws를 사용한 부분이 다수 존재하기에 runner에 aws cli를 설치해 주어야한다.

${{ secrets.블라블라 }}

이 부분은 깃허브 - Settings - Secrets 탭에서 Name-Value를 지정할 수 있다.

 

 

${{ secrets.블라블라 }} 에서 블라블라에 Name을 입력해주면 대응되는 Value를 사용할 수 있다.

 

 

 

 

npm ci

이거는 npm install과 같은 역할이다.

다만,

ci 과정에서 쓰는 install이다.

거의 두배 이상 빠른 속도를 보여준다.

 

npm install은 package.json 내부의 dependencies와 devDependencies를 기준으로 패키지 파일을 설치하는 반면

npm ci는 package-lock.json의 lockfile을 기준으로 패키지를 설치한다.

 

이렇게 되면 package.json내의 파일과 package-lock.json 내의 버전 등이 다를 경우,

package-lock.json을 기준으로 package.json 파일을 수정하며, 명시되지 않은 부분에서는 오류를 발생시키므로 Application 관리에 있어서 안정성을 확보할 수 있다.

 

 

 

 

npm run build

# Build
- run: npm run build

package.json에 build 스크립트를 실행한다.

이때 한가지 사용한것이 

 

 

npm install concurrently --save

이거다.

 

 

 

빌드시 .env파일을 비롯한 여러 설정파일들이 존재해야하는데,

이런 중요파일들은 S3에서 다운로드해야만 한다.

따라서 build 스크립트 이전에 수행되는 prebuild스크립트에서 중요 파일들을 미리 다운 받는 행동을 먼저 해야한다.

 

그런데 중요 파일들이 2개 이상일 경우 prebuild 안에서 동시에 스크립트가 수행될 수 있도록 해주는 concurrently를 사용했다.

 

"scripts": {
	...
    "prebuild": "concurrently \"aws s3 cp s3://blahblah .env\" \"aws s3 cp s3://blahblah somethingimportant.json \"",
    "build": "tsc",
    ...
  },

이렇게 concurrently 키워드를 쓰고 ""로 묶어서 스크립트를 넣어주면 된다.

 

 

 

 

deploy - Make upload zip file

# Make upload zip file
- name: Generate deployment package
  run: zip -r deploy.zip . -x '*.git*' './aws/*' './node_modules/*' './dist/*' awscliv2.zip

Beanstalk에 올리기위한 zip 파일을 생성한다.

zip 명령어로 압축파일을 생성하고 다음 step에서 S3로 업로드하고, Beanstalk에 배포된다.

 

 

이때, 압축파일에 포함시키지 않을 파일들을 -x 옵션으로 지정하는데,

node_modules를 지정하지 않았더니 문제가 생겼었다. (node_modules를 zip에 포함시켰더니)

 

 

어자피 .ebextensions에 npm install 키워드가 있기에 node_modules를 zip에 안넣어도 되지만,

어자피 덮어쓰기가 될 줄 알고 딱히 신경쓰지 않았었다.

그랬더니 안됏따아아아ㅏㅏ

정확히는 

 ./lib/cli

를 찾을 수 없다고 에러가 발생했다. 저게 뭔지 모르겠다.

* .ebextensions란 Beanstalk 구성파일로, Beanstalk에 배포될 때 최초에 실행되는 커맨드라고 보면된다.

 

 

우연히 node_modules를 제외시키면서 문제가 해결되긴 했는데 왜 내부에서 충돌이 난것인지는 모르겠다.

혹시 명쾌한 답을 아신다면 댓글 부탁드립니다.

 

 

 

 


삽질한 점

  • ci때와 cd때 둘다 aws cli를 설치해야한다.
  • zip할때 node_modules를 빼줘야한다.

 

 

 

참고:

더보기

'DevOps' 카테고리의 다른 글

[docker] 도커 초기화하기  (0) 2023.04.20
[DevOps] CI/CD 파이프라인이란  (0) 2021.11.17
[DevOps] 도커(docker)란?  (0) 2021.10.09

+ Recent posts