// 에드센스

nodejs로 개발을 하던 중 데이터베이스 테이블에 컬럼을 추가할 일이 있었다.

그때 사용한 방법을 기록해볼까 한다.


1. 데이터베이스 마이그레이션?

 

 

어디서 많이 들어보긴했지만 정확한 의미는 잘 몰랐다.

우선 마이그레이션이란 큰 의미로

"한 운영환경으로부터 다른 운영환경으로 옮기는 작업"

 

이라고 할 수 있다. Migration을 사전에 검색하면 나오는 '이주, 이송'의 개념이다.

하드웨어, 소프트웨어, 네트워크 등 넓은 범위에서 마이그레이션의 개념이 사용되고 있다. 

 

 

데이터베이스에서 한 예시로,

개발환경에서 스키마와 운영환경에서 스키마에 차이가 있을때 마이그레이션을 진행한다고 할 수 있다.

작게는 테이블 생성과 수정부터 하나의 애플리케이션 혹은 전체 시스템을 옮기는 것까지를 마이그레이션이라고 한다.

나는 데이터베이스에서 한 테이블에 컬럼을 추가하고자 한다.

 

 

 

 


2. 실습

현재 Nodejs에서 Sequelize 라이브러리로 데이터베이스를 조작하고 있다.

개발단계에서는 sync({force: true})를 통해서 컬럼을 강제로 추가할 순 있겠지만 운영단계에서는 불가능하다.

따라서 Sequelize에서 지원하는 마이그레이션으로 컬럼을 추가해볼까 한다.

 

 

0. Sequelize CLI 설치

# using npm
npm install --save-dev sequelize-cli
# using yarn
yarn add sequelize-cli --dev

 

 

 

1. init 명령

# using npm
npx sequelize-cli init
# using yarn
yarn sequelize-cli init

init 명령을 실행하면

  • config, 데이터베이스에 연결하는 방법을 CLI에 알려주는 구성 파일이 포함돼 있다.
  • models, 프로젝트의 모든 모델을 포함한다.
  • migrations, 모든 마이그레이션 파일 포함한다.
  • seeders, 모든 시드 파일을 포함한다.

이 4개의 폴더가 생성된다.

 

 

 

2. 데이터베이스 설정

config/config.json 파일에서 각 환경별로 나의 데이터베이스 정보를 입력해두어야 한다.

{
  "development": {
    "username": "mydb",
    "password": "mydb-password",
    "database": "postgres",
    "host": "mydb-dev.1234.ap-northeast-2.rds.amazonaws.com",
    "post": 5432,
    "dialect": "postgres"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

개발환경만 설정했다.

Sequelize CLI는 기본값이 mysql로 설정돼있다. 다른 데이터베이스를 사용하는 경우 "dialect"옵션의 내용을 수정해줘야한다.

또 Sequelize는 각 데이터베이스에 대한 포트로 기본값들을 사용한다.(postgres의 경우 5432포트)

다른 포트를 지정해야 하는 경우 "port"필드를 추가하여 설정할 수 있다.

 

 

 

3. 마이그레이션 파일 생성

sequelize migration:create --name test-migration

이 명령어로 마이그레이션 설정 파일을 생성한다. --name 옵션으로 파일의 이름을 지정한다.

그러면 migration 폴더에 20220318184913-test-migration.js 이라는 파일이 생성될 것이다.(날짜-이름.js)

 

 

이렇게 생긴 파일이다.

"use strict"

module.exports = {
  up: function (queryInterface, Sequelize) {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
  },

  down: function (queryInterface, Sequelize) {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.dropTable('users');
    */
  },
}

up 메소드에는 마이그레이션 할 내용을 기재하고 

down 메소드에는 롤백할 내용을 기재한다.

 

 

컬럼을 추가하는 up메서드를 작성해보자.

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.addColumn('product', 'url', {
      type: Sequelize.STRING,
      allowNull: false,
      defaultValue: 'https://myProduct.s3.ap-northeast-2.amazonaws.com/default.png',
    });
  },

  down: async (queryInterface, Sequelize) => {
    return queryInterface.removeColumn("product", "url")
  },
};

up 메소드에는 product테이블에 상품의 사진을 저장하는 url 컬럼을 추가한다.

down 메소드에는 up 메소드에 대한 롤백 내용을 기재해보았다.

 

 

 

4. 마이그레이션 진행

sequelize db:migrate --env development

를 통해서 up 메소드에 기재한 마이그레이션을 실행할 수 있다.

--env는 default로 development로 설정돼있긴하다.

 

 

 

5. 롤백

롤백은 이렇게 해주면 된다.

sequelize db:migrate:undo --env development

 

 

 

6. 다중 마이그레이션 

해보진 않았지만 잘 정리된 블로그의 내용을 참고했다.

 

만약 컬럼을 여러개 추가할 때는 어떻게 해야할까.

up() 함수는 프라미스를 리턴하게 되어있는데 promise로 구성된 배열을 반환해도 된다.

컬럼을 여러개 추가할 것이라면 addColumn()을 배열에 담아 리턴하면 된다.

물론 롤백할 때도 동일하게 removeColumn()를 배열에 담아서 반환한다. 

module.exports = {
  up: function (queryInterface, Sequelize) {
    return [
      queryInterface.addColumn("product", "url", {
        type: Sequelize.STRING,
      }),
      queryInterface.addColumn("product", "price", {
        type: Sequelize.STRING,
      }),
    ]
  },

  down: function (queryInterface, Sequelize) {
    return [
      queryInterface.removeColumn("product", "url"),
      queryInterface.removeColumn("product", "price"),
    ]
  },
}

