React/Next.js 환경을 선택했고, 폴더구조는 다음처럼 잡았다(frontend-service부분 주목)

(coin) sevity@raspberrypi:~/workspace/online_judge $ tree -L 2
.
├── auth-service
│   ├── application.log
│   ├── cookies.txt
│   ├── HELP.md
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   ├── src
│   └── target
├── frontend-service
│   ├── jsconfig.json
│   ├── next.config.js
│   ├── node_modules
│   ├── package.json
│   ├── package-lock.json
│   ├── public
│   ├── README.md
│   └── src
├── install
│   └── auth-service.zip
└── README.md

8 directories, 13 files

한가지 주목할만한 점은 프론트엔드의 경우 스프링부트 서비스로 만들 필요가 없다는 점이었다.

 

다음 명령어를 통해 node관련 설치(apt-get으로 시스템전역에 node를 설치하지 않고 nvm을 사용하기로 함)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
. ~/.bashrc
nvm install node
nvm install 16.8.0

다음명령을 통해 next.js react 설치

npm install --save next react react-dom

 

 

다음 명령어를 통해 Next.js 앱을 생성

(coin) sevity@raspberrypi:~/workspace/online_judge/frontend-service $ npx create-next-app@latest .
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
Creating a new Next.js app in /home/sevity/workspace/online_judge/frontend-service.

Using npm.

Initializing project with template: app


Installing dependencies:
- react
- react-dom
- next


added 22 packages, and audited 23 packages in 41s

4 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Success! Created frontend-service at /home/sevity/workspace/online_judge/frontend-service

 

bootstrap을 통한 디자인 적용

먼저 아래 명령을 통해 프로젝트에 bootstrap을 설치

npm install bootstrap

 Next.js 프로젝트에서 Bootstrap을 사용하려면, _app.js 파일에 Bootstrap CSS를 import해야함.

_app.js 파일을 만들고, 다음 코드를 추가(맨위가 디자인 커스텀이며 나머지 내용은 _app.js를 만든이상 필수로 넣어야함)

_app.js는  모든 페이지에 공통으로 적용되는 컴포넌트를 정의하는 곳으로 전역 상태 관리, 레이아웃, 스타일링, 인증 등과 같이 여러 페이지에서 공유되는 로직을 처리하는 데 사용됨.

import 'bootstrap/dist/css/bootstrap.css'
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

login.js를 만들고 로그인 정보는 브라우저의 local storage를 활용해서 다음과 같이 저장했다.

import { useState, useEffect } from 'react';
import axios from 'axios';

export default function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userInfo, setUserInfo] = useState(null);

  // 페이지 로드 시 Local Storage에서 로그인 정보 불러오기
  useEffect(() => {
    const storedUserInfo = localStorage.getItem('userInfo');
    const storedIsLoggedIn = localStorage.getItem('isLoggedIn');

    if (storedUserInfo && storedIsLoggedIn === 'true') {
      setIsLoggedIn(true);
      setUserInfo(JSON.parse(storedUserInfo));
    }
  }, []);

  const handleSubmit = async (event) => {
    event.preventDefault();

    try {
      const response = await axios.post('http://sevity.com:9991/login', `username=${username}&password=${password}`, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      });

      // 서버에서 받은 세션정보를 local storage에 저장
      console.log('Login successful:', response.data);
      setIsLoggedIn(true);
      setUserInfo(response.data);

      // Local Storage에 사용자 정보 저장
      localStorage.setItem('userInfo', JSON.stringify(response.data));
      localStorage.setItem('isLoggedIn', 'true');
    } catch (error) {
      console.error('Login error:', error);
    }
  };
...

  if (isLoggedIn) {
    return (
      <div>
        Welcome, {userInfo}!
        <button onClick={handleLogout}>Logout</button>
      </div>
    );
  }
...
}
반응형

라즈베리파이 환경에서..

일단 DB는 postgreSQL로 세팅했고, DB스키마는 다음으로 설정했다.

-- 데이터베이스 생성
CREATE DATABASE online_judge;

-- 사용자 생성
CREATE USER online_judge_admin WITH ENCRYPTED PASSWORD '****';
GRANT ALL PRIVILEGES ON DATABASE online_judge TO online_judge_admin;

-- 데이터베이스 접속
\c online_judge;

-- 문제 테이블 생성
CREATE TABLE problems (
  id SERIAL PRIMARY KEY,
  title VARCHAR(100) NOT NULL,
  description TEXT NOT NULL,
  example_input TEXT NOT NULL,
  example_output TEXT NOT NULL
);

-- 사용자 테이블 생성
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  password VARCHAR(100) NOT NULL,
  name VARCHAR(50)
);

-- 제출 테이블 생성
CREATE TABLE submissions (
  id SERIAL PRIMARY KEY,
  problem_id INT NOT NULL,
  user_id INT NOT NULL,
  code TEXT NOT NULL,
  result TEXT NOT NULL,
  FOREIGN KEY (problem_id) REFERENCES problems (id),
  FOREIGN KEY (user_id) REFERENCES users (id)
);

SpringBoot Service는 다음과 같이 7개로 잡아 보았다. (진행하면서 변경가능, 링크를 누르면 각 서비스에 대한 블로그로 이동가능)

  1. 인증 서비스 (Backend): 사용자의 회원 가입, 로그인, 로그아웃, 세션 관리 등을 담당
  2. 인증 서비스 (Frontend): 사용자 인터페이스를 제공 (로그인 폼, 회원가입 폼 등)
  3. 문제 관리 서비스 (Backend): 문제의 추가, 삭제, 수정 등을 관리
  4. 문제 관리 서비스 (Frontend): 문제를 보여주고, 문제 추가, 삭제, 수정 등의 인터페이스를 제공
  5. 제출 관리 서비스 (Backend): 사용자의 코드 제출 및 제출 기록을 관리
  6. 제출 관리 서비스 (Frontend): 코드 제출 인터페이스와 제출 기록 확인 인터페이스를 제공
  7. 채점 서비스 (Backend): 제출된 코드를 채점

이 중 1번 인증 서비스부터 Spring Boot Initializr를 통해 프로젝트를 생성해보자.

