(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
-- 데이터베이스 생성
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개로 잡아 보았다. (진행하면서 변경가능, 링크를 누르면 각 서비스에 대한 블로그로 이동가능)
문제 관리 서비스 (Frontend): 문제를 보여주고, 문제 추가, 삭제, 수정 등의 인터페이스를 제공
제출 관리 서비스 (Backend): 사용자의 코드 제출 및 제출 기록을 관리
제출 관리 서비스 (Frontend): 코드 제출 인터페이스와 제출 기록 확인 인터페이스를 제공
채점 서비스 (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에게 이 클래스가 보안 설정을 담당하는 클래스임을 알림.
//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명령어로 테스트 할 수 있다.
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만 복사 실행하는 간단한 버전도 가능하다.
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의 기본 세션 관리 방식은 일반적으로 단일 노드에서 작동하며, 여러 서비스나 여러 인스턴스 간에 세션을 공유하기 어려움. 이는 각 서비스나 인스턴스가 자체적인 세션 저장소를 가지고 있기 때문에 발생하는 문제.
<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>
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 *로 확인 가능하다.
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)?
아래처럼 계약주소(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를 보상으로 지급된다.
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. 지속적인 개발 및 관리: 토큰의 성장을 위해 지속적인 개발과 관리가 필요합니다. 새로운 기능 추가, 보안 업데이트, 토큰 이벤트 및 파트너십을 통해 토큰의 가치를 높일 수 있습니다.