설치

하둡은 java기반이라 java sdk설치가 필요.

sudo apt-get update
sudo apt-get install default-jdk

그다음, 도커 이미지 받아서 그 안에서 연습해볼수도 있지만, 제대로 하려면 아래처럼 다운받고 설정하는 과정이 필요

# 하둡의 공식 웹사이트에서 최신 버전을 다운받아 설치
sudo wget https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz

# 압축풀고 설정진행
tar -xvf hadoop-3.3.0.tar.gz
sudo mv hadoop-3.3.0 /usr/local/hadoop
#~/.bashrc에 아래 것들 추가
export HADOOP_HOME=/usr/local/hadoop
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64


#반영
source ~/.bashrc

그다음 어떤 파일시스템을 하부로 쓸것인지 설정파일들을 좀 만져야함. 기본으로 깔렸을때는 hdfs가 아닌 로컬파일시스템을 쓰도록 되어 있음(;;)

# sudo vi $HADOOP_HOME/etc/hadoop/core-site.xml
# <configuration> 태그 사이에 아래내용 추가
<property>
    <name>fs.defaultFS</name>
    <value>hdfs://localhost:9000</value><!--이거 해줘야 로컬파일시스템이 아닌 hdfs씀-->
</property>

# sudo vi $HADOOP_HOME/etc/hadoop/hdfs-site.xml
# <configuration> 태그 사이에 아래내용 추가

<property>
    <name>dfs.replication</name>
    <value>1</value> <!--HDFS에 저장되는 각 데이터 블록이 클러스터 전체에서 복제되는 횟수를 결정-->
</property>

단일노드가 아닌 클러스터 구성시 추가 설정들이 필요(여기서는 생략)

 

다음은 포맷하고 전체시작

# 처음에 포맷해줘야 함
hdfs namenode -format

#전체 재시작
cd /usr/local/hadoop/sin
./stop-all.sh
./start-all.sh

#java ps. 아래처럼 NameNode, DataNode, SecondaryNameNode가 뜨는것을 확인
jps
128290 SecondaryNameNode
127865 NameNode
128026 DataNode

 

HDFS기본 핸들링 방법

아래처럼 hdfs dfs로 시작하는 명령을 주거나,  Hadoop이 제공하는 Hadoop FUSE(FIlesystem in USErspace) 모듈을 이용하면 기존파일시스템에 mount해서 ls,mv,cp등 그대로 사용하는 것도 가능

# /logs라는 폴더 만들기
hdfs dfs -mkdir /logs

# ls하기. Hadoop Distributed File System (HDFS)의 루트 디렉토리 내용을 나열
hadoop dfs -ls /
Found 1 items
drwxr-xr-x   - sevity supergroup          0 2023-07-29 13:20 /logs