위에서 생성된 zip파일을 ~/workspace/online-judge/auth-service에 풀어주고, github에 등록

 

VSCode에서 Spring환경세팅하고 ssh-remote설정해서 서버와 연동함

 

src/main/resources/appliction.properties에 다음 내용을 설정하여 PostgreSQL과 연결

Spring Boot의 일반적인 패키지(폴더) 구조는 아래와 같다.

Spring Boot 프로젝트 구조는 대체로 다음과 같습니다:

com.companyname.projectname : 이는 프로젝트의 메인 패키지입니다. 이 패키지 아래에는 Spring Boot Application 클래스와 기타 구성 클래스가 위치합니다.
com.companyname.projectname.controller : 이 패키지에는 모든 컨트롤러 클래스가 위치합니다. 컨트롤러 클래스는 HTTP 요청을 처리하고 응답을 반환하는 역할을 합니다.
com.companyname.projectname.service : 이 패키지에는 모든 서비스 클래스가 위치합니다. 서비스 클래스는 비즈니스 로직을 수행하는 역할을 합니다.
com.companyname.projectname.repository : 이 패키지에는 모든 레포지토리 클래스가 위치합니다. 레포지토리 클래스는 데이터베이스와 상호작용하는 역할을 합니다.
com.companyname.projectname.model or com.companyname.projectname.entity : 이 패키지에는 모든 모델 또는 엔티티 클래스가 위치합니다. 이러한 클래스는 데이터베이스 테이블을 표현합니다.
com.companyname.projectname.config : 이 패키지에는 모든 구성 클래스가 위치합니다. 구성 클래스는 프로젝트의 구성을 담당합니다.
com.companyname.projectname.exception : 이 패키지에는 사용자 정의 예외 클래스가 위치합니다.

나는 여기서 repository와 entity를 DDD에 따라 domain으로 합쳤다.

 

이제 도메인모델설정을 진행해보자.

domain 패키지(폴더) 만들고 그안에 User.java를 만들어서 users DB 테이블에 대응하는 엔티티 클래스작성

package com.yourcompany.authservice.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {

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

    private String username;
    private String password;
	private String name;
    // getters and setters

}

 

UserRepository 인터페이스를 작성하여 DB와 연동하는 동작을 작성

package com.yourcompany.authservice.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);

}

 

이제 Spring Security를 통해 사용자등록, 로그인, 로그아웃을 처리하는 컨트롤러와 서비스를 작성해보자.

1. Spring Security 설정

다음과 같은 SecurityConfig.java를 작성

@EnableWebSecurity 어노테이션을 붙여서 Spring Boot에게 이 클래스가 보안 설정을 담당하는 클래스임을 알림.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/register", "/login").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessURL("/home")
                .permitAll()
            .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

 

 

다음 단계는 UserDetailsService와 UserDetails를 구현(주석참조)

//UserDetails는 스프링에서 정의한 인터페이스이며 이파일은 그 구현체입니다.
//도메인 모델 클래스인 User와 UserDetails 인터페이스를 구현한 UserDetailsImpl(이파일)은 서로 다른 목적을 가지고 있습니다. 
//User는 데이터베이스의 users 테이블과 매핑되어 데이터베이스에서 사용자 정보를 조회하는 데 사용되며, 
//UserDetailsImpl은 Spring Security에서 사용자 인증 정보를 관리하는 데 사용됩니다.
//그렇다면 왜 UserDetails 인터페이스를 구현하는 클래스를 따로 만들 필요가 있을까요? 이는 Spring Security의 유연성 때문입니다. 
//Spring Security는 다양한 인증 방식과 사용자 정보 형태를 지원하기 위해 UserDetails 인터페이스를 제공합니다. 
//이를 통해 개발자는 자신의 애플리케이션에 맞는 사용자 인증 정보 형태를 자유롭게 구현할 수 있습니다.
package com.sevity.authservice.service;

import com.sevity.authservice.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Arrays;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {
    
    private User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("USER"));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
package com.sevity.authservice.service;

import com.sevity.authservice.domain.User;
import com.sevity.authservice.domain.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    private UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
}

 

다음 단계는 사용자 등록, 로그인, 로그아웃을 처리하는 서비스와 컨트롤러를 작성하는 것입니다.

//이 인터페이스는 사용자 등록과 로그인 기능을 정의합니다.
package com.sevity.authservice.service;

import com.sevity.authservice.domain.User;
import com.sevity.authservice.dto.UserRegistrationDto;

public interface AuthService {
    User register(UserRegistrationDto registrationDto);
    User login(String username, String password);
}

 

그 다음 AuthService 인터페이스의 구현체인 AuthServiceImpl 클래스를 작성합니다. 이 클래스는 AuthService 인터페이스의 메소드를 구현하며, UserRepository와 PasswordEncoder를 주입받아 사용합니다.

//AuthService 인터페이스의 구현체인 AuthServiceImpl 클래스를 작성합니다. 
//이 클래스는 AuthService 인터페이스의 메소드를 구현하며, UserRepository와 PasswordEncoder를 주입받아 사용합니다.
//웹에 접속한 사용자가 userid/password를 서버에 전달하면 db랑 매칭시켜서 등록하거나, 로그인검증하는 역할을 한다.
package com.sevity.authservice.service;

import com.sevity.authservice.domain.User;
import com.sevity.authservice.domain.UserRepository;
import com.sevity.authservice.dto.UserRegistrationDto;
import com.sevity.authservice.exception.UsernameAlreadyExistsException;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthServiceImpl implements AuthService {

    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;

    public AuthServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }


    @Override
    public User register(UserRegistrationDto registrationDto) {
        if (userRepository.existsByUsername(registrationDto.getUsername())) {
            throw new UsernameAlreadyExistsException("Username already exists: " + registrationDto.getUsername());
        }
        User user = new User();
        user.setUsername(registrationDto.getUsername());
        user.setPassword(passwordEncoder.encode(registrationDto.getPassword()));
        return userRepository.save(user);
    }


    @Override
    public User login(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user != null && passwordEncoder.matches(password, user.getPassword())) {
            return user;
        } else {
            return null;
        }
    }
}

 