물론 직접 raw 쿼리를 사용할 수도 있다.

module.exports = {
  up: function (queryInterface, Sequelize) {
    var sql = "ALTER TABLE product ADD COLUMN url varchar(255) NOT NULL"

    return queryInterface.sequelize.query(sql, {
      type: Sequelize.QueryTypes.RAW,
    })
  },

  down: function (queryInterface, Sequelize) {
    var sql = "ALTER TABLE product DROP COLUMN url"

    return queryInterface.sequelize.query(sql, {
      type: Sequelize.QueryTypes.RAW,
    })
  },
}

 

 

 

 

 

 

 

참고:

더보기

최근 코틀린을 처음으로 사용해보고 있다.

처음 언어를 배울때 일단 부딪혀가며 습득하자는 생각이라 주먹구구식으로 일단은 개발했던것 같다.

그 과정에서 정리가 필요한 부분들을 느꼈고 간단하게나마 적어보려한다.

 


1. 기본생성자

코틀린 클래스는 다음과 같이 생성한다.

class TestClass {
}

이 클래스는 생성자도 프로퍼티(얘도 상당히 중요한 개념인것 같다. 다음 글에서 정리해봐야겠다)도 없는 클래스다

여기에 기본생성자를 추가해보자면,

 

 

 

 

class TestClass(name: String, age: Int){
}

이런 모양이 된다. 이름과 나이를 초기화할 수 있는 기본생성자를 추가해보았다.

 

 

 

 

만약, 자바였다면 다음과 같았을 것이다.

class TestClass {    
        String name;
        int age;
    
        // 생성자
        public TestClass(String n1, int n2) {
                this.name = n1;
                this.age = n2;
        }
}

 

코틀린이 훨씬 간결하다.

 

 

 


2. init

바로 위의 코드처럼 자바에서는 생성자에서 바로 멤버변수들을 초기화 해줄 수 있다.

하지만 코틀린에서는 그러지 못하고 다음과 같은 방법을 사용할 수 있다.

class TestClass(name: String, age: Int){
    val name: String = "kim"
    var age: Int = 0
    init {
        this.age = age
    }
}

init은 기본생성자 호출이후 바로 다음에 호출되는 키워드이다.

 

 

 

하지만 우린 굳이 init 안쓰고 받아온 매개변수를 사용할 수 있다.

class TestClass(var name: String, val age: Int){
    fun introduce() {
        println("Hi! I'm $name and I'm $age years old")
    }
}

기본생성자의 매개변수 옆에 var과 val가 슬쩍 생긴것을 볼 수 있다.

얘네들을 붙혀주면 생성자의 매개변수를 클래스 내부에서 사용할 수 있다.

 

 

 

 


3. 보조생성자 constructor()

클래스 이름 옆에서만 생성자를 만들 수 있는걸까?

생성자를 여러개 만들고 싶을때는 어떻게 하면될까..

 

우리는 constructor 키워드를 사용할 수 있다.

class TestClass{
    var name = ""
    var age = 0
    constructor(name: String, age: Int){
        this.name = name
        this.age = age
    }
}

참고로 var name = ""에서 자동으로 String으로 타입추론이 되기에 :String이라고 명시해주지 않아도 된다.

 

 

하지만 이렇게 constructor로 생성자를 선언해주면 똑똑한 인텔리제이에서는 이러지 말라고 권유한다.

"Convert to primary constructor"를 누르면

 

 

이렇게 다시 정리가 된다.

 

 

아무튼 constructor 키워드로 생성자를 여러개 선언해 줄 수 있다.

class TestClass{
    var name = ""
    var age = 0
    
    constructor(name: String, age: Int){
        this.name = name
        this.age = age
    }

    constructor(age: Int){
        this.age = age
    }

    constructor(name: String){
        this.name = name
    }
}

 

 

하지만,

constructor 키워드를 사용할 때 한가지 주의할 것이 있다.

기본 생성자를 선언하고 constructor를 사용하면 다음과 같이 에러가 발생한다.

 

 

인텔리제이가 하라는 대로 this()를 추가해주면 에러는 사라진다.

왜 이럴까?

 

 

 

constructor로부터 생성된 생성자는 기본 생성자를 상속받아야 한다.

그렇기 때문에 기본 생성자를 상속받고 난 이후에는 에러가 사라진 것이다.

물론 기본 생성자를 상속 받는 것이니 constructor로 만든 생성자들은 반드시 기본 생성자가 갖고 있는 인자들을 갖고 있어야 한다.

 

 

 

이 경우에는 기본생성자에 name이 있지만 보조생성자에는 없다.

this()에 name을 넣어주도록 하자

 

 

 

에러 해결

 

 

 

기본생성자에는 존재하는 name이 보조생성자에는 없기에 에러가 발생한 모습이다.

 

 

 

생성자들의 호출 흐름을 보자면 다음과 같다.

https://jaejong.tistory.com/50

 

 

 

 

참고:

더보기


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. 이슈어의 공개키로 복호화가 되었다는 것은, 해당 이슈어가 발급한 클레임이 맞다는 것이고, 이는 사용자의 인증 정보가 신뢰할 수 있음을 의미한다.

 

 

 

 

 

 

참고:

더보기

+ Recent posts