# 파일 복사하기.
hdfs dfs -put iis_logs/* /logs

# 헬스체크
hdfs fsck /

 

 

트러블슈팅

기본적인 로그 모니터링

tail -f /usr/local/hadoop/logs/*.log

start-dfs.sh로 하둡을 시작했을때 namenode가 시작되지 않았을때(jps했을때 안보일때)

# 아래 명령으로 이유 파악
 grep -C 5 'ERROR' $HADOOP_HOME/logs/hadoop-sevity-namenode-sevityubuntu.log

# 만약 아래처럼 /tmp 아래 디렉토리 접근이 안된다는 것이면
2023-07-30 16:03:30,469 ERROR org.apache.hadoop.hdfs.server.namenode.NameNode: Failed to start namenode.
org.apache.hadoop.hdfs.server.common.InconsistentFSStateException: Directory /tmp/hadoop-sevity/dfs/name is in an inconsistent state: storage directory does not exist or is not accessible.
        at org.apache.hadoop.hdfs.server.namenode.FSImage.recoverStorageDirs(FSImage.java:392)

# hdfs-site.xml을 열어서 경로를 /tmp가 아닌 /var/lib등 임시가 아닌곳으로 옮겨준다.
vi /usr/local/hadoop/etc/hadoop/hdfs-site.xml
아래 내용 추가
    <property>
      <name>dfs.namenode.name.dir</name>
      <value>/var/lib/hadoop-hdfs/cache/hdfs/dfs/name</value>
    </property>

# 관련 폴더 만들고 권한 부여
sudo mkdir -p /var/lib/hadoop-hdfs/cache/hdfs/dfs/name
sudo chown -R sevity:sevity /var/lib/hadoop-hdfs/cache/hdfs/dfs/name

# 아래명령어로 다시 실행
stop-dfs.sh
start-dfs.sh

datanode가 시작되지 않을때

# jps 했는데 datanode관련된게 안보이면
grep -C 5 "ERROR" $HADOOP_HOME/logs/hadoop-*-datanode-*.log
# 아래와 같은 로그면
2023-07-30 16:24:23,819 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Initialization failed for Block pool <registering> (Datanode Uuid 6b5e2ab5-f8a4-4eb8-b3d3-fe37de728e60) service to localhost/127.0.0.1:9000. Exiting.

# 다음처럼 /hadoop/data 안에 있는 데이터를 날려준다(데이터 포맷은 각오)
rm -rf /hadoop/data/*

부팅할때마다 hdfs 구동되도록 하기(기본적으로는 항상 start-dfs.sh로 시작해줘야함)

sudo vi /etc/systemd/system/hadoop-hdfs.service # 해서 아래내용 입력
[Unit]
Description=Hadoop HDFS
After=network.target

[Service]
User=sevity
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ExecStart=/usr/local/hadoop/sbin/start-dfs.sh
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

# 그 다음 다음을 입력해서 systemctl 리로딩하고, enable을 통해서 부팅시마다 시작되도록 함
sudo systemctl daemon-reload
sudo systemctl enable hadoop-hdfs

# 재부팅해서 jps해서 다음처럼 namenode, datanode, SecondaryNameNode 3가지 띁거 확인
jps
4673 SecondaryNameNode
3929 NameNode
4255 DataNode

# 마지막으로 헬스체크
hdfs fsck /

 

 

 

 

병렬처리 관련 중요 포인트

mapper가 병렬처리되는것으로 보이나 reducer는 모든 mapper가 끝나야 작업을 시작할 수 있나?
네, 맞습니다. MapReduce 모델에서 Mapper와 Reducer의 작업은 서로 다른 단계에 속하며, 이는 병렬 처리와 데이터 의존성 문제 때문입니다.
Mapper: 각 Mapper는 독립적으로 입력 데이터의 한 부분을 처리하며, 그 결과를 키-값 쌍의 형태로 출력합니다. 이 과정은 병렬로 진행될 수 있으며, 여러 컴퓨터 또는 프로세서 코어에서 동시에 일어날 수 있습니다.
Shuffle and Sort: Mapper의 출력은 모든 Reducer에게 보내지기 전에 "Shuffle and Sort" 단계를 거칩니다. 이 단계에서는 동일한 키를 가진 모든 데이터를 같은 Reducer로 라우팅하고, 키에 대한 데이터를 정렬합니다.
Reducer: Reducer는 키별로 그룹화된 데이터를 받아 처리합니다. 이 과정에서 Reducer는 자신에게 할당된 특정 키에 대한 모든 데이터를 받아야만 작업을 시작할 수 있습니다. 이는 각 키에 대한 모든 값들이 Reducer에서 합쳐지거나 처리되어야 하기 때문입니다. 따라서 Reducer는 모든 Mapper의 작업이 끝나야만 시작됩니다.
이와 같은 방식은 분산 환경에서 대규모 데이터를 처리하는 데 효율적인 방법을 제공합니다. Mapper는 독립적으로 실행되므로 컴퓨터 클러스터의 모든 머신을 활용할 수 있습니다. Reducer는 특정 키에 대한 모든 데이터를 처리하므로, 결과를 재결합하거나 요약하는 작업을 처리할 수 있습니다. 이 모델의 중요한 특성 중 하나는 개발자가 병렬 처리에 대한 세부 사항을 걱정할 필요가 없다는 것입니다. 이는 MapReduce 프레임워크가 처리하기 때문입니다.

반응형

여기를 참조했다.

아래 코드에서 find_intersection 함수를 라이브러리로 활용하자.

이 문제의 정답이기도 하다.

#include <bits/stdc++.h>
#define int long long
using namespace std;
#define REP(i,n) for(int i=1;i<=(int)(n);i++)

struct point { double x, y; 
    bool operator==(const point& other) const {return x == other.x && y == other.y;}
    bool operator<=(const point& other) const {return y < other.y || (y == other.y && x <= other.x);}
    bool operator>(const point& other) const {return y > other.y || (y == other.y && x > other.x);}
};
double CCW(point A, point B, point C, bool sign_only=true) {
    double r = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y);
    if (sign_only == false) return r;
    if (r == 0)return 0;
    return r > 0 ? 1 : -1;
}
struct line { point s, e; };
//touch_ok가 false이면, 두 선분이 교차하지 않고 만나기만 하는 경우에는 false를 리턴
bool Intersect(line x, line y, bool touch_ok=false) {
    point a = x.s, b = x.e;
    point c = y.s, d = y.e;
    double ab = CCW(a, b, c) * CCW(a, b, d);
    double cd = CCW(c, d, a) * CCW(c, d, b);
    if (ab == 0 && cd == 0) { // 이건 두 선분이 평행한 경우
        pair<double, double> aa = { a.x, a.y }, bb = { b.x,b.y }, 
            cc = { c.x, c.y }, dd = { d.x,d.y };
        if (aa > bb)swap(aa, bb);
        if (cc > dd)swap(cc, dd);
        if(touch_ok) return cc <= bb && aa <= dd; // 0이면 점끼리 만나는 것
        return cc < bb && aa < dd; // a<d이면서 b,c가 교차하면 선분
    }
    if(touch_ok) return ab <= 0 && cd <= 0; // 0이면 두 선분이 한점에서 만나는 것
    return ab < 0 && cd < 0; // 이게 기본. 각선분에서 나머지 2개점 방향이 달라야 교차
}

bool find_intersection(line l1, line l2, point& out) // 교점 구하기
{
    point A = l1.s, B=l1.e, C=l2.s, D=l2.e;
	if (A > B) swap(A, B);
	if (C > D) swap(C, D);
	double px = (A.x * B.y - A.y * B.x) * (C.x - D.x) - (A.x - B.x) * (C.x * D.y - C.y * D.x);
	double py = (A.x * B.y - A.y * B.x) * (C.y - D.y) - (A.y - B.y) * (C.x * D.y - C.y * D.x);
	double p = (A.x - B.x) * (C.y - D.y) - (A.y - B.y) * (C.x - D.x);

    bool found = false;
	if (p == 0) // 평행할 때
	{
		// 교점이 하나일 때
		if (B == C && A <= C) found=true, out = B;
		else if (A == D && C <= A) found=true, out = A;
	}
	else // 교차할 때
	{
		double x = px / p;
		double y = py / p;
        out = {x,y};
        found=true;
	}
    return found;
}


int32_t main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    vector<line> l;
    double a, b, c, d; REP(i, 2)cin >> a >> b >> c >> d, l.push_back({ a,b,c,d });
    if(Intersect(l[0], l[1], true)==false) puts("0");
    else{
        puts("1");
        point intercection;
        bool found = find_intersection(l[0], l[1], intercection);
        if(found) printf("%.16lf %.16lf", intercection.x, intercection.y);
    }
    return 0;
}
반응형

ubuntu 기준으로는 sudo apt install docker.io로 설치가능하나, 라즈베리파이 기준으로는 너무 오래된 docker가 설치되거나 설치가 안될수 있어서 다음 방법으로 설치하는걸 추천한다고 함

curl -sSL https://get.docker.com | sh

만약 sudo apt-get install로 하려면 다음과정을 거쳐도 됨

# 또는 아래 방법도 가능
#1. 먼저, 기존에 설치되어 있는 Docker 패키지를 제거합니다(있는 경우):
sudo apt-get remove docker docker-engine docker.io containerd runc
#2. Docker설치를 위한 패키지 업데이트
sudo apt-get update
#3. Dockertㅓㄹ치에 필요한 패키지들 미리 설치
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
#4. Docker의 공식 GPG추가(Docker에서 제공했고 신뢰할수 있다는 디지털 서명)
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | sudo apt-key add -
#5. Docker repository를 APT sources에 추가합니다(APT:패키지관리자 가 인식할수있도록 저장소추가)
echo "deb [arch=armhf] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
    $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
#6. 다시 패키지를 업데이트한 후 Docker를 설치합니다: (이제 apt-get사용가능)
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

설치후 잘 설치됐는지 확인하고 특정 사용자에게 docker그룹추가를 통한 일종의 docker sudo권한 부여

# 잘 설치되었는지 확인
sudo docker images 
sudo docker run hello-world
# sevity사용자를 Docker그룹에추가(해당사용자가 root가 아니어도 docker명령을 다룰수 있게 해줌)
sudo usermod -aG docker sevity

 

docker images해보면 현재 서버에 있는 도커이미지들 목록이 나온다. (일종의 git으로 치면 로컬 저장소개념으로 원격서버에 저장된건 아님)

$ docker images
REPOSITORY                  TAG       IMAGE ID       CREATED        SIZE
auth-service_auth-service   latest    cf72ade83f7f   7 hours ago    455MB
postgres                    13        9ca11a5f9994   12 days ago    350MB
hello-world                 latest    38d49488e3b0   2 months ago   4.85kB

특정 image를 지우려면 docker rmi 사용가능. 예를들어 docker rmi  9ca11a5f9994

 

Dockerfile을 만들고 아래처럼  build하면 된다.(맨뒤 .은 생략불가하면 Dockerfile 위치를 나타낸다)

docker build -t auth-service .

Dockerfile에 대한 샘플은 여기참조

 

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

docker run auth-service

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

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

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

version: '3.8'
services:
  auth-service:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL
      - DATABASE_USERNAME
      - DATABASE_PASSWORD

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

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

docker-compose build를 하면 docker build까지만 된다.

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

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

 

AWS와 연동하기

Elastic Container Service (ECS)는 Docker 컨테이너를 쉽게 배포, 실행 및 스케일링할 수 있게 해주는 완전관리형 컨테이너 오케스트레이션 서비스입니다.이걸 사용해서 연동해보겠습니다.

ECS는 아마존 EC2 인스턴스나 AWS Fargate를 사용하여 실행할 수 있습니다.

 

AWS Elastic Container Service(ECS)를 사용해 애플리케이션을 배포하려면 다음과 같은 단계를 따릅니다.

1. AWS 계정 생성: AWS에서 제공하는 서비스를 이용하려면 먼저 AWS 계정이 필요합니다. 계정이 없다면 AWS 웹사이트에서 생성할 수 있습니다.

2. Docker 이미지를 Docker Hub에 푸시: 먼저, 로컬에서 Docker 이미지를 생성한 후 이를 Docker Hub에 푸시합니다.

# 로컬에서 Docker 이미지를 빌드합니다. 
docker build -t online-judge-auth-service .

# Docker Hub에 로그인합니다.
docker login --username=your-username --password=your-password

# Docker Hub에 이미지를 푸시합니다.
docker tag online-judge-auth-service:latest your-username/online-judge-auth-service:latest
docker push your-username/online-judge-auth-service:latest


3. ECS 클러스터 생성: AWS Management Console에 로그인한 후, ECS 페이지로 이동해 새 클러스터를 생성합니다. 

4. Task Definition 생성: ECS 클러스터 내에 'Task Definitions' 페이지로 이동해 새로운 Task Definition을 생성합니다. 이 과정에서 Docker 이미지의 위치(Docker Hub의 URL), 컨테이너에 필요한 CPU와 메모리, 포트 매핑 등을 설정합니다.
* 이 과정이 좀 중복적으로 느껴지고 직관적이지 않았지만 필요했다.

5. ECS Service 생성: 생성한 Task Definition을 이용해 ECS Service를 생성합니다. 이 과정에서 원하는 수의 Task를 실행하도록 설정하고, 필요한 경우 로드 밸런서를 설정합니다.

* 이번 프로젝트에서는 로드밸런서는 사용하지 않고 그냥 EC2인스턴스로 실행

6. 보안 그룹 설정 확인: 생성한 ECS Service가 외부에서 접근 가능하도록, 해당 서비스가 사용하는 보안 그룹의 인바운드 규칙을 확인합니다. 필요한 경우 포트를 열어줍니다.
* 보안규칙을 통해 8080포트를 열어줘야 했다.

7. 서비스 확인: ECS Service가 성공적으로 시작되면, Task의 IP 주소나 연결된 로드 밸런서의 DNS 이름을 이용해 애플리케이션에 접근해봅니다.

* curl로 접근해보니 잘 되었다.

이러한 단계들은 AWS Management Console을 통해 수행할 수 있지만, AWS CLI나 SDK를 이용해 스크립트로 자동화할 수도 있습니다. 비록 초기 설정에는 약간의 시간이 소요될 수 있지만, 한 번 환경을 구성하고 나면 새로운 버전의 애플리케이션을 쉽게 배포할 수 있습니다.

 

이해하기 따라로웠던개념들

1. VPC: 이건 가상서버라는 개념이 아니라 가상랜환경이란 의미에 가까웠다. 랜환경안에 여러 호스트가 있을 수 있는..

2. 서브넷개념: 서브넷을 여러개 분리해서 둘 수 있었는데 물리적으로 다른 위치로 구성할 수 있었다. 서브넷은 쉽게 말해서 공유기나 스위치로 묶이는 작은 단위라고 볼 수 있다.

3. 인스턴스 설정후 키페어 설정후 다음 커멘드로 ssh접속이 가능했다.

ssh -i /path/my-key-pair.pem ec2-user@my-instance-public-dns-name

4. ssh로 들어가서 도커를 수동으로 시작하려면 다음명령어를 사용

sudo docker run -p 8080:8080 -e DATABASE_URL='jdbc:postgresql://sevity.com:5432/online_judge' -e DATABASE_USERNAME='online_judge_admin' -e DATABASE_PASSWORD='password_here' ee6ac0195619

 

Docker Image관련

docker images: 생성된 image나열하기

sevity@sevityubuntu:~/workspace/online_judge/problem-frontend$ docker images
REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
frontend-service   latest    34ae6e43cd4e   4 hours ago     1.92GB
problem-service    latest    a360d436d839   4 hours ago     517MB
auth-service       latest    befdc9e37ac1   4 hours ago     526MB
<none>             <none>    96430f927c57   4 hours ago     526MB
<none>             <none>    58e82ecb1610   11 hours ago    1.92GB
<none>             <none>    f9bb20adb713   35 hours ago    1.92GB
<none>             <none>    de59f27bcff9   2 days ago      1.92GB
<none>             <none>    c4cf8ec43872   3 days ago      1.92GB
<none>             <none>    25e3f42a4b64   4 days ago      1.92GB

docker system prune: 불필요한 이미지, 컨테이너, 볼륨 및 네트워크를 일괄 삭제

위의 <none>처럼 누적되는 image들 정리가능(Docker 이미지 빌드 과정에서 새로운 이미지 태그를 생성할 때마다 새로운 이미지 ID가 생성되는데, 만약 이전 이미지 태그를 유지하지 않고 새로운 태그를 계속 생성한다면, <none> 태그와 함께 이전 버전의 이미지가 누적될 수 있다. 이러한 누적은 디스크 공간을 차지하므로, 불필요한 이미지를 정리하는 것이 좋다.)

sevity@sevityubuntu:~/workspace/online_judge/problem-frontend$ docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - all dangling build cache

Are you sure you want to continue? [y/N] y
Deleted Containers:
7e93e0f073dccae0ea2ec28b7051138a38fb8137feb8bd1b9e9c5d9df1a084ab
c18f484efede43192c18b4987096409454ac358a6ca802c93061ad233d3a214a

Deleted Networks:
auth-service_default
my-network
problem-service_default

Deleted Images:
deleted: sha256:2d86f8a15708197b0dcdb54d2461d060b289ac122e1f49d27e79ada0b34f01b2
deleted: sha256:52c9722d7ab889e55af2f0f8d1540f5b218a6e40df5b5fa331bcb1a08ac6aa5b
deleted: sha256:d6d5fafa7731073ac0bf4e2d731d04dc413fae64a39de29255e6b530fada5f97
deleted: sha256:708bb9cc93ee48ba372c9d329441b1735717e09516670cf08dbe2518f02261d5

Deleted build cache objects:
i8eaq1edr2a8koj2n1o043xva
wpr37m52re2lf3gmkndy630t6

Total reclaimed space: 31.52GB

 

docker rmi: 특정 image 삭제

 

 

모든 image삭제하기(아래 과정을 거쳐야 의존성 문제가 해결되면서 다 지워진다)

# 모든 컨테이너 정지:
docker stop $(docker ps -aq)
# 모든 컨테이너 삭제:
docker rm $(docker ps -aq)
# 모든 이미지 삭제:
docker image prune -a

 

Docker Container 관련

Docker Container는 현재 실행 중인 Docker image를 의미하며

docker ps를 하면 컨테이너의 목록을 나열한다(어떤 포트를 listen하고 있는지도 알 수 있다)

$ docker ps
CONTAINER ID   IMAGE              COMMAND                  CREATED       STATUS       PORTS                                       NAMES
7e93e0f073dc   problem-service    "java -jar /app/app.…"   3 hours ago   Up 3 hours   0.0.0.0:9993->9993/tcp, :::9993->9993/tcp   cranky_yonath
b2fb854a135c   frontend-service   "docker-entrypoint.s…"   3 hours ago   Up 3 hours                                               frontend-service
b4bf33f71cf4   auth-service       "java -jar /app/app.…"   3 hours ago   Up 3 hours                                               affectionate_feynman4

docker ps --filter "expose=9993"를 하면 특정 포트만 필터링해서 볼 수도 있다.

$ docker ps --filter "expose=9993"
CONTAINER ID   IMAGE             COMMAND                  CREATED       STATUS       PORTS                                       NAMES
7e93e0f073dc   problem-service   "java -jar /app/app.…"   3 hours ago   Up 3 hours   0.0.0.0:9993->9993/tcp, :::9993->9993/tcp   cranky_yonath

docker stop을 통해 실행중인 docker container를 종료할 수 있다.

주의할점은 위의 docker ps결과에 나온 컬럼기준으로 IMAGE이름으로는 안되고 CONTAINER_ID또는 맨 오른쪽 컬럼인 NAMES를 인자로 주어야 동작한다는 점이다.

# IMAGE이름으로는 stop이 안된다.
$ docker stop problem-service
Error response from daemon: No such container: problem-service

# CONTAINER_ID나 NAME으로는 가능
$ docker stop 7e93e0f073dc
7e93e0f073dc

만약 IMAGE이름으로 stop하고 싶다면 다음처럼 해준다.

# problem-service라는 이름의 IMAGE로 부터 생성된 container 모두를 stop한다.
$ docker ps -q --filter ancestor=problem-service | xargs docker stop
7e93e0f073dc

 

docker stop을 했다고 해도 지워진건 아니며 docker rm을 통해 지워줘야 한다.

stop된 컨테이너를 포함해서 조회하려면 docker ps -a를 해주면 된다.

 

 

 

 

 

 

 

 

 

 

 

 

반응형

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

하둡 - 실습 - 웹서버로그분석  (0) 2023.07.29
하둡(Hadoop)  (0) 2023.07.29
vimdiff  (0) 2021.04.07
X Window System(X11) - 여러 호스트에서 ssh로 붙어서 사용하기  (0) 2021.01.08
Ansible  (1) 2020.10.22

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>
반응형

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

tf flash또는 USB에 Raspberry Pi OS설치

https://www.raspberrypi.com/software/

 

Raspberry Pi OS – Raspberry Pi

From industries large and small, to the kitchen table tinkerer, to the classroom coder, we make computing accessible and affordable for everybody.

www.raspberrypi.com

여기서 안내에 따라 진행

 

ssh enable

sudo raspi-config

Interfacing Options > SSH > Yes

여기 참조

 

반응형

'Programming' 카테고리의 다른 글

window에서 vscode로 원격 linux에 대한 ssh 개발환경 설정하기  (0) 2024.07.20
yaml  (0) 2024.03.02
디자인패턴  (0) 2023.08.17
STL lower_bound, upper_bound  (0) 2020.04.12

여기, 여기 참조

 

브루트포스로 풀기에는 N이 약간만 너무 클때, 

분할정복처럼 절반씩 나누고 연산한다음 합치는 연산을 하는걸 meet in the middle이라고 한다.

분할정복처럼 반복적으로 나누고 합치고 하지는 않고 1회성으로만 하는거 같다.

 

합치는 연산은 문제마다 다르며, 투포인터가 사용될때도 있다.

샘플 문제는 여기를 참고하자.

 

아직 정확히 모르는점들

meet in the middle을 반복하면 계속해서 복잡도를 줄일 수 있는가? 만약 가능하다면 분할정복과의 차이점은?

합치는 연산에서 투포인터 말고 다른게 쓰는 예는 어떤게 있는가?

  • 요건 이분탐색이 있는데, 이분탐색도 직접구현 기준으로 보면 left, right 포인터를 쓰는거라 투포인터의 일종으로 볼 수는 있을듯?

 

반응형

투 포인터는 한 가지 알고리즘을 말하는 것이라기보다,

포인터 i, j 두 개를 사용해서, 브루트 포스로는 O(N^2)이 걸리는 문제를,

O(N)으로 만드는 그리디 알고리즘의 모음라고 보는 게 좋다.

 

따라서, 투 포인터는 예제를 통해서 익히는 게 좋고,

투 포인터를 통해서 그리디가 가능한 문제는 부분합,

배열의 두 값의 합이나 차가 특정 수가 되는지(A [i]+A [j]=S) 찾는 문제 등이 있다.

위 링크들은 백준 문제로 연결된다.

 

구현 난이도는, 아무래도 그리디다 보니 조건절이 브루트 포스보다는 더 들어가야 해서,

조금 더 어렵고 주의를 요한다고 볼 수 있다.

 

 

반응형

그림 1

위처럼,

어떤 그래프를 V1과 V2 그룹으로 나누었을때,

그룹내의 정점끼리는 간선이 없고 그룹간 간선만 존재할 때 이분 그래프라고 한다.

 

주어진 그래프가 이분 그래프인지 판정하기

그림2

undirected 이면서 모든 두 꼭지점의 연결 경로가 존재하는 연결 그래프(connected graph)인 경우,

아무노드나 잡고, BFS나 DFS로 위처럼 색칠을 해 나가다가 색깔의 conflict 가 있으면 이분 그래프가 아니고, 없으면 이분 그래프로 판정 가능하다.

 

undirected 이면서 연결 그래프는 아닌경우(위의 그림1도 연결그래프는 아님),

이경우도 단순하게 분리된 각각의 연결 그래프에 대해서 모두 위의 색칠로직을 적용해주면 됨(모든 분리된 그래프가 이분 그래프여야 전체도 이분그래프인걸로 판정)

아래 소스코드 참조. 이 문제의 답안이기도 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for(int i=0;i<(int)(n);i++)
enum Color {BLACK, BLUE, RED};
int32_t main()
{
    int K; scanf("%d"&K);
    while (K--) {
        int V, E; scanf("%d%d"&V, &E);
        vector<vector<int>> adj_list(V + 1);
        while (E--) {
            int u, v; scanf("%d%d"&u, &v);
            adj_list[u].push_back(v);
            adj_list[v].push_back(u);
        }
        stack<int> qn; REP(i, V)qn.push(i + 1);
        vector<int> color(V + 1, BLACK);
        bool ans = true;
        while (ans == true && qn.empty() == false) {
            int n = qn.top(); qn.pop();
            if (color[n] == BLACK) color[n] = BLUE;
            for (auto adj : adj_list[n]) {
                if (color[adj] == BLACK) {
                    color[adj] = (color[n] == BLUE) ? RED : BLUE;
                    qn.push(adj);
                }
                else if (color[adj] == color[n]) {
                    ans = false;
                    break;
                }
            }
        }
        printf("%s\n", ans ? "YES" : "NO");
    }
    return 0;
}
 
cs

 

directed인경우,

이 경우도 판정 가능한거 같은데, 자세히 알아보진 않았다.

 

 

 

 

반응형

+ Recent posts