AuthController 컨트롤러 생성(컨트롤러는 처음 등장했는데 모델(서비스)과 뷰를 연결하는 역할을 한다)
이제 클라이언트의 요청을 처리할 AuthController를 작성합니다. 

이 컨트롤러는 AuthService를 주입받아 사용하며, 사용자 등록과 로그인 요청을 처리합니다.

AuthController를 부르는 것은 스프링 프레임워크이며 웹으로 사용자요청이 왔을때 부른다.(@RestController)

//이 Rest컨트롤러는 AuthService를 주입받아 사용하며, 클라이언트의 요청을 받아, 사용자 등록과 로그인을 처리합니다.

package com.sevity.authservice.controller;

import com.sevity.authservice.domain.User;
import com.sevity.authservice.dto.UserRegistrationDto;
import com.sevity.authservice.service.AuthService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

    private AuthService authService;

    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/register")
    public User register(@RequestBody UserRegistrationDto registrationDto) {
        return authService.register(registrationDto);
    }

    @PostMapping("/login")
    public User login(@RequestBody UserRegistrationDto loginDto) {
        return authService.login(loginDto.getUsername(), loginDto.getPassword());
    }
}

 

UserRegistrationDto는 클라이언트로부터 받은 데이터를 도메인 모델에 바인딩하기 위해 사용하는 DTO(Data Transfer Object)입니다. UserRegistrationDto는 사용자 등록 요청을 처리하는 메소드에서 사용됩니다.

//이거랑 User.java랑 유사하게 생겼는데
//User 클래스와 UserRegistrationDto 클래스는 서로 다른 목적으로 사용됩니다.
//User 클래스는 데이터베이스의 users 테이블에 대응하는 엔티티 클래스입니다. 이 클래스는 JPA가 데이터베이스와의 상호 작용을 위해 사용하는 도메인 모델입니다.
//UserRegistrationDto 클래스는 클라이언트로부터 받은 사용자 등록 요청 데이터를 바인딩하기 위해 사용하는 DTO(Data Transfer Object)입니다. 
//이 클래스는 사용자 등록 API의 요청 본문에 담긴 데이터를 Java 객체로 변환하는 데 사용되며 이유는 다음과 같음
/*
1. 표현 계층과 영속성 계층 분리: 엔티티 클래스는 영속성 계층에서 사용되며, DTO는 표현 계층에서 사용됩니다. 이 두 계층을 분리함으로써 각 계층의 책임을 명확히 할 수 있습니다.
2. API 스펙 변경에 유연하게 대응: 클라이언트와 서버 간에 주고받는 데이터의 형태가 변경되더라도, 이에 따라 엔티티 클래스를 변경하지 않고 DTO만 변경하면 되므로 유연하게 대응할 수 있습니다.
3. 데이터 유효성 검사: DTO에서는 클라이언트로부터 받은 데이터의 유효성을 검사할 수 있습니다. 예를 들어, 사용자 등록 요청에서 비밀번호와 비밀번호 확인이 일치하는지 검사하는 것은 DTO에서 수행할 수 있습니다. */
package com.sevity.authservice.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserRegistrationDto {
    private String username;
    private String password;
}

/user 엔트포인트 구현(이것은 사용자정보 페이지에 해당한다)

//authentication은 Spring Security에서 제공하는 인터페이스이며, 사용자 인증에 성공하면
//인증정보를 담는다. authentication.getName()은 UserDetailsService인터페이스를 통해 결국 DB에서 
//username값을 가져오게 된다.

package com.sevity.authservice.controller;

import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Admin;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @GetMapping("/user")
    public ResponseEntity<?> getUser(Authentication authentication) {
        // 이 부분에서는 인증된 사용자의 정보를 가져와서 반환하는 로직이 들어갑니다.
        return ResponseEntity.ok("User page. user: "+authentication.getName());
    }
}

 

admin과 user를 나누기위한 Role구현

먼저 DB스키마를 아래처럼 넣는다.

-- 역할 테이블 생성
CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);

-- 사용자와 역할의 관계 테이블 생성
CREATE TABLE user_roles (
    user_id INTEGER NOT NULL,
    role_id INTEGER NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE
);

그다음 User에서 했듯이 Role.java , RoleRepository.java 구현

package com.sevity.authservice.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonBackReference;

import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
@Table(name = "roles")
@Getter
@Setter
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    //아래 users는 이역할(예를들어 name="ROLE_ADMIN")을 가지는 모든 user를 의미함. online judge에서는 필요없을수 있으나 학습용으로 유지함.
    //User와 Role은 n:n관계 이지만 관계의 주인은 User쪽에 있다. 왜냐면 mappedBy키워드를 쓰는순간 spring JPA에서 노예로 판정함!
    //JPA에서는 관계의 주인쪽에서만 데이터베이스 연산이 수행됨!!!
    //mappedBy = "roles"는 User.java안의 User class의 roles 멤버변수를 의미
    //문자열형태로 표현하는 이유는 Java에서 직접적인 참조를 할경우 상호참조 형태가 되어 순환참조 문제를 일으킬 수 있기 때문
    @ManyToMany(mappedBy = "roles")
    @JsonBackReference // 이게 없으면 무한반복 오류로 죽어버림
    private Set<User> users;
    // getter, setter, etc.
}
package com.sevity.authservice.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface RoleRepository extends JpaRepository<Role, Long> {
    Role findByName(String name);
}

 

postgreSQL기준 다음과 같은 권한부여 필요

GRANT ALL PRIVILEGES ON TABLE roles TO online_judge_admin;
GRANT ALL PRIVILEGES ON TABLE user_roles TO online_judge_admin;

 

실행시 user, admin role을 table에 등록하도록 코딩(db에 하드코딩하는 것보다 이렇게 해두는게 추적하거나 재설치시 유리)

@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }

    @Bean
    CommandLineRunner init(RoleRepository roleRepository) {
        return args -> {
            Role adminRole = roleRepository.findByName("ROLE_ADMIN");
            if (adminRole == null) {
                Role newAdminRole = new Role();
                newAdminRole.setName("ROLE_ADMIN");
                roleRepository.save(newAdminRole);
            }
            
            Role userRole = roleRepository.findByName("ROLE_USER");
            if (userRole == null) {
                Role newUserRole = new Role();
                newUserRole.setName("ROLE_USER");
                roleRepository.save(newUserRole);
            }
        };
    }
}

이렇게 하면 백엔드 인증서비스 구현이 완료되었다. 아래 curl명령어로 테스트 할 수 있다.

 

1. 사용자등록

curl -X POST http://localhost:8080/register -H 'Content-type:application/json' -d '{
  "username": "testuser",
  "password": "testpassword"
}'

2. 로그인(register와 다르게 json형태가 아닌 key&value형태임에 주의.. 역사적/관습적인 이유로 이게 디폴트라한다)

curl -X POST http://localhost:8080/login -c cookies.txt -d 'username=testuser&password=testpassword'

3. 사용자정보페이지 접근

curl -X GET http://localhost:8080/user -b cookies.txt

4. 어드민페이지 접근

curl -X GET http://localhost:8080/admin -b cookies.txt

 

frontend-service와 연동할때, 브라우저로 호출하는 경우에, 스프링부트와 next.js등이 HOST나 PORT가 다른점 때문에 도메인내 호출이 아닌걸로 판단돼서, CORS가 문제 될 수 있다. 그러면 SecurityConfig.java를 아래처럼 허용적으로 바꾸어서 넘어갈 수 있다. 아래는 무지성으로 모든걸 허용하는 버전이나 필요한 만큼 조절하면 된다.

package com.sevity.authservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final AuthenticationSuccessHandler authenticationSuccessHandler;
    private final AuthenticationFailureHandler authenticationFailureHandler;

    public SecurityConfig(UserDetailsService userDetailsService,
                          AuthenticationSuccessHandler authenticationSuccessHandler,
                          AuthenticationFailureHandler authenticationFailureHandler) {
        this.userDetailsService = userDetailsService;
        this.authenticationSuccessHandler = authenticationSuccessHandler;
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    CorsConfigurationSource corsConfigurationSource() {  // 아래 .cors()가 불릴때 주입된다.
        CorsConfiguration configuration = new CorsConfiguration();
        //아래는 최대한 허용적인 구성. 실제로는 필요한 만큼 허용해야한다.
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors().and()   // CORS 설정 적용
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/register", "/login").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .permitAll()
            .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

도커로 배포

도커배포를 위해서는 먼저 다음과 같은 Dockerfile이 필요하다. Dockerfile은 Docker image를 생성하는데 필요한 명령을 담고 있으며, 필수적이다.

# 첫 번째 단계: Maven Wrapper를 사용하여 어플리케이션 빌드
# 아래 AS build부분은 docker build명령을 실행하면 진행됨
# FROM base_image형태이며 여기서는 java를 포함한 debian기반 운영체제의 이미지를 베이스로 해서 추가설정을 한다는 의미
FROM eclipse-temurin:17-jdk-jammy AS build
# 도커이미지내 작업디렉토리를 /workspace/app으로 설정.  이는 후속 RUN, CMD, ENTROYPOINT, COPY, ADD명령의 실행경로가 됨
WORKDIR /workspace/app

# maven, spring 관련 파일들을 도커로 복사
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

# maven으로 스프링부트프로젝트 빌드(도커아니어도 하는거)
RUN ./mvnw install -DskipTests
# 필요한디렉토리를 만들고 빌드된 jar 압축해재. 필수는 아니고 최적화전략의 일환
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

# 멀티스테이지 전략의 두 번째 단계
# jdk가 아닌 jre만 포함하는 가벼운 이미지로 전환
# 이렇게 하면 최종 이미지는 jre기준으로 되어 크기를 줄일수 있음.
FROM eclipse-temurin:17-jre-jammy  
VOLUME /tmp  # 일종의 nas설정처럼 컨테이너간 또는 컨테이너와 호스트간 데이터 공유공간을 만드는것
ARG DEPENDENCY=/workspace/app/target/dependency
# 첫번째 빌드단계에서 생성된 파일들을 새 이미지로 복사. 
# 멀티스테이지 구성의 경우 첫번째 이미지는 소멸되기 때문에 아래와 같은 복사과정이 필요.
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app

# 아래 부분은 docker run명령을 사용하면 진행됨(아래 ENTRYPOINT부분. 또는 CMD도 가능)
ENTRYPOINT ["java","-cp","app:app/lib/*","com.sevity.authservice.AuthServiceApplication"]

아래처럼 docker내 빌드과정을 생략하고 jar만 복사 실행하는 간단한 버전도 가능하다.

FROM openjdk:17
WORKDIR /app
COPY target/auth-service-0.0.2-SNAPSHOT.jar /app
CMD ["java", "-jar", "auth-service-0.0.2-SNAPSHOT.jar"]

버전이 하드코딩된게 단점인데, 아래처럼 보완가능(약간꼼수)

FROM openjdk:17
WORKDIR /app
COPY target/*.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]

 

그다음 다음 명령으로 build하면 된다.(맨뒤 .은 생략불가하며 Dockerfile 위치를 나타낸다)

docker build -t auth-service .

실행은 다음처럼 docker run으로 하면 된다.

docker run auth-service

환경변수 설정이 필요한 경우 다음처럼 -e를 쓰면됨

docker run -e "ENV_VAR_NAME1=value1" -e "ENV_VAR_NAME2=value2" my-image

또는 .env파일을 쓴다면 아래와 같이 가능

docker run --env-file .env auth-service

보통은 포트도 연결해야 서비스기능이 가능하니 아래처럼 -p 옵션까지 주자

docker run --env-file .env -p 8080:8080 auth-service

 

하지만 커멘드라인이 길어지니 docker-compose.yml에 다음처럼 기록가능

version: '3.8'
services:
  auth-service:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:  # 아래 환경변수는 .env에 정의하면 연동된다.
      - DATABASE_URL
      - DATABASE_USERNAME
      - DATABASE_PASSWORD

이경우 docker run이 아닌 docker-compose 를 쓰면 된다.

docker-compose up을 하면 docker build와 docker run을 모두 수행하며,

docker-compose build를 하면 docker build까지만 된다. (docker-compose를 쓰더라도 Dockerfile은 빌드를 위해 필요하다)

docker-compose를 쓰면 여러 도커이미지를 동시에 켜거나 끄는등의 동시 핸들링에도 용이하다.

docker-compose up --build를 해서 --build 옵션을 주면 실행전 무조건 빌드를 거치게 된다.

 

세션을 Redis로 바꾸기

스프링부트에서는 다음과 같이 기본적으로 SpringSecurity를 통한 세션관리를 지원한다. 

public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

이 설정은 전통적인 세션 기반 인증을 사용하고 있으며, 서버 측에서 세션을 관리하고 클라이언트에게는 JSESSIONID 쿠키를 통해 세션을 식별. Spring Security의 기본 세션 관리 방식은 일반적으로 단일 노드에서 작동하며, 여러 서비스나 여러 인스턴스 간에 세션을 공유하기 어려움. 이는 각 서비스나 인스턴스가 자체적인 세션 저장소를 가지고 있기 때문에 발생하는 문제.

이를 해결하기위해 Redis세션 저장소를 활용할 수 있다.

단계1: Redis설치

sudo apt-get update
sudo apt-get install redis-server

단계2: Spring Session Redis의존성 추가
pom.xml에 다음 내용추가

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.5.0</version> <!-- 버전은 프로젝트에 맞게 조정 -->
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

단계3: application.properties 파일에 Redis 연결 정보를 추가

spring.redis.host=localhost
spring.redis.port=6379

단계4: 아래 클래스를 config 폴더내 추가

import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
}

단계5: Entity클래스들을 수정해서 Serialize가능하게 만들어주기

import java.io.Serializable;

@Entity
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    // 기존 코드
}


...



import java.io.Serializable;

@Entity
public class Role implements Serializable { // Serializable 인터페이스 구현
    private static final long serialVersionUID = 1L; // serialVersionUID 추가

    // 기존코드
}

실제로 redis에 인증정보가 저장되는지는 터미널에서 redis-cli한 뒤 keys *로 확인 가능하다.

sevity@sevityubuntu:~$ redis-cli
127.0.0.1:6379> keys *
1) "spring:session:expirations:1696602840000"
2) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:sevity"
3) "spring:session:sessions:expires:c5a0bf12-504a-45e9-b970-fe41e93c3aa3"
4) "spring:session:sessions:c5a0bf12-504a-45e9-b970-fe41e93c3aa3"
127.0.0.1:6379>
반응형

일반적인 정렬

일반적인 정렬은 다음과 같이 std::sort를 쓰면 손쉽게 가능하다.

#include <bits/stdc++.h>
using namespace std;
int main() {
    int arr[7] = {5,3,67,21,67,2,69};
    sort(arr, arr+7);
    for(int i=0;i<7;i++) printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

실행 결과는 다음과 같다.

$ ./a.out
2 3 5 21 67 67 69

 

비교함수 제작을 통한 커스텀 정렬 구현하기

주의 할점

1. 두 원소가 같을때 true를 리턴하지 않도록 주의(항상 return a>b; 또는 return a<b; 형태로 등호를 포함하지 않도록 리턴하자)

2. 매개변수를 받을때 &를 붙여서 불필요한 복사가 일어나지 않도록 하자.

아래는 커스텀 비교함수의 예시이다. 이 문제의 답이기도 하다.

#include <bits/stdc++.h>
using namespace std;
map<string, int> cnt;
bool compare(string& a, string& b) {
    if(cnt[a]!=cnt[b]){
        return cnt[a]>cnt[b];  // 출현빈도 기준 내림차순
    }
    if(a.length() != b.length()){
        return a.length()>b.length();  // 단어 길이에 따른 내림차순
    }
    return a < b;  // 사전순 오름차순
}
int main() {
    int N, M; scanf("%d%d\n", &N, &M);
    vector<string> v;
    while(N--) {
        char word[11];scanf("%s\n", word);
        if(strlen(word)<M) continue;
        cnt[word]++;
        if(cnt[word]==1) v.push_back(word);
    }
    sort(v.begin(), v.end(), compare);
    for(auto e:v) printf("%s\n", e.c_str());
    return 0;
}

아래처럼 람다함수를 써도 된다.

#include <bits/stdc++.h>
using namespace std;
map<string, int> cnt;
int main() {
    int N, M; scanf("%d%d\n", &N, &M);
    vector<string> v;
    while(N--) {
        char word[11];scanf("%s\n", word);
        if(strlen(word)<M) continue;
        cnt[word]++;
        if(cnt[word]==1) v.push_back(word);
    }
    sort(v.begin(), v.end(), [&](string& a, string& b){
        if(cnt[a]!=cnt[b]){
            return cnt[a]>cnt[b];  // 출현빈도 기준 내림차순
        }
        if(a.length() != b.length()){
            return a.length()>b.length();  // 단어 길이에 따른 내림차순
        }
        return a < b;  // 사전순
    });
    for(auto e:v) printf("%s\n", e.c_str());
    return 0;
}

 

반응형

NFT란 싸인볼에 비유할 수 있다.

그냥 공은 가격이 싸지만, 유명선수의 싸인볼은 비싸다. 

유명선수의 싸인이라는 희귀하고 고유한 속성이 붙기 때문.

NFT도 그렇다. 일반 코인은 그냥 공처럼 하나하나의 의미가 없고 개수로서 의미를 가지나,

NFT는 JSON 메타데이터로 각각의 NFT가 구분되어 별도의 가치를 가지게 된다.

반응형

'비트코인' 카테고리의 다른 글

나만의 ERC-20 토큰 만들기 실습편  (1) 2023.05.21
Solidity  (0) 2023.05.17
나만의 ERC-20 토큰 만들기  (0) 2023.05.09
스마트 컨트랙트  (0) 2023.03.15
유동성풀, LP공급, 비영구적 손실의 개념  (0) 2023.03.14

git commit을 위한 사용자정보 등록. 커밋할때 누가 했는지에 대한 정보를 git이 알게 해줌.

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

 

편집기를 vim으로 설정(초기 값은 nano로 되어 있음)

git config --global core.editor "vim"

 

git push를 위한 인증정보 설정(GitHub는 2021년 8월 13일부터 패스워드를 사용한 인증 방식을 지원하지 않음)

 

ssh키를 생성해서 github사이트에 등록해준다.

아래처럼 ssh-keygen실행후 ~/.ssh/id_rsa의 내용을 cat해서 복사한다음

 $ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/sevity/.ssh/id_rsa):
/home/sevity/.ssh/id_rsa already exists.
Overwrite (y/n)?

 

github사이트의 세팅에 들어가서 ssh키를 등록해주면 된다.

Setting > SSH and GPG keys > New SSH key

 

 

반응형

'Programming > Git' 카테고리의 다른 글

git PR(pull request) 관련  (1) 2023.10.27
git log  (0) 2019.12.09
git 자주 쓰는 명령어 모음  (0) 2019.09.27
git branch 관련  (0) 2019.04.17
github  (0) 2018.11.07

리눅스 머신에 직접 연결된 모니터가 없거나 SSH를 통해 원격으로 머신에 접속한 경우 그래픽 환경이 없을 수 있습니다.
원격에서 SSH에 접속하여 그래픽 프로그램을 실행하려면 X11 포워딩을 사용해야 합니다.

이를 위해서는 SSH 클라이언트가 X11 포워딩을 지원해야 합니다. 또한, 원격 리눅스 호스트에서도 X11 포워딩이 가능해야 합니다.
아래는 X11 포워딩을 설정하는 방법입니다:

1. 먼저 SSH 서버(여기서는 라즈베리파이)에서 X11 포워딩이 허용되도록 설정 합니다. 이를 위해 /etc/ssh/sshd_config 파일을 열고, X11Forwarding이 yes로 설정되어 있는지 확인합니다.


2. SSH 클라이언트에서도 X11 포워딩을 허용해야 합니다. 일부 클라이언트에서는 -X 플래그를 사용하여 SSH에 접속할 때 X11 포워딩을 활성화할 수 있습니다.

ssh -X sevity@sevity.com

3. X 서버가 클라이언트 머신에 설치되어 있어야 합니다. Windows에서는 Xming과 같은 프로그램을 사용할 수 있습니다. MacOS에서는 XQuartz를 사용할 수 있습니다.

 

반응형

유사한 가이드들

 

환경

라즈베리파이 콘솔에서 진행(nodejs는 12.22.7 버전, truffle은 5.1.67버전으로 설치함)

 

 

1. nodejs 설치(sudo apt-get install nodejs npm)

  • truffle포함 대부분 이더리움 배포환경은 node js기반으로 작성되어 있기 때문에 필요

2. infura.io 설정(웹사이트 가입 및 WEB3 API KEY설정)

  • 비트코인과 마찬가지로 이더리움도 분산화된 노드 기반으로 동작하는데, 원래는 ERC-20을 이 노드에 올려야 한다.
  • (이더리움 노드는 비트코인 노드와 다르게 EVM을 구동하고 바이트코인을 돌리는 가상 컴퓨터역할도 한다. 스마트계약은 모든 노드에서 반복수행되며 같은 결과가 나오는지 검증된다. 이에 따라 결정론성이 중요한데, 이는 나중에 후술하기로 하자)
  • 하지만 노드 운용에는 전체 블록체인을 다운받아야 하기도 하고, 복잡하고 무거운 컴퓨팅 설정이 필요하다.
  • infura는 이러한 개인이 노드운용을 해야하는 부담을 대신해주며, 가볍게 스마트계약을 배포할 수 있다.

3. truffle 설치(npm install -g truffle)

  • 원래 스마트계약 solidity 파일은 solc로 컴파일 하는데, truffle을 사용하면 컴파일부터 테스트, 마이그레이션까지 원툴로 전과정 제공
  • 설치후 truffle init을 해주면 기본 폴더나 config파일(truffle-config.js) 만들어준다.
  • truffle-config.js는 아래처럼 작성
  • 복잡해 보이지만 development는 로컬 환경, sepolia는 테스트넷 환경을 위한거
require('dotenv').config();
const { MNEMONIC, PROJECT_ID } = process.env;

const HDWalletProvider = require('@truffle/hdwallet-provider');


module.exports = {
  networks: {
    development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 8545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
     gas: 30000000,
    },
    sepolia: {
      //provider: () => new HDWalletProvider(MNEMONIC, `https://sepolia.infura.io/v3/${PROJECT_ID}`),
      provider: () => new HDWalletProvider(MNEMONIC, `wss://sepolia.infura.io/ws/v3/${PROJECT_ID}`),
      network_id: 11155111,
      gas: 4465030,
      networkCheckTimeout: 100000,
    },
  },

  // Set default mocha options here, use special reporters, etc.
  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: "0.8.19",      // Fetch exact version from solc-bin (default: truffle's version)
      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
      // settings: {          // See the solidity docs for advice about optimization and evmVersion
      //  optimizer: {
      //    enabled: false,
      //    runs: 200
      //  },
      //  evmVersion: "byzantium"
      // }
    }
  },

};

 

4. openzeppelin 설치(npm install @openzeppelin/contracts)

  • 스마트계약을 처음부터 코딩하지 않도록 상속만 하면 되는 base 클래스(contract) 제공

5. 스마트 계약 solidity 파일 작성

  • 아래 처럼 openzeppelin에서 제공하는 ERC20을 상속하면 만들기 쉽다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SevityToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("Sevity Token", "SEVITY") {
        _mint(msg.sender, initialSupply);
    }
}

6. migrations 폴더에 다음 파일 작성

  • 1_initial.js로 저장(사실 이름은 큰 상관 없고 숫자를 앞에 붙이면 그 순서대로 실행해준다)
  • 아래는 스마트 계약을 테스트넷에 배포하면서 초기 발행량을 10억개로 하는 코드
const SevityContract = artifacts.require("SevityToken");

module.exports = function(deployer) {
  const initialSupply = web3.utils.toWei('1000000000', 'ether'); // 10억 토큰
  deployer.deploy(SevityContract, initialSupply);
};

7. sepolia넷에서 공짜로 ETH받기

  • 메타마스크에서 메인넷 대신 sepolia테스트넷으로 설정하고(그냥 고르면 되는 수준이다)
  • 테스트용 ETH는 https://sepoliafaucet.com/ 여기접속해서 받으면 된다(테스트용이라 공짜로 받을 수 있다)

 

8. 단순한 ERC-20제작 테스트이므로 로컬환경에 배포는 생략하고, 테스트넷중 하나인 sepolia에 배포(truffle migrate --network sepolia)

  • 이때 contracts안에 있는 sevity_token.sol 컴파일
  • migrations안에있는 스크립트들 수행된다.

truffle 폴더 구조

 

9. 배포성공후 이더스캔에 가보면 다음처럼 Contract Creation 트랜잭션을 볼 수 있다.

10. 코인발행이후 스마트계약의 읽기 작업은 가스를 소모하지 않으며, 초기 발행량을 조회하려면 truffle로 다음처럼 할 수 있다.

(coin) sevity@raspberrypi:~/workspace/erc20 $ truffle console --network sepolia
truffle(sepolia)> let instance = await SevityToken.deployed()
undefined
truffle(sepolia)> let balanceInWei = await instance.balanceOf("0xFbb5164e8884ccB4893381b374D1ec8BbbC56F75")
undefined
truffle(sepolia)> let balanceInTokens = web3.utils.fromWei(balanceInWei.toString())
undefined
truffle(sepolia)> console.log(balanceInTokens)
1000000000
undefined
truffle(sepolia)>

11. 메타마스크에 커스텀 토큰으로 추가하기

  • 아래처럼 계약주소(truffle migrate 과정에서 결과로 출력되며, 이더스캔에서도 확인가능)를 넣으면 토큰기호나 소수점은 알아서 세팅된다.

추가완료!

12. 메인넷에 배포

  • truffle-config.js에 다음 부분 추가해주면 되는데
    mainnet: {
      provider: () => new HDWalletProvider(MNEMONIC, `wss://mainnet.infura.io/ws/v3/${PROJECT_ID}`),
      network_id: 1,
      confirmations: 2,    // # of confirmations to wait between deployments. (default: 0)
      timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true,     // Skip dry run before migrations? (default: false for public nets )
      gas: 21000,
      gasPrice: 50000000000, // 50GWEI
      networkCheckTimeout: 100000,
    },
  • gas와 gasPrice 부분은 설명이 좀 필요하다.
  • gas는 gasLimit을 설정하는 부분인데, gas는 스마트계약 복잡성에 따라 런타임에 이러리움에서 정확하게 부여하게 되지만, 사전에 알수는 없다. 사전에 알기 힘든 이유는 정지문제(Halting Problem)때문인데 주어진 프로그램이 언제 멈출지, 또는 계속 실행될지를 모르기 때문이다. 요금은 실제 사용된 gas에 의해 부과되기에 gasLimit을 무조건 높은 값으로 해놓고 보는게 개발자에게는 유리할 수 있는데, 이더리움 블록마다 gasLimit 합계의 제한이 걸려있어서(80만 GWEI던가), 채굴자들이 블록하나 안에 많은 트랜잭션을 포함하기 위해 기피할 수 있다는 점이 있다. 그렇다고 너무 작은 값을 주었다가 실제 사용량이 그보다 크면 out of gas 오류가 나며 트랜잭션이 실패해 버리게 된다(단, 트랜잭션 실패시에도 사용된 gas는 환원되지 않고 gas 비용은 손실되므로 주의가 필요!!) 적절한 gasLimit을 대략적으로 계산해주는 툴도 있다고 하는데 아직 써보진 않았다.
  • gasPrice는 이와 다르게, 개발자가 높게 책정할수록 그대로 채굴자의 이득이 되는데, 너무 낮으면 채굴이 진행이 잘 안고, 너무 높으면 손해이므로 어느 정도가 적절한지는 가스트래커를 통해 확인할 수 있다.
  • 채굴자에게는 실제로 스마트계약 복잡성에 따라 사용된 gas * gasPrice를 보상으로 지급된다.
반응형

'비트코인' 카테고리의 다른 글

NFT  (0) 2023.06.24
Solidity  (0) 2023.05.17
나만의 ERC-20 토큰 만들기  (0) 2023.05.09
스마트 컨트랙트  (0) 2023.03.15
유동성풀, LP공급, 비영구적 손실의 개념  (0) 2023.03.14

Solidity는 이더리움 블록체인에서 스마트 계약을 개발하기 위한 프로그래밍 언어이며 EVM상에서 돌아갑니다.

ERC-20토큰을 발행하기 위해서도 사용됩니다.

 

스마트 계약 코드는 EVM바이트코드로 컴파이되며, 이더리움 네트워크에 배포될 때 이 바이트 코드가 블록체인에 저장됩니다.

이 바이트 코드는 모든 노드에 복제되어 분산 저장됩니다.

 

바이트코드의 실행은 특정 노드에서만 진행되지 않고 모든 노드는 독립적으로 같은 바이트코드를 실행하고 그 결과를 검증하는 식으로 진행됩니다.

 

Solidity는 스마트 계약 코드작성에만 사용되고, 작성된 코드를 이더리움에 배포하는 기능은 없습니다.

해당 기능은 별도 Web3.js 등을 통해 수행됩니다(주로 javascript 사용)

 

 

공식 사이트: https://soliditylang.org/

반응형

'비트코인' 카테고리의 다른 글

NFT  (0) 2023.06.24
나만의 ERC-20 토큰 만들기 실습편  (1) 2023.05.21
나만의 ERC-20 토큰 만들기  (0) 2023.05.09
스마트 컨트랙트  (0) 2023.03.15
유동성풀, LP공급, 비영구적 손실의 개념  (0) 2023.03.14

 

ISA, IRP, 퇴직연금, 연금저축은 모두 개인의 장기적인 자산 관리와 노후 대비를 위한 금융상품입니다. 각각의 특징은 다음과 같습니다:

 

 
일반계좌
연금저축
ISA
IRP
풀네임
 
 
개인저축계좌
개인형퇴직연금
도입시기
 
 
23년 1월1일
 
멀티계좌가능여부
 
 
 
좌동
연간납입한도
 
1,800만원(IRP 포함)
 
1,800만원(연금저축 포함)
가입기간 또는
연금수령조건
 
가입기간 5년이상
만 55세 이후
10년에 나눠서 수령가능
최소3년
가입기간 5년이상
만 55세 이후
10년에 나눠서 수령가능
주요혜택
손익통산이 아닌 수익모두에 과세하므로 불리
손익통산 연간 400만원
(IRP와 한도공유)
주식/채권/펀드등을 하나의 계좌에서 관리(주식 전액 비과세)
국내주식투자시 배당소득 비과세(200만원 한도)
주식양도차익 5천만원초과시 비과세(일반계좌는 과세)
소득세 감면

손익통산 연간 700만원까지 세액공제
(연금저축과 한도공유)
 
주요제약
 
중도인출가능
위험자산 100%투자가능
 
중도인출불가
위험자산 70%투자가능
가입자격
 
누구나
 
근로소득자, 자영업자
납입단계 세액공제
 
 
 
있다.
투자수익 과세
 
과세이연, 연금수령시 저리과세
 
과세이연, 연금수령시 저리과세
배당금 과세
15.4%
 
 
 
연금수령시세금
 
연금소득세 3.3~5.5%
 
연금소득세 3.3~5.5%
중도해지시세금
 
기타소득세 16.5%
 
기타소득세 16.5%
담보대출
 
가능
 
불가능
수수료출금
 
 
 
 
반응형

'재무 금융' 카테고리의 다른 글

Direct Indexing  (0) 2022.05.04
금리와 채권의 관계  (0) 2022.04.08
TLS(Transport Layer Security)  (0) 2021.05.31
오픈뱅킹 정보제공자 인증관련  (0) 2021.05.31
마이데이터 관점에서 본 OAuth2.0  (0) 2020.09.23

ERC-20란?

ERC는 Ethereum Request for Comments의 약자로 이더리움 표준을 의미합니다.

그 중에서 ERC-20은 이더리움 플랫폼에서 스마트 계약을 통해 생성되는 토큰을 위한 기술 표준입니다.

이 표준은 토큰이 어떻게 전송되고, 어떻게 접근할 수 있는지, 전체 공급량이 얼마인지 등과 같은 규칙을 정의합니다.

2015년 제정후 많은 코인들이 발행되었으며 그 예시는 체인링크(LINK), 유니스왑(UNI), 골렘(GNT), MANA, SNT 등입니다.

 

WIK라는 ERC-20 토큰을 만든다고 가정하고, 단계별 개발 과정은 다음과 같습니다.

1. Solidity 학습: Solidity는 이더리움 스마트 컨트랙트를 작성하는 데 사용되는 프로그래밍 언어입니다. Solidity에 대한 이해를 바탕으로 ERC-20 토큰을 구현할 수 있습니다. 온라인에서 제공되는 Solidity 관련 문서, 튜토리얼 및 자료를 참고하세요.

2. 개발 환경 설정: Remix, Truffle, Hardhat 등의 이더리움 개발 프레임워크를 사용하여 개발 환경을 설정하세요. 이러한 프레임워크는 스마트 컨트랙트 개발, 테스트, 배포를 쉽게 할 수 있도록 도와줍니다.

3. ERC-20 스마트 컨트랙트 작성: ERC-20 토큰 표준에 따른 스마트 컨트랙트를 작성하세요. 토큰 이름(Wonil Token), 심볼(WIK), 총 발행량, 소수점 자릿수 등의 정보를 포함해야 합니다. 또한, 표준 ERC-20 인터페이스를 구현해야 하며, 다음 함수들을 포함해야 합니다.
totalSupply
balanceOf
transfer
transferFrom
approve
allowance

 

4. 스마트 컨트랙트 테스트: 작성한 스마트 컨트랙트를 테스트하세요. 로컬 이더리움 개발 환경(Ganache 등) 또는 테스트넷(Ropsten, Rinkeby 등)을 사용하여 컨트랙트 기능을 테스트하고, 문제가 없는지 확인합니다.

5. 스마트 컨트랙트 배포: 테스트를 완료한 후, 이더리움 메인넷에 스마트 컨트랙트를 배포합니다. MetaMask, MyEtherWallet, Truffle, Hardhat 등의 도구를 사용하여 컨트랙트를 배포할 수 있습니다. 배포 과정에서 이더리움을 소비하는 가스비를 지불해야 합니다.

6. 토큰 관리 및 분배: 스마트 컨트랙트가 성공적으로 배포되면, 토큰을 관리하고 사용자에게 분배할 수 있습니다. 토큰의 소유권 이전, 토큰 락업, 에어드랍 등 다양한 기능을 구현하고 활용할 수 있습니다.

7/ 토큰 추적 및 지갑 통합: WIK 토큰 사용자들이 토큰을 지갑에서 추적하고 관리할 수 있도록 지갑 통합을 지원해야 합니다. 이를 위해 사용자들에게 토큰의 컨트랙트 주소, 심볼, 소수점 자릿수를 제공해야 합니다. 일반적으로 이더리움 지갑들은 ERC-20 토큰을 자동으로 인식하고 지원합니다.

8. 거래소 상장: 토큰의 유동성을 높이고 가치를 창출하기 위해 암호화폐 거래소에 상장을 고려할 수 있습니다. 거래소에 따라 상장 요구사항과 절차가 다르므로, 원하는 거래소의 상장 가이드라인을 확인하고 준수해야 합니다.

9. 토큰 마케팅 및 커뮤니티 활성화: WIK 토큰의 인지도를 높이기 위해 마케팅 활동을 계획하고 실행해야 합니다. 또한, 커뮤니티를 만들어 토큰 사용자들과 소통하며 토큰 가치를 향상시키는 데 도움이 됩니다.

10. 지속적인 개발 및 관리: 토큰의 성장을 위해 지속적인 개발과 관리가 필요합니다. 새로운 기능 추가, 보안 업데이트, 토큰 이벤트 및 파트너십을 통해 토큰의 가치를 높일 수 있습니다.

반응형

'비트코인' 카테고리의 다른 글

나만의 ERC-20 토큰 만들기 실습편  (1) 2023.05.21
Solidity  (0) 2023.05.17
스마트 컨트랙트  (0) 2023.03.15
유동성풀, LP공급, 비영구적 손실의 개념  (0) 2023.03.14
파일코인  (0) 2022.05.14

+ Recent